oxlint-plugin-react-doctor 0.6.1 → 0.6.2-dev.173cc0a
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 +1205 -742
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -212,8 +212,16 @@ const sliceBelowSourceRoot = (filename) => {
|
|
|
212
212
|
if (cutAt < 0) return filename;
|
|
213
213
|
return filename.slice(cutAt);
|
|
214
214
|
};
|
|
215
|
+
let lastFilename;
|
|
216
|
+
let lastResult = false;
|
|
215
217
|
const isTestlikeFilename = (rawFilename) => {
|
|
216
218
|
if (!rawFilename) return false;
|
|
219
|
+
if (rawFilename === lastFilename) return lastResult;
|
|
220
|
+
lastFilename = rawFilename;
|
|
221
|
+
lastResult = computeIsTestlikeFilename(rawFilename);
|
|
222
|
+
return lastResult;
|
|
223
|
+
};
|
|
224
|
+
const computeIsTestlikeFilename = (rawFilename) => {
|
|
217
225
|
const filename = rawFilename.replaceAll("\\", "/");
|
|
218
226
|
const lastSlash = filename.lastIndexOf("/");
|
|
219
227
|
const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
|
|
@@ -286,15 +294,38 @@ const defineRule = (rule) => {
|
|
|
286
294
|
};
|
|
287
295
|
//#endregion
|
|
288
296
|
//#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
|
|
297
|
+
let lastContentLineIndex;
|
|
298
|
+
const buildLineStartOffsets = (content) => {
|
|
299
|
+
const lineStartOffsets = [0];
|
|
300
|
+
for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
|
|
301
|
+
return lineStartOffsets;
|
|
302
|
+
};
|
|
303
|
+
const getLineStartOffsets = (content) => {
|
|
304
|
+
if (lastContentLineIndex?.content === content) return lastContentLineIndex.lineStartOffsets;
|
|
305
|
+
const lineStartOffsets = buildLineStartOffsets(content);
|
|
306
|
+
lastContentLineIndex = {
|
|
307
|
+
content,
|
|
308
|
+
lineStartOffsets
|
|
309
|
+
};
|
|
310
|
+
return lineStartOffsets;
|
|
311
|
+
};
|
|
289
312
|
const getLocationAtIndex = (content, matchIndex) => {
|
|
290
|
-
if (matchIndex < 0) return {
|
|
313
|
+
if (Number.isNaN(matchIndex) || matchIndex < 0) return {
|
|
291
314
|
line: 1,
|
|
292
315
|
column: 1
|
|
293
316
|
};
|
|
294
|
-
const
|
|
317
|
+
const lineStartOffsets = getLineStartOffsets(content);
|
|
318
|
+
const boundedIndex = Math.min(Math.trunc(matchIndex), content.length);
|
|
319
|
+
let lowLineIndex = 0;
|
|
320
|
+
let highLineIndex = lineStartOffsets.length - 1;
|
|
321
|
+
while (lowLineIndex < highLineIndex) {
|
|
322
|
+
const midLineIndex = lowLineIndex + highLineIndex + 1 >> 1;
|
|
323
|
+
if (lineStartOffsets[midLineIndex] <= boundedIndex) lowLineIndex = midLineIndex;
|
|
324
|
+
else highLineIndex = midLineIndex - 1;
|
|
325
|
+
}
|
|
295
326
|
return {
|
|
296
|
-
line:
|
|
297
|
-
column:
|
|
327
|
+
line: lowLineIndex + 1,
|
|
328
|
+
column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
|
|
298
329
|
};
|
|
299
330
|
};
|
|
300
331
|
//#endregion
|
|
@@ -347,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
|
347
378
|
const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
|
|
348
379
|
//#endregion
|
|
349
380
|
//#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
|
|
381
|
+
const classificationCacheByPattern = /* @__PURE__ */ new Map();
|
|
350
382
|
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
383
|
+
let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
|
|
384
|
+
if (!classificationByPath) {
|
|
385
|
+
classificationByPath = /* @__PURE__ */ new Map();
|
|
386
|
+
classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
|
|
387
|
+
}
|
|
388
|
+
const cached = classificationByPath.get(relativePath);
|
|
389
|
+
if (cached !== void 0) return cached;
|
|
390
|
+
const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
|
|
391
|
+
classificationByPath.set(relativePath, isProduction);
|
|
392
|
+
return isProduction;
|
|
357
393
|
};
|
|
358
394
|
//#endregion
|
|
359
395
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
@@ -552,7 +588,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
|
|
|
552
588
|
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
553
589
|
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
554
590
|
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
555
|
-
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
591
|
+
const HOOK_NAME_PATTERN$1 = /^use[A-Z]/;
|
|
556
592
|
const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
|
|
557
593
|
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
558
594
|
const HOOKS_WITH_DEPS = new Set([
|
|
@@ -752,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
|
|
|
752
788
|
into.add(name);
|
|
753
789
|
});
|
|
754
790
|
};
|
|
755
|
-
const findSameFileComponentBody = (programRoot, componentName) => {
|
|
756
|
-
let foundBody = null;
|
|
757
|
-
walkAst(programRoot, (node) => {
|
|
758
|
-
if (foundBody) return false;
|
|
759
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
|
|
760
|
-
foundBody = node.body;
|
|
761
|
-
return false;
|
|
762
|
-
}
|
|
763
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
|
|
764
|
-
const initializer = node.init;
|
|
765
|
-
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
|
|
766
|
-
foundBody = initializer.body;
|
|
767
|
-
return false;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
return foundBody;
|
|
772
|
-
};
|
|
773
791
|
const countEffectHookCalls = (body) => {
|
|
774
792
|
if (!body) return 0;
|
|
775
793
|
let count = 0;
|
|
@@ -779,6 +797,36 @@ const countEffectHookCalls = (body) => {
|
|
|
779
797
|
});
|
|
780
798
|
return count;
|
|
781
799
|
};
|
|
800
|
+
const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
|
|
801
|
+
const getComponentEffectIndex = (programRoot) => {
|
|
802
|
+
const cached = componentEffectIndexCache.get(programRoot);
|
|
803
|
+
if (cached) return cached;
|
|
804
|
+
const bodyByName = /* @__PURE__ */ new Map();
|
|
805
|
+
walkAst(programRoot, (node) => {
|
|
806
|
+
if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
|
|
807
|
+
bodyByName.set(node.id.name, node.body);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
|
|
811
|
+
const initializer = node.init;
|
|
812
|
+
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
const index = {
|
|
816
|
+
bodyByName,
|
|
817
|
+
effectCountByName: /* @__PURE__ */ new Map()
|
|
818
|
+
};
|
|
819
|
+
componentEffectIndexCache.set(programRoot, index);
|
|
820
|
+
return index;
|
|
821
|
+
};
|
|
822
|
+
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
823
|
+
const index = getComponentEffectIndex(programRoot);
|
|
824
|
+
const cachedCount = index.effectCountByName.get(componentName);
|
|
825
|
+
if (cachedCount !== void 0) return cachedCount;
|
|
826
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
827
|
+
index.effectCountByName.set(componentName, count);
|
|
828
|
+
return count;
|
|
829
|
+
};
|
|
782
830
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
783
831
|
id: "activity-wraps-effect-heavy-subtree",
|
|
784
832
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -835,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
835
883
|
let totalEffects = 0;
|
|
836
884
|
const effectfulChildren = [];
|
|
837
885
|
for (const componentName of childComponentNames) {
|
|
838
|
-
const
|
|
839
|
-
if (!body) continue;
|
|
840
|
-
const effectCount = countEffectHookCalls(body);
|
|
886
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
841
887
|
if (effectCount === 0) continue;
|
|
842
888
|
totalEffects += effectCount;
|
|
843
889
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -983,16 +1029,8 @@ const buildBindingIndex = (root) => {
|
|
|
983
1029
|
}
|
|
984
1030
|
}
|
|
985
1031
|
}
|
|
986
|
-
const nodeRecord = node;
|
|
987
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
988
|
-
if (key === "parent") continue;
|
|
989
|
-
const child = nodeRecord[key];
|
|
990
|
-
if (Array.isArray(child)) {
|
|
991
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
992
|
-
} else if (isAstNode(child)) visit(child);
|
|
993
|
-
}
|
|
994
1032
|
};
|
|
995
|
-
|
|
1033
|
+
walkAst(root, visit);
|
|
996
1034
|
return out;
|
|
997
1035
|
};
|
|
998
1036
|
const programRootCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1394,11 +1432,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1394
1432
|
};
|
|
1395
1433
|
//#endregion
|
|
1396
1434
|
//#region src/plugin/utils/get-element-type.ts
|
|
1435
|
+
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1436
|
+
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
1397
1437
|
const readJsxA11ySettings = (settings) => {
|
|
1398
|
-
if (!settings) return
|
|
1438
|
+
if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
|
|
1439
|
+
const cachedSettings = jsxA11ySettingsCache.get(settings);
|
|
1440
|
+
if (cachedSettings) return cachedSettings;
|
|
1399
1441
|
const block = settings["jsx-a11y"];
|
|
1400
|
-
|
|
1401
|
-
|
|
1442
|
+
const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
|
|
1443
|
+
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1444
|
+
return a11ySettings;
|
|
1402
1445
|
};
|
|
1403
1446
|
const flattenJsxName$2 = (name) => {
|
|
1404
1447
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
@@ -1406,8 +1449,7 @@ const flattenJsxName$2 = (name) => {
|
|
|
1406
1449
|
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1407
1450
|
return "";
|
|
1408
1451
|
};
|
|
1409
|
-
const
|
|
1410
|
-
const a11ySettings = readJsxA11ySettings(settings);
|
|
1452
|
+
const computeElementType = (openingElement, a11ySettings) => {
|
|
1411
1453
|
const baseName = flattenJsxName$2(openingElement.name);
|
|
1412
1454
|
if (a11ySettings.polymorphicPropName) {
|
|
1413
1455
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
@@ -1419,53 +1461,61 @@ const getElementType = (openingElement, settings) => {
|
|
|
1419
1461
|
if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
|
|
1420
1462
|
return baseName;
|
|
1421
1463
|
};
|
|
1464
|
+
const elementTypeCache = /* @__PURE__ */ new WeakMap();
|
|
1465
|
+
const getElementType = (openingElement, settings) => {
|
|
1466
|
+
const cachedEntry = elementTypeCache.get(openingElement);
|
|
1467
|
+
if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
|
|
1468
|
+
const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
|
|
1469
|
+
elementTypeCache.set(openingElement, {
|
|
1470
|
+
settings,
|
|
1471
|
+
elementType
|
|
1472
|
+
});
|
|
1473
|
+
return elementType;
|
|
1474
|
+
};
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/plugin/utils/has-jsx-a11y-settings.ts
|
|
1477
|
+
const hasJsxA11ySettings = (settings) => {
|
|
1478
|
+
const block = settings?.["jsx-a11y"];
|
|
1479
|
+
return typeof block === "object" && block !== null;
|
|
1480
|
+
};
|
|
1422
1481
|
//#endregion
|
|
1423
1482
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1424
1483
|
const collectFromProgram = (programRoot) => {
|
|
1425
1484
|
const lookup = /* @__PURE__ */ new Map();
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1485
|
+
const bodyStatements = programRoot.body ?? [];
|
|
1486
|
+
for (const node of bodyStatements) {
|
|
1487
|
+
if (node.type !== "ImportDeclaration" || !("source" in node) || !node.source) continue;
|
|
1488
|
+
const source = node.source.value;
|
|
1489
|
+
if (typeof source !== "string") continue;
|
|
1490
|
+
if (!("specifiers" in node) || !Array.isArray(node.specifiers)) continue;
|
|
1491
|
+
for (const specifier of node.specifiers) {
|
|
1492
|
+
if (!("local" in specifier) || !specifier.local) continue;
|
|
1493
|
+
const local = specifier.local;
|
|
1494
|
+
if (typeof local.name !== "string") continue;
|
|
1495
|
+
if (specifier.type === "ImportDefaultSpecifier") lookup.set(local.name, {
|
|
1496
|
+
source,
|
|
1497
|
+
imported: null,
|
|
1498
|
+
isDefault: true,
|
|
1499
|
+
isNamespace: false
|
|
1500
|
+
});
|
|
1501
|
+
else if (specifier.type === "ImportNamespaceSpecifier") lookup.set(local.name, {
|
|
1502
|
+
source,
|
|
1503
|
+
imported: null,
|
|
1504
|
+
isDefault: false,
|
|
1505
|
+
isNamespace: true
|
|
1506
|
+
});
|
|
1507
|
+
else if (specifier.type === "ImportSpecifier") {
|
|
1508
|
+
const importedNode = specifier.imported;
|
|
1509
|
+
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
1510
|
+
lookup.set(local.name, {
|
|
1441
1511
|
source,
|
|
1442
|
-
imported:
|
|
1512
|
+
imported: importedName,
|
|
1443
1513
|
isDefault: false,
|
|
1444
|
-
isNamespace:
|
|
1514
|
+
isNamespace: false
|
|
1445
1515
|
});
|
|
1446
|
-
else if (specifier.type === "ImportSpecifier") {
|
|
1447
|
-
const importedNode = specifier.imported;
|
|
1448
|
-
const importedName = importedNode?.name ?? (typeof importedNode?.value === "string" ? importedNode.value : null);
|
|
1449
|
-
lookup.set(local.name, {
|
|
1450
|
-
source,
|
|
1451
|
-
imported: importedName,
|
|
1452
|
-
isDefault: false,
|
|
1453
|
-
isNamespace: false
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
1516
|
}
|
|
1457
|
-
return;
|
|
1458
|
-
}
|
|
1459
|
-
const nodeRecord = node;
|
|
1460
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
1461
|
-
if (key === "parent") continue;
|
|
1462
|
-
const child = nodeRecord[key];
|
|
1463
|
-
if (Array.isArray(child)) {
|
|
1464
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
1465
|
-
} else if (isAstNode(child)) visit(child);
|
|
1466
1517
|
}
|
|
1467
|
-
}
|
|
1468
|
-
visit(programRoot);
|
|
1518
|
+
}
|
|
1469
1519
|
return lookup;
|
|
1470
1520
|
};
|
|
1471
1521
|
const importLookupCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1479,6 +1529,12 @@ const getImportLookup = (node) => {
|
|
|
1479
1529
|
}
|
|
1480
1530
|
return cached;
|
|
1481
1531
|
};
|
|
1532
|
+
const hasImportFromModules = (contextNode, moduleSources) => {
|
|
1533
|
+
const lookup = getImportLookup(contextNode);
|
|
1534
|
+
if (!lookup) return false;
|
|
1535
|
+
for (const info of lookup.values()) if (moduleSources.includes(info.source)) return true;
|
|
1536
|
+
return false;
|
|
1537
|
+
};
|
|
1482
1538
|
const isImportedFromModule = (contextNode, localIdentifierName, moduleSource) => {
|
|
1483
1539
|
const lookup = getImportLookup(contextNode);
|
|
1484
1540
|
if (!lookup) return false;
|
|
@@ -1626,11 +1682,12 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1626
1682
|
};
|
|
1627
1683
|
//#endregion
|
|
1628
1684
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1629
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1685
|
+
const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
|
|
1630
1686
|
//#endregion
|
|
1631
1687
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1632
1688
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
1633
1689
|
const SATORI_MODULE = "satori";
|
|
1690
|
+
const GENERATED_IMAGE_MODULES = [...IMAGE_RESPONSE_MODULES, SATORI_MODULE];
|
|
1634
1691
|
const generatedImageJsxCache = /* @__PURE__ */ new WeakMap();
|
|
1635
1692
|
const isGeneratedImageRenderFilename = (rawFilename) => {
|
|
1636
1693
|
if (!rawFilename) return false;
|
|
@@ -1774,7 +1831,7 @@ const collectGeneratedImageJsxNodes = (programRoot) => {
|
|
|
1774
1831
|
const cached = generatedImageJsxCache.get(programRoot);
|
|
1775
1832
|
if (cached) return cached;
|
|
1776
1833
|
const generatedImageJsxNodes = /* @__PURE__ */ new WeakSet();
|
|
1777
|
-
walkAst(programRoot, (descendantNode) => {
|
|
1834
|
+
if (hasImportFromModules(programRoot, GENERATED_IMAGE_MODULES)) walkAst(programRoot, (descendantNode) => {
|
|
1778
1835
|
if (!isGeneratedImageRendererCall(descendantNode)) return;
|
|
1779
1836
|
for (const argument of descendantNode.arguments) markGeneratedImageExpression(argument, programRoot, generatedImageJsxNodes, /* @__PURE__ */ new Set());
|
|
1780
1837
|
});
|
|
@@ -1974,7 +2031,12 @@ const altText = defineRule({
|
|
|
1974
2031
|
const objectAliases = new Set(settings.object ?? []);
|
|
1975
2032
|
const areaAliases = new Set(settings.area ?? []);
|
|
1976
2033
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2034
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
1977
2035
|
return { JSXOpeningElement(node) {
|
|
2036
|
+
if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
|
|
2037
|
+
const rawName = node.name.name;
|
|
2038
|
+
if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
|
|
2039
|
+
}
|
|
1978
2040
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1979
2041
|
const tag = getElementType(node, context.settings);
|
|
1980
2042
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2157,7 +2219,9 @@ const anchorIsValid = defineRule({
|
|
|
2157
2219
|
category: "Accessibility",
|
|
2158
2220
|
create: (context) => {
|
|
2159
2221
|
const settings = resolveSettings$50(context.settings);
|
|
2222
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2160
2223
|
return { JSXOpeningElement(node) {
|
|
2224
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2161
2225
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2162
2226
|
let hrefAttribute;
|
|
2163
2227
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3460,7 +3524,7 @@ const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
|
3460
3524
|
];
|
|
3461
3525
|
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
3462
3526
|
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
3463
|
-
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
3527
|
+
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth(?!or(?!i[sz])))/i;
|
|
3464
3528
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
3465
3529
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
3466
3530
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
@@ -3583,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3583
3647
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3584
3648
|
//#endregion
|
|
3585
3649
|
//#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
|
|
3650
|
+
const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
|
|
3586
3651
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3587
|
-
for (const match of content.matchAll(
|
|
3652
|
+
for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
|
|
3588
3653
|
const value = match[0] ?? "";
|
|
3589
3654
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3590
3655
|
}
|
|
@@ -3967,9 +4032,11 @@ const collectScopedReferenceIdentifierNames = (node, into, shadowed) => {
|
|
|
3967
4032
|
return;
|
|
3968
4033
|
}
|
|
3969
4034
|
if (typeof node.type === "string" && node.type.startsWith("TS")) return;
|
|
3970
|
-
|
|
4035
|
+
const nodeRecord = node;
|
|
4036
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
3971
4037
|
if (key === "parent") continue;
|
|
3972
4038
|
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
4039
|
+
const child = nodeRecord[key];
|
|
3973
4040
|
if (Array.isArray(child)) {
|
|
3974
4041
|
for (const item of child) if (isAstNode(item)) collectScopedReferenceIdentifierNames(item, into, shadowed);
|
|
3975
4042
|
} else if (isAstNode(child)) collectScopedReferenceIdentifierNames(child, into, shadowed);
|
|
@@ -4049,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4049
4116
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4050
4117
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4051
4118
|
};
|
|
4052
|
-
const collectAssignedIdentifiers = (block) => {
|
|
4053
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
4054
|
-
walkAst(block, (child) => {
|
|
4055
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4056
|
-
if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
|
|
4057
|
-
});
|
|
4058
|
-
return assigned;
|
|
4059
|
-
};
|
|
4060
|
-
const collectAwaitedArgIdentifiers = (block) => {
|
|
4061
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
4062
|
-
walkAst(block, (child) => {
|
|
4063
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4064
|
-
if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
|
|
4065
|
-
collectReferenceIdentifierNames(child.argument, referenced);
|
|
4066
|
-
});
|
|
4067
|
-
return referenced;
|
|
4068
|
-
};
|
|
4069
4119
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4070
4120
|
"push",
|
|
4071
4121
|
"unshift",
|
|
4072
4122
|
"splice"
|
|
4073
4123
|
]);
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4124
|
+
const addDerivedBindings = (block, names) => {
|
|
4125
|
+
const declaratorBindings = [];
|
|
4076
4126
|
walkAst(block, (child) => {
|
|
4077
4127
|
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4078
|
-
if (!isNodeOfType(child, "
|
|
4079
|
-
|
|
4080
|
-
|
|
4128
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
4129
|
+
if (!isNodeOfType(child.id, "Identifier")) return;
|
|
4130
|
+
const referencedNames = /* @__PURE__ */ new Set();
|
|
4131
|
+
collectReferenceIdentifierNames(child.init, referencedNames);
|
|
4132
|
+
declaratorBindings.push({
|
|
4133
|
+
declaredName: child.id.name,
|
|
4134
|
+
referencedNames
|
|
4135
|
+
});
|
|
4081
4136
|
});
|
|
4082
|
-
return mutated;
|
|
4083
|
-
};
|
|
4084
|
-
const addDerivedBindings = (block, names) => {
|
|
4085
4137
|
let didGrow = true;
|
|
4086
4138
|
while (didGrow) {
|
|
4087
4139
|
didGrow = false;
|
|
4088
|
-
|
|
4089
|
-
if (
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
const initReferences = /* @__PURE__ */ new Set();
|
|
4093
|
-
collectReferenceIdentifierNames(child.init, initReferences);
|
|
4094
|
-
for (const referenced of initReferences) if (names.has(referenced)) {
|
|
4095
|
-
names.add(child.id.name);
|
|
4140
|
+
for (const { declaredName, referencedNames } of declaratorBindings) {
|
|
4141
|
+
if (names.has(declaredName)) continue;
|
|
4142
|
+
for (const referenced of referencedNames) if (names.has(referenced)) {
|
|
4143
|
+
names.add(declaredName);
|
|
4096
4144
|
didGrow = true;
|
|
4097
4145
|
break;
|
|
4098
4146
|
}
|
|
4099
|
-
}
|
|
4147
|
+
}
|
|
4100
4148
|
}
|
|
4101
4149
|
};
|
|
4102
4150
|
const hasLoopCarriedDependency = (block) => {
|
|
4103
|
-
const carried =
|
|
4104
|
-
|
|
4151
|
+
const carried = /* @__PURE__ */ new Set();
|
|
4152
|
+
const awaitedReferences = /* @__PURE__ */ new Set();
|
|
4153
|
+
walkAst(block, (child) => {
|
|
4154
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4155
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.left) {
|
|
4156
|
+
collectPatternIdentifiers(child.left, carried);
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
if (isNodeOfType(child, "AwaitExpression") && child.argument) {
|
|
4160
|
+
collectReferenceIdentifierNames(child.argument, awaitedReferences);
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
4164
|
+
const callee = child.callee;
|
|
4165
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) carried.add(callee.object.name);
|
|
4166
|
+
});
|
|
4105
4167
|
if (carried.size === 0) return false;
|
|
4106
4168
|
addDerivedBindings(block, carried);
|
|
4107
|
-
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4108
4169
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4109
4170
|
return false;
|
|
4110
4171
|
};
|
|
@@ -4782,7 +4843,8 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
4782
4843
|
]);
|
|
4783
4844
|
const resolveSettings$48 = (settings) => {
|
|
4784
4845
|
const reactDoctor = settings?.["react-doctor"];
|
|
4785
|
-
|
|
4846
|
+
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
|
|
4847
|
+
return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
|
|
4786
4848
|
};
|
|
4787
4849
|
const autocompleteValid = defineRule({
|
|
4788
4850
|
id: "autocomplete-valid",
|
|
@@ -4795,7 +4857,7 @@ const autocompleteValid = defineRule({
|
|
|
4795
4857
|
const settings = resolveSettings$48(context.settings);
|
|
4796
4858
|
return { JSXOpeningElement: (node) => {
|
|
4797
4859
|
const tag = getElementType(node, context.settings);
|
|
4798
|
-
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.
|
|
4860
|
+
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
|
|
4799
4861
|
const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
|
|
4800
4862
|
if (!attribute) return;
|
|
4801
4863
|
const value = getJsxPropStringValue(attribute);
|
|
@@ -4839,9 +4901,8 @@ const isCreateElementCall = (node) => {
|
|
|
4839
4901
|
const callee = node.callee;
|
|
4840
4902
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
|
|
4841
4903
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
4842
|
-
if (
|
|
4843
|
-
|
|
4844
|
-
return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
|
|
4904
|
+
if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
|
|
4905
|
+
return !memberChainContainsDocument(callee.object);
|
|
4845
4906
|
}
|
|
4846
4907
|
return false;
|
|
4847
4908
|
};
|
|
@@ -4867,7 +4928,8 @@ const isValidTypeValue = (rawValue, settings) => {
|
|
|
4867
4928
|
if (rawValue === "reset") return settings.reset;
|
|
4868
4929
|
return false;
|
|
4869
4930
|
};
|
|
4870
|
-
const isProvenValidExpression = (
|
|
4931
|
+
const isProvenValidExpression = (rawExpression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
|
|
4932
|
+
const expression = stripParenExpression(rawExpression);
|
|
4871
4933
|
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return isValidTypeValue(expression.value, settings);
|
|
4872
4934
|
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
4873
4935
|
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
@@ -5034,11 +5096,20 @@ const resolveSettings$46 = (settings) => {
|
|
|
5034
5096
|
ignoreExclusiveCheckedAttribute: ruleSettings.ignoreExclusiveCheckedAttribute ?? false
|
|
5035
5097
|
};
|
|
5036
5098
|
};
|
|
5099
|
+
const isTruthyDisabledJsxValue = (attribute) => {
|
|
5100
|
+
if (!attribute.value) return true;
|
|
5101
|
+
if (isNodeOfType(attribute.value, "JSXExpressionContainer")) {
|
|
5102
|
+
const expression = attribute.value.expression;
|
|
5103
|
+
return isNodeOfType(expression, "Literal") && expression.value === true;
|
|
5104
|
+
}
|
|
5105
|
+
return false;
|
|
5106
|
+
};
|
|
5037
5107
|
const collectFromJsxAttributes = (attributes) => {
|
|
5038
5108
|
let checkedNode = null;
|
|
5039
5109
|
let defaultCheckedNode = null;
|
|
5040
5110
|
let hasOnChangeOrReadOnly = false;
|
|
5041
5111
|
let hasSpread = false;
|
|
5112
|
+
let hasTruthyDisabled = false;
|
|
5042
5113
|
for (const attribute of attributes) {
|
|
5043
5114
|
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
5044
5115
|
hasSpread = true;
|
|
@@ -5049,12 +5120,14 @@ const collectFromJsxAttributes = (attributes) => {
|
|
|
5049
5120
|
if (name === "checked") checkedNode = attribute;
|
|
5050
5121
|
else if (name === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = attribute;
|
|
5051
5122
|
else if (name === "onChange" || name === "readOnly") hasOnChangeOrReadOnly = true;
|
|
5123
|
+
else if (name === "disabled" && isTruthyDisabledJsxValue(attribute)) hasTruthyDisabled = true;
|
|
5052
5124
|
}
|
|
5053
5125
|
return {
|
|
5054
5126
|
checkedNode,
|
|
5055
5127
|
defaultCheckedNode,
|
|
5056
5128
|
hasOnChangeOrReadOnly,
|
|
5057
|
-
hasSpread
|
|
5129
|
+
hasSpread,
|
|
5130
|
+
hasTruthyDisabled
|
|
5058
5131
|
};
|
|
5059
5132
|
};
|
|
5060
5133
|
const collectFromObjectProperties = (objectExpression) => {
|
|
@@ -5062,6 +5135,7 @@ const collectFromObjectProperties = (objectExpression) => {
|
|
|
5062
5135
|
let defaultCheckedNode = null;
|
|
5063
5136
|
let hasOnChangeOrReadOnly = false;
|
|
5064
5137
|
let hasSpread = false;
|
|
5138
|
+
let hasTruthyDisabled = false;
|
|
5065
5139
|
for (const property of objectExpression.properties) {
|
|
5066
5140
|
if (isNodeOfType(property, "SpreadElement")) {
|
|
5067
5141
|
hasSpread = true;
|
|
@@ -5076,12 +5150,17 @@ const collectFromObjectProperties = (objectExpression) => {
|
|
|
5076
5150
|
if (propertyName === "checked") checkedNode = property;
|
|
5077
5151
|
else if (propertyName === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = property;
|
|
5078
5152
|
else if (propertyName === "onChange" || propertyName === "readOnly") hasOnChangeOrReadOnly = true;
|
|
5153
|
+
else if (propertyName === "disabled") {
|
|
5154
|
+
const propertyValue = property.value;
|
|
5155
|
+
if (isNodeOfType(propertyValue, "Literal") && propertyValue.value === true) hasTruthyDisabled = true;
|
|
5156
|
+
}
|
|
5079
5157
|
}
|
|
5080
5158
|
return {
|
|
5081
5159
|
checkedNode,
|
|
5082
5160
|
defaultCheckedNode,
|
|
5083
5161
|
hasOnChangeOrReadOnly,
|
|
5084
|
-
hasSpread
|
|
5162
|
+
hasSpread,
|
|
5163
|
+
hasTruthyDisabled
|
|
5085
5164
|
};
|
|
5086
5165
|
};
|
|
5087
5166
|
const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
@@ -5097,7 +5176,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5097
5176
|
node: presence.checkedNode,
|
|
5098
5177
|
message: EXCLUSIVE_MESSAGE
|
|
5099
5178
|
});
|
|
5100
|
-
if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !settings.ignoreMissingProperties) context.report({
|
|
5179
|
+
if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !presence.hasTruthyDisabled && !settings.ignoreMissingProperties) context.report({
|
|
5101
5180
|
node: presence.checkedNode,
|
|
5102
5181
|
message: MISSING_MESSAGE$1
|
|
5103
5182
|
});
|
|
@@ -5658,12 +5737,20 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5658
5737
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5659
5738
|
};
|
|
5660
5739
|
const isInertParseTarget = (target, fileContent) => {
|
|
5740
|
+
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5741
|
+
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5742
|
+
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5661
5743
|
const escapedTarget = escapeRegExp(target);
|
|
5662
5744
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5663
5745
|
if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
|
|
5664
|
-
if (
|
|
5665
|
-
|
|
5666
|
-
|
|
5746
|
+
if (fileHasCreateElement) {
|
|
5747
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5748
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5749
|
+
}
|
|
5750
|
+
if (fileHasIsolatedDocument) {
|
|
5751
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5752
|
+
}
|
|
5753
|
+
if (!fileHasCreateElement) return false;
|
|
5667
5754
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5668
5755
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5669
5756
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5756,10 +5843,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
5756
5843
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5757
5844
|
if (valueIdentifier !== void 0) {
|
|
5758
5845
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5846
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5847
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5848
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
5763
5849
|
}
|
|
5764
5850
|
}
|
|
5765
5851
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5908,6 +5994,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5908
5994
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5909
5995
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5910
5996
|
if (!classNameLiteral) return;
|
|
5997
|
+
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5911
5998
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5912
5999
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5913
6000
|
if (matchedPairs.length === 0) return;
|
|
@@ -5932,6 +6019,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
5932
6019
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5933
6020
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5934
6021
|
if (!classNameLiteral) return;
|
|
6022
|
+
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
5935
6023
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
5936
6024
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
5937
6025
|
if (matchedPairs.length === 0) return;
|
|
@@ -5956,6 +6044,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
5956
6044
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5957
6045
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5958
6046
|
if (!classNameLiteral) return;
|
|
6047
|
+
if (!classNameLiteral.includes("space-")) return;
|
|
5959
6048
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
5960
6049
|
let hasFlexOrGridLayout = false;
|
|
5961
6050
|
for (const token of tokens) {
|
|
@@ -6147,7 +6236,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6147
6236
|
const actualMinor = Number(match[2]);
|
|
6148
6237
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6149
6238
|
};
|
|
6239
|
+
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6150
6240
|
const containsJsx$1 = (root) => {
|
|
6241
|
+
const cached = containsJsxCache.get(root);
|
|
6242
|
+
if (cached !== void 0) return cached;
|
|
6151
6243
|
let found = false;
|
|
6152
6244
|
const visit = (node) => {
|
|
6153
6245
|
if (found) return;
|
|
@@ -6170,6 +6262,7 @@ const containsJsx$1 = (root) => {
|
|
|
6170
6262
|
}
|
|
6171
6263
|
};
|
|
6172
6264
|
visit(root);
|
|
6265
|
+
containsJsxCache.set(root, found);
|
|
6173
6266
|
return found;
|
|
6174
6267
|
};
|
|
6175
6268
|
const getStaticMemberName = (node) => {
|
|
@@ -6261,27 +6354,6 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6261
6354
|
for (const member of members) if ((isNodeOfType(member, "PropertyDefinition") || isNodeOfType(member, "MethodDefinition")) && "static" in member && member.static && isNodeOfType(member.key, "Identifier") && member.key.name === "displayName") return true;
|
|
6262
6355
|
return false;
|
|
6263
6356
|
};
|
|
6264
|
-
const hasDisplayNameAssignment = (className, programRoot) => {
|
|
6265
|
-
let found = false;
|
|
6266
|
-
const visit = (node) => {
|
|
6267
|
-
if (found) return;
|
|
6268
|
-
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
|
|
6269
|
-
found = true;
|
|
6270
|
-
return;
|
|
6271
|
-
}
|
|
6272
|
-
const record = node;
|
|
6273
|
-
for (const key of Object.keys(record)) {
|
|
6274
|
-
if (key === "parent") continue;
|
|
6275
|
-
const child = record[key];
|
|
6276
|
-
if (Array.isArray(child)) {
|
|
6277
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6278
|
-
} else if (isAstNode(child)) visit(child);
|
|
6279
|
-
if (found) return;
|
|
6280
|
-
}
|
|
6281
|
-
};
|
|
6282
|
-
visit(programRoot);
|
|
6283
|
-
return found;
|
|
6284
|
-
};
|
|
6285
6357
|
const memberExpressionPath = (node) => {
|
|
6286
6358
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6287
6359
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6289,15 +6361,16 @@ const memberExpressionPath = (node) => {
|
|
|
6289
6361
|
const propertyName = getStaticMemberName(node);
|
|
6290
6362
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6291
6363
|
};
|
|
6292
|
-
const
|
|
6293
|
-
|
|
6364
|
+
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6365
|
+
const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
6366
|
+
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6367
|
+
if (cached) return cached;
|
|
6368
|
+
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6369
|
+
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6294
6370
|
const visit = (node) => {
|
|
6295
|
-
if (found) return;
|
|
6296
6371
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6297
|
-
if (
|
|
6298
|
-
|
|
6299
|
-
return;
|
|
6300
|
-
}
|
|
6372
|
+
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
6373
|
+
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
6301
6374
|
}
|
|
6302
6375
|
const record = node;
|
|
6303
6376
|
for (const key of Object.keys(record)) {
|
|
@@ -6306,12 +6379,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
|
6306
6379
|
if (Array.isArray(child)) {
|
|
6307
6380
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6308
6381
|
} else if (isAstNode(child)) visit(child);
|
|
6309
|
-
if (found) return;
|
|
6310
6382
|
}
|
|
6311
6383
|
};
|
|
6312
6384
|
visit(programRoot);
|
|
6313
|
-
|
|
6385
|
+
const index = {
|
|
6386
|
+
identifierTargets,
|
|
6387
|
+
memberPathSegments
|
|
6388
|
+
};
|
|
6389
|
+
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6390
|
+
return index;
|
|
6314
6391
|
};
|
|
6392
|
+
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6393
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6315
6394
|
const displayName = defineRule({
|
|
6316
6395
|
id: "display-name",
|
|
6317
6396
|
title: "Component missing display name",
|
|
@@ -6371,7 +6450,7 @@ const displayName = defineRule({
|
|
|
6371
6450
|
},
|
|
6372
6451
|
ArrowFunctionExpression(node) {
|
|
6373
6452
|
if (!containsJsx$1(node)) return;
|
|
6374
|
-
if (isNodeOfType(node.parent, "ArrowFunctionExpression")) {
|
|
6453
|
+
if (isNodeOfType(node.parent, "ArrowFunctionExpression") || isNodeOfType(node.parent, "ReturnStatement")) {
|
|
6375
6454
|
reportAt(node);
|
|
6376
6455
|
return;
|
|
6377
6456
|
}
|
|
@@ -7183,27 +7262,8 @@ const isDescendantScope = (inner, outer) => {
|
|
|
7183
7262
|
return false;
|
|
7184
7263
|
};
|
|
7185
7264
|
//#endregion
|
|
7186
|
-
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7187
|
-
/**
|
|
7188
|
-
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7189
|
-
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7190
|
-
* match (`true`) or the chain's root (`false`).
|
|
7191
|
-
*
|
|
7192
|
-
* Was duplicated byte-identical in:
|
|
7193
|
-
* - exhaustive-deps-symbol-stability.ts
|
|
7194
|
-
* - semantic/closure-captures.ts
|
|
7195
|
-
*/
|
|
7196
|
-
const isAstDescendant = (inner, outer) => {
|
|
7197
|
-
let current = inner;
|
|
7198
|
-
while (current) {
|
|
7199
|
-
if (current === outer) return true;
|
|
7200
|
-
current = current.parent ?? null;
|
|
7201
|
-
}
|
|
7202
|
-
return false;
|
|
7203
|
-
};
|
|
7204
|
-
//#endregion
|
|
7205
7265
|
//#region src/plugin/semantic/closure-captures.ts
|
|
7206
|
-
const
|
|
7266
|
+
const computeClosureCaptures = (functionNode, scopes) => {
|
|
7207
7267
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
7208
7268
|
const out = [];
|
|
7209
7269
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7238,7 +7298,20 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
7238
7298
|
}
|
|
7239
7299
|
};
|
|
7240
7300
|
visit(functionNode);
|
|
7241
|
-
return out
|
|
7301
|
+
return out;
|
|
7302
|
+
};
|
|
7303
|
+
const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
7304
|
+
const closureCaptures = (functionNode, scopes) => {
|
|
7305
|
+
let capturesByFunction = capturesByAnalysis.get(scopes);
|
|
7306
|
+
if (!capturesByFunction) {
|
|
7307
|
+
capturesByFunction = /* @__PURE__ */ new WeakMap();
|
|
7308
|
+
capturesByAnalysis.set(scopes, capturesByFunction);
|
|
7309
|
+
}
|
|
7310
|
+
const memoizedCaptures = capturesByFunction.get(functionNode);
|
|
7311
|
+
if (memoizedCaptures) return memoizedCaptures;
|
|
7312
|
+
const computedCaptures = computeClosureCaptures(functionNode, scopes);
|
|
7313
|
+
capturesByFunction.set(functionNode, computedCaptures);
|
|
7314
|
+
return computedCaptures;
|
|
7242
7315
|
};
|
|
7243
7316
|
//#endregion
|
|
7244
7317
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
@@ -7294,7 +7367,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
7294
7367
|
"ParenthesizedExpression",
|
|
7295
7368
|
"ChainExpression"
|
|
7296
7369
|
]);
|
|
7297
|
-
const unwrapExpression$
|
|
7370
|
+
const unwrapExpression$3 = (node) => {
|
|
7298
7371
|
let current = node;
|
|
7299
7372
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
7300
7373
|
const inner = current.expression;
|
|
@@ -7367,6 +7440,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
|
|
|
7367
7440
|
};
|
|
7368
7441
|
};
|
|
7369
7442
|
//#endregion
|
|
7443
|
+
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7444
|
+
/**
|
|
7445
|
+
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7446
|
+
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7447
|
+
* match (`true`) or the chain's root (`false`).
|
|
7448
|
+
*
|
|
7449
|
+
* Was duplicated byte-identical in:
|
|
7450
|
+
* - exhaustive-deps-symbol-stability.ts
|
|
7451
|
+
* - semantic/closure-captures.ts
|
|
7452
|
+
*/
|
|
7453
|
+
const isAstDescendant = (inner, outer) => {
|
|
7454
|
+
let current = inner;
|
|
7455
|
+
while (current) {
|
|
7456
|
+
if (current === outer) return true;
|
|
7457
|
+
current = current.parent ?? null;
|
|
7458
|
+
}
|
|
7459
|
+
return false;
|
|
7460
|
+
};
|
|
7461
|
+
//#endregion
|
|
7370
7462
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
7371
7463
|
/**
|
|
7372
7464
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -7401,7 +7493,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7401
7493
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
7402
7494
|
const initializerRaw = declarator.init;
|
|
7403
7495
|
if (!initializerRaw) return false;
|
|
7404
|
-
const initializer = unwrapExpression$
|
|
7496
|
+
const initializer = unwrapExpression$3(initializerRaw);
|
|
7405
7497
|
if (symbol.kind === "const") {
|
|
7406
7498
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
7407
7499
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -7421,13 +7513,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7421
7513
|
return false;
|
|
7422
7514
|
};
|
|
7423
7515
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
7424
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7516
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7425
7517
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
7426
7518
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
7427
7519
|
};
|
|
7428
7520
|
const getFunctionValueNode = (symbol) => {
|
|
7429
7521
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
7430
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7522
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7431
7523
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
7432
7524
|
return null;
|
|
7433
7525
|
};
|
|
@@ -7523,6 +7615,14 @@ const flattenReferenceRootName = (reference) => {
|
|
|
7523
7615
|
if (isNodeOfType(referencedIdentifier, "JSXIdentifier")) return referencedIdentifier.name;
|
|
7524
7616
|
return "";
|
|
7525
7617
|
};
|
|
7618
|
+
const REF_CURRENT_SEGMENT = ".current";
|
|
7619
|
+
const truncateAtRefCurrent = (chain) => {
|
|
7620
|
+
const refCurrentIndex = chain.indexOf(REF_CURRENT_SEGMENT);
|
|
7621
|
+
if (refCurrentIndex === -1) return chain;
|
|
7622
|
+
const segmentEndIndex = refCurrentIndex + 8;
|
|
7623
|
+
if (segmentEndIndex === chain.length || chain[segmentEndIndex] === ".") return chain.slice(0, refCurrentIndex);
|
|
7624
|
+
return chain;
|
|
7625
|
+
};
|
|
7526
7626
|
const computeDepKey = (reference) => {
|
|
7527
7627
|
const referencedIdentifier = reference.identifier;
|
|
7528
7628
|
let parent = referencedIdentifier.parent ?? null;
|
|
@@ -7553,21 +7653,22 @@ const computeDepKey = (reference) => {
|
|
|
7553
7653
|
const declarator = outermost.parent;
|
|
7554
7654
|
if (declarator && isNodeOfType(declarator, "VariableDeclarator") && declarator.init === outermost) {
|
|
7555
7655
|
const destructuredPath = getDestructuredPropertyPath(declarator.id);
|
|
7556
|
-
if (destructuredPath) return `${fullName}.${destructuredPath}
|
|
7656
|
+
if (destructuredPath) return truncateAtRefCurrent(`${fullName}.${destructuredPath}`);
|
|
7557
7657
|
}
|
|
7658
|
+
const truncatedName = truncateAtRefCurrent(fullName);
|
|
7659
|
+
if (truncatedName !== fullName) return truncatedName;
|
|
7558
7660
|
if (reference.flag !== "read") {
|
|
7559
7661
|
const lastDotIndex = fullName.lastIndexOf(".");
|
|
7560
7662
|
if (lastDotIndex !== -1) return fullName.slice(0, lastDotIndex);
|
|
7561
7663
|
}
|
|
7562
|
-
if (fullName.endsWith(".current")) return fullName.slice(0, -8);
|
|
7563
7664
|
return fullName;
|
|
7564
7665
|
};
|
|
7565
7666
|
const computeDeclaredDepKey = (entry) => {
|
|
7566
|
-
const stripped = unwrapExpression$
|
|
7667
|
+
const stripped = unwrapExpression$3(entry);
|
|
7567
7668
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7568
7669
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
7569
7670
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) {
|
|
7570
|
-
const callee = unwrapExpression$
|
|
7671
|
+
const callee = unwrapExpression$3(stripped.callee);
|
|
7571
7672
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7572
7673
|
if (isNodeOfType(callee, "MemberExpression") && !hasComputedMemberExpression(callee)) return stringifyMemberChain(callee);
|
|
7573
7674
|
}
|
|
@@ -7575,16 +7676,16 @@ const computeDeclaredDepKey = (entry) => {
|
|
|
7575
7676
|
};
|
|
7576
7677
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
7577
7678
|
if (!depsArgument) return false;
|
|
7578
|
-
const strippedDepsArgument = unwrapExpression$
|
|
7679
|
+
const strippedDepsArgument = unwrapExpression$3(depsArgument);
|
|
7579
7680
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
7580
7681
|
return strippedDepsArgument.elements.some((element) => {
|
|
7581
7682
|
if (!element) return false;
|
|
7582
|
-
const strippedElement = unwrapExpression$
|
|
7683
|
+
const strippedElement = unwrapExpression$3(element);
|
|
7583
7684
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
7584
7685
|
});
|
|
7585
7686
|
};
|
|
7586
7687
|
const stringifyMemberChain = (node) => {
|
|
7587
|
-
const stripped = unwrapExpression$
|
|
7688
|
+
const stripped = unwrapExpression$3(node);
|
|
7588
7689
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7589
7690
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
7590
7691
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -7626,13 +7727,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
7626
7727
|
return false;
|
|
7627
7728
|
};
|
|
7628
7729
|
const getMemberRootIdentifier = (node) => {
|
|
7629
|
-
const stripped = unwrapExpression$
|
|
7730
|
+
const stripped = unwrapExpression$3(node);
|
|
7630
7731
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
7631
7732
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
7632
7733
|
return null;
|
|
7633
7734
|
};
|
|
7634
7735
|
const hasComputedMemberExpression = (node) => {
|
|
7635
|
-
const stripped = unwrapExpression$
|
|
7736
|
+
const stripped = unwrapExpression$3(node);
|
|
7636
7737
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
7637
7738
|
if (stripped.computed) return true;
|
|
7638
7739
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -7648,7 +7749,7 @@ const getRootSymbol = (node, scopes) => {
|
|
|
7648
7749
|
return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7649
7750
|
};
|
|
7650
7751
|
const getDeclaredDepSymbolSource = (node) => {
|
|
7651
|
-
const stripped = unwrapExpression$
|
|
7752
|
+
const stripped = unwrapExpression$3(node);
|
|
7652
7753
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) return stripped.callee;
|
|
7653
7754
|
return node;
|
|
7654
7755
|
};
|
|
@@ -7658,7 +7759,7 @@ const isRegExpLiteral = (node) => {
|
|
|
7658
7759
|
};
|
|
7659
7760
|
const isUnstableInitializer = (node) => {
|
|
7660
7761
|
if (!node) return false;
|
|
7661
|
-
const stripped = unwrapExpression$
|
|
7762
|
+
const stripped = unwrapExpression$3(node);
|
|
7662
7763
|
if (isRegExpLiteral(stripped)) return true;
|
|
7663
7764
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
7664
7765
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -7750,9 +7851,9 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7750
7851
|
};
|
|
7751
7852
|
findReturn(callback);
|
|
7752
7853
|
if (!cleanupFunction) return null;
|
|
7753
|
-
let
|
|
7854
|
+
let found = null;
|
|
7754
7855
|
const visitCleanup = (node) => {
|
|
7755
|
-
if (
|
|
7856
|
+
if (found) return;
|
|
7756
7857
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
7757
7858
|
const candidateName = getRefCurrentNameFromMemberExpression(node);
|
|
7758
7859
|
if (candidateName) {
|
|
@@ -7760,7 +7861,10 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7760
7861
|
const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7761
7862
|
const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
|
|
7762
7863
|
if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
|
|
7763
|
-
|
|
7864
|
+
found = {
|
|
7865
|
+
refCurrentName: candidateName,
|
|
7866
|
+
refSymbol: symbol
|
|
7867
|
+
};
|
|
7764
7868
|
return;
|
|
7765
7869
|
}
|
|
7766
7870
|
}
|
|
@@ -7775,7 +7879,19 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7775
7879
|
}
|
|
7776
7880
|
};
|
|
7777
7881
|
visitCleanup(cleanupFunction);
|
|
7778
|
-
return
|
|
7882
|
+
return found;
|
|
7883
|
+
};
|
|
7884
|
+
const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
7885
|
+
if (!refSymbol) return false;
|
|
7886
|
+
for (const reference of refSymbol.references) {
|
|
7887
|
+
const memberParent = reference.identifier.parent;
|
|
7888
|
+
if (!memberParent || !isNodeOfType(memberParent, "MemberExpression")) continue;
|
|
7889
|
+
if (memberParent.object !== reference.identifier) continue;
|
|
7890
|
+
if (getRefCurrentNameFromMemberExpression(memberParent) === null) continue;
|
|
7891
|
+
const assignmentParent = memberParent.parent;
|
|
7892
|
+
if (assignmentParent && isNodeOfType(assignmentParent, "AssignmentExpression") && assignmentParent.left === memberParent) return true;
|
|
7893
|
+
}
|
|
7894
|
+
return false;
|
|
7779
7895
|
};
|
|
7780
7896
|
const hasRefCurrentAssignment = (callback, refCurrentName) => {
|
|
7781
7897
|
let didAssignRefCurrent = false;
|
|
@@ -7814,9 +7930,21 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
7814
7930
|
const visit = (current) => {
|
|
7815
7931
|
if (didFindMemberCall) return;
|
|
7816
7932
|
if (isNodeOfType(current, "CallExpression")) {
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
|
|
7933
|
+
const callee = unwrapExpression$3(current.callee);
|
|
7934
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
7935
|
+
let chainObject = unwrapExpression$3(callee.object);
|
|
7936
|
+
let doesChainPassThroughCurrent = false;
|
|
7937
|
+
while (chainObject && isNodeOfType(chainObject, "MemberExpression")) {
|
|
7938
|
+
if (isNodeOfType(chainObject.property, "Identifier") && chainObject.property.name === "current") {
|
|
7939
|
+
doesChainPassThroughCurrent = true;
|
|
7940
|
+
break;
|
|
7941
|
+
}
|
|
7942
|
+
chainObject = unwrapExpression$3(chainObject.object);
|
|
7943
|
+
}
|
|
7944
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
7945
|
+
didFindMemberCall = true;
|
|
7946
|
+
return;
|
|
7947
|
+
}
|
|
7820
7948
|
}
|
|
7821
7949
|
}
|
|
7822
7950
|
const record = current;
|
|
@@ -7877,7 +8005,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7877
8005
|
return;
|
|
7878
8006
|
}
|
|
7879
8007
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
7880
|
-
const callbackExpression = unwrapExpression$
|
|
8008
|
+
const callbackExpression = unwrapExpression$3(callbackArgument);
|
|
7881
8009
|
let callbackToAnalyze = null;
|
|
7882
8010
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
7883
8011
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -7885,7 +8013,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7885
8013
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
7886
8014
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
7887
8015
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
7888
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$
|
|
8016
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$3(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
7889
8017
|
else if (depsArgumentRaw) {
|
|
7890
8018
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
7891
8019
|
context.report({
|
|
@@ -7918,11 +8046,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7918
8046
|
message: buildAssignmentMessage(assignment.name)
|
|
7919
8047
|
});
|
|
7920
8048
|
if (outerAssignments.length > 0) return;
|
|
7921
|
-
const
|
|
8049
|
+
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
7922
8050
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
7923
|
-
if (
|
|
8051
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol)) context.report({
|
|
7924
8052
|
node: callbackToAnalyze,
|
|
7925
|
-
message: buildRefCleanupMessage(refCurrentName)
|
|
8053
|
+
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
7926
8054
|
});
|
|
7927
8055
|
}
|
|
7928
8056
|
if (!depsArgumentRaw) {
|
|
@@ -7942,8 +8070,16 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7942
8070
|
});
|
|
7943
8071
|
return;
|
|
7944
8072
|
}
|
|
7945
|
-
const depsArgument = unwrapExpression$
|
|
7946
|
-
if (isNodeOfType(depsArgument, "
|
|
8073
|
+
const depsArgument = unwrapExpression$3(depsArgumentRaw);
|
|
8074
|
+
if (isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
8075
|
+
if (isAutoDependenciesHook(hookName)) return;
|
|
8076
|
+
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) context.report({
|
|
8077
|
+
node: depsArgument,
|
|
8078
|
+
message: buildMissingDepArrayMessage(hookName)
|
|
8079
|
+
});
|
|
8080
|
+
return;
|
|
8081
|
+
}
|
|
8082
|
+
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null) {
|
|
7947
8083
|
if (isAutoDependenciesHook(hookName)) return;
|
|
7948
8084
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
7949
8085
|
context.report({
|
|
@@ -7952,19 +8088,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7952
8088
|
});
|
|
7953
8089
|
return;
|
|
7954
8090
|
}
|
|
7955
|
-
const nonArrayCaptureKeys = callbackToAnalyze !== null ? new Set(collectCaptureDepKeys(callbackToAnalyze, context.scopes).keys) : /* @__PURE__ */ new Set();
|
|
7956
|
-
for (const forcedCaptureKey of forcedCaptureKeys) nonArrayCaptureKeys.add(forcedCaptureKey);
|
|
7957
|
-
if (nonArrayCaptureKeys.size > 0) {
|
|
7958
|
-
context.report({
|
|
7959
|
-
node: depsArgument,
|
|
7960
|
-
message: buildNonArrayDepsMessage(hookName)
|
|
7961
|
-
});
|
|
7962
|
-
context.report({
|
|
7963
|
-
node: depsArgument,
|
|
7964
|
-
message: buildMissingDepMessage(hookName, [...nonArrayCaptureKeys].join(", "))
|
|
7965
|
-
});
|
|
7966
|
-
}
|
|
7967
|
-
return;
|
|
7968
8091
|
}
|
|
7969
8092
|
if (!isNodeOfType(depsArgument, "ArrayExpression")) {
|
|
7970
8093
|
context.report({
|
|
@@ -7983,11 +8106,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7983
8106
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
7984
8107
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
7985
8108
|
if (!element) return false;
|
|
7986
|
-
return isLiteralOrEmptyTemplate(unwrapExpression$
|
|
8109
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
|
|
7987
8110
|
});
|
|
7988
8111
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
7989
8112
|
if (!element) return false;
|
|
7990
|
-
return isNonStringLiteral(unwrapExpression$
|
|
8113
|
+
return isNonStringLiteral(unwrapExpression$3(element));
|
|
7991
8114
|
});
|
|
7992
8115
|
if (hasNonStringLiteralDep) context.report({
|
|
7993
8116
|
node: depsArgument,
|
|
@@ -8008,7 +8131,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8008
8131
|
});
|
|
8009
8132
|
continue;
|
|
8010
8133
|
}
|
|
8011
|
-
const stripped = unwrapExpression$
|
|
8134
|
+
const stripped = unwrapExpression$3(elementNode);
|
|
8012
8135
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
8013
8136
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
8014
8137
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -8150,7 +8273,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8150
8273
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
8151
8274
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
8152
8275
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
8153
|
-
if (isNodeOfType(unwrapExpression$
|
|
8276
|
+
if (isNodeOfType(unwrapExpression$3(reportNode), "CallExpression")) {
|
|
8154
8277
|
context.report({
|
|
8155
8278
|
node: reportNode,
|
|
8156
8279
|
message: buildComplexDepMessage(hookName)
|
|
@@ -8266,9 +8389,14 @@ const firebaseQueryFilterAsAuth = defineRule({
|
|
|
8266
8389
|
* `*Handler`), so the cheap escape-then-replace shape is enough
|
|
8267
8390
|
* and avoids pulling picomatch into the per-file rule path.
|
|
8268
8391
|
*/
|
|
8392
|
+
const compiledGlobs = /* @__PURE__ */ new Map();
|
|
8269
8393
|
const compileGlob = (pattern) => {
|
|
8394
|
+
const cached = compiledGlobs.get(pattern);
|
|
8395
|
+
if (cached) return cached;
|
|
8270
8396
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
8271
|
-
|
|
8397
|
+
const compiled = new RegExp(`^${escaped}$`);
|
|
8398
|
+
compiledGlobs.set(pattern, compiled);
|
|
8399
|
+
return compiled;
|
|
8272
8400
|
};
|
|
8273
8401
|
//#endregion
|
|
8274
8402
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
@@ -8558,7 +8686,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
8558
8686
|
const resolveSettings$40 = (settings) => {
|
|
8559
8687
|
const reactDoctor = settings?.["react-doctor"];
|
|
8560
8688
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
8561
|
-
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
8689
|
+
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
8562
8690
|
};
|
|
8563
8691
|
const headingHasContent = defineRule({
|
|
8564
8692
|
id: "heading-has-content",
|
|
@@ -8571,7 +8699,7 @@ const headingHasContent = defineRule({
|
|
|
8571
8699
|
const settings = resolveSettings$40(context.settings);
|
|
8572
8700
|
return { JSXOpeningElement(node) {
|
|
8573
8701
|
const elementType = getElementType(node, context.settings);
|
|
8574
|
-
if (!settings.headingTags.
|
|
8702
|
+
if (!settings.headingTags.has(elementType)) return;
|
|
8575
8703
|
const parent = node.parent;
|
|
8576
8704
|
if (parent && isNodeOfType(parent, "JSXElement")) {
|
|
8577
8705
|
if (objectHasAccessibleChild(parent, context.settings)) return;
|
|
@@ -9169,9 +9297,9 @@ const resolveSettings$37 = (settings) => {
|
|
|
9169
9297
|
};
|
|
9170
9298
|
};
|
|
9171
9299
|
const isWordBoundary = (text, start, end) => {
|
|
9172
|
-
const
|
|
9173
|
-
const startsBoundary = start === 0 || !
|
|
9174
|
-
const endsBoundary = end === text.length || !
|
|
9300
|
+
const isWordCharacter = (charCode) => charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122 || charCode === 45 || charCode === 95;
|
|
9301
|
+
const startsBoundary = start === 0 || !isWordCharacter(text.charCodeAt(start - 1));
|
|
9302
|
+
const endsBoundary = end === text.length || !isWordCharacter(text.charCodeAt(end));
|
|
9175
9303
|
return startsBoundary && endsBoundary;
|
|
9176
9304
|
};
|
|
9177
9305
|
const containsRedundantWord = (altText, words) => {
|
|
@@ -9210,10 +9338,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9210
9338
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9211
9339
|
const settings = resolveSettings$37(context.settings);
|
|
9212
9340
|
return { JSXOpeningElement(node) {
|
|
9213
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9214
9341
|
const tag = getElementType(node, context.settings);
|
|
9215
9342
|
if (!settings.components.includes(tag)) return;
|
|
9216
9343
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9344
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9217
9345
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9218
9346
|
if (!altAttribute) return;
|
|
9219
9347
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9268,6 +9396,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9268
9396
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9269
9397
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9270
9398
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9399
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9271
9400
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9272
9401
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9273
9402
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9297,6 +9426,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9297
9426
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9298
9427
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9299
9428
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9429
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9300
9430
|
const content = getScannableContent(file);
|
|
9301
9431
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9302
9432
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9475,6 +9605,7 @@ const KEYBOARD_EVENT_HANDLERS = [
|
|
|
9475
9605
|
"onKeyUp"
|
|
9476
9606
|
];
|
|
9477
9607
|
const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
|
|
9608
|
+
const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
|
|
9478
9609
|
//#endregion
|
|
9479
9610
|
//#region src/plugin/utils/is-disabled-element.ts
|
|
9480
9611
|
const isDisabledElement = (openingElement) => {
|
|
@@ -9529,15 +9660,25 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9529
9660
|
const settings = resolveSettings$36(context.settings);
|
|
9530
9661
|
const tabbableSet = new Set(settings.tabbable);
|
|
9531
9662
|
return { JSXOpeningElement(node) {
|
|
9663
|
+
if (node.attributes.length === 0) return;
|
|
9532
9664
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9533
9665
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9534
9666
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9667
|
+
if (!role) return;
|
|
9668
|
+
let hasInteractiveHandler = false;
|
|
9669
|
+
for (const attribute of node.attributes) {
|
|
9670
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
9671
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
9672
|
+
if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
9673
|
+
hasInteractiveHandler = true;
|
|
9674
|
+
break;
|
|
9675
|
+
}
|
|
9676
|
+
}
|
|
9677
|
+
if (!hasInteractiveHandler) return;
|
|
9535
9678
|
const elementType = getElementType(node, context.settings);
|
|
9536
9679
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9537
|
-
|
|
9680
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9538
9681
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9539
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9540
|
-
if (!role) return;
|
|
9541
9682
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9542
9683
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9543
9684
|
context.report({
|
|
@@ -9555,7 +9696,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
9555
9696
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
9556
9697
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
9557
9698
|
};
|
|
9558
|
-
const isFunctionExpressionLike$
|
|
9699
|
+
const isFunctionExpressionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
9559
9700
|
const getFirstParameterName = (fn) => {
|
|
9560
9701
|
const parameters = fn.params ?? [];
|
|
9561
9702
|
if (parameters.length !== 1) return null;
|
|
@@ -9688,7 +9829,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
9688
9829
|
const args = node.arguments ?? [];
|
|
9689
9830
|
if (args.length === 0) return;
|
|
9690
9831
|
const reader = args[0];
|
|
9691
|
-
if (!isFunctionExpressionLike$
|
|
9832
|
+
if (!isFunctionExpressionLike$2(reader)) return;
|
|
9692
9833
|
const getParameterName = getFirstParameterName(reader);
|
|
9693
9834
|
if (!getParameterName) return;
|
|
9694
9835
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -9786,14 +9927,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
9786
9927
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
9787
9928
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
9788
9929
|
const declaredName = functionNode.id.name;
|
|
9789
|
-
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
|
|
9930
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$1.test(declaredName);
|
|
9790
9931
|
}
|
|
9791
9932
|
let cursor = functionNode.parent ?? null;
|
|
9792
9933
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
9793
9934
|
if (!cursor) return false;
|
|
9794
9935
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
9795
9936
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
9796
|
-
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN.test(cursor.id.name);
|
|
9937
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$1.test(cursor.id.name);
|
|
9797
9938
|
};
|
|
9798
9939
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
9799
9940
|
let didFindRenderPathInvocation = false;
|
|
@@ -10176,8 +10317,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10176
10317
|
walkAst(loopBody, (child) => {
|
|
10177
10318
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10178
10319
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10179
|
-
});
|
|
10180
|
-
walkAst(loopBody, (child) => {
|
|
10181
10320
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10182
10321
|
if (child.computed) return;
|
|
10183
10322
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10439,10 +10578,15 @@ const jsCombineIterations = defineRule({
|
|
|
10439
10578
|
severity: "warn",
|
|
10440
10579
|
recommendation: "Combine `.map().filter()` style chains into one pass with `.reduce()` or a `for...of` loop, so you only loop over the list once",
|
|
10441
10580
|
create: (context) => {
|
|
10442
|
-
let
|
|
10581
|
+
let programNode = null;
|
|
10582
|
+
let generatorNamesInFile = null;
|
|
10583
|
+
const getGeneratorNamesInFile = () => {
|
|
10584
|
+
generatorNamesInFile ??= programNode ? collectGeneratorNames(programNode) : /* @__PURE__ */ new Set();
|
|
10585
|
+
return generatorNamesInFile;
|
|
10586
|
+
};
|
|
10443
10587
|
return {
|
|
10444
|
-
Program(
|
|
10445
|
-
|
|
10588
|
+
Program(node) {
|
|
10589
|
+
programNode = node;
|
|
10446
10590
|
},
|
|
10447
10591
|
CallExpression(node) {
|
|
10448
10592
|
if (!isNodeOfType(node.callee, "MemberExpression") || !isNodeOfType(node.callee.property, "Identifier")) return;
|
|
@@ -10464,7 +10608,7 @@ const jsCombineIterations = defineRule({
|
|
|
10464
10608
|
if (isNullFilteringPredicate(filterArgument)) return;
|
|
10465
10609
|
if (isTypePredicateArrow(filterArgument)) return;
|
|
10466
10610
|
}
|
|
10467
|
-
if (isReceiverChainIteratorRooted(innerCall.callee.object,
|
|
10611
|
+
if (isReceiverChainIteratorRooted(innerCall.callee.object, getGeneratorNamesInFile())) return;
|
|
10468
10612
|
if (isSmallLiteralArrayRootedChain(innerCall.callee.object)) return;
|
|
10469
10613
|
if (isStringSplitRootedChain(innerCall.callee.object)) return;
|
|
10470
10614
|
context.report({
|
|
@@ -10632,7 +10776,6 @@ const jsHoistIntl = defineRule({
|
|
|
10632
10776
|
recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
|
|
10633
10777
|
create: (context) => ({ NewExpression(node) {
|
|
10634
10778
|
if (!isIntlNewExpression(node)) return;
|
|
10635
|
-
if (isInsideCacheMemo(node)) return;
|
|
10636
10779
|
let cursor = node.parent ?? null;
|
|
10637
10780
|
let inFunctionBody = false;
|
|
10638
10781
|
while (cursor) {
|
|
@@ -10649,6 +10792,7 @@ const jsHoistIntl = defineRule({
|
|
|
10649
10792
|
cursor = cursor.parent ?? null;
|
|
10650
10793
|
}
|
|
10651
10794
|
if (!inFunctionBody) return;
|
|
10795
|
+
if (isInsideCacheMemo(node)) return;
|
|
10652
10796
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10653
10797
|
context.report({
|
|
10654
10798
|
node,
|
|
@@ -10723,7 +10867,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10723
10867
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10724
10868
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10725
10869
|
};
|
|
10726
|
-
const
|
|
10870
|
+
const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
10871
|
+
const getLoopBoundNames = (loop) => {
|
|
10872
|
+
const cached = loopBoundNamesCache.get(loop);
|
|
10873
|
+
if (cached) return cached;
|
|
10874
|
+
const names = /* @__PURE__ */ new Set();
|
|
10727
10875
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10728
10876
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10729
10877
|
});
|
|
@@ -10740,6 +10888,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10740
10888
|
if (targetRoot) names.add(targetRoot);
|
|
10741
10889
|
}
|
|
10742
10890
|
});
|
|
10891
|
+
loopBoundNamesCache.set(loop, names);
|
|
10892
|
+
return names;
|
|
10743
10893
|
};
|
|
10744
10894
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10745
10895
|
let cursor = receiver;
|
|
@@ -10765,7 +10915,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10765
10915
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10766
10916
|
let ancestor = node.parent;
|
|
10767
10917
|
while (ancestor) {
|
|
10768
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10918
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10769
10919
|
ancestor = ancestor.parent;
|
|
10770
10920
|
}
|
|
10771
10921
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -12524,6 +12674,7 @@ const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
|
12524
12674
|
"bottomContent",
|
|
12525
12675
|
"leftContent",
|
|
12526
12676
|
"rightContent",
|
|
12677
|
+
"config",
|
|
12527
12678
|
"value",
|
|
12528
12679
|
"currentValue",
|
|
12529
12680
|
"form",
|
|
@@ -12662,6 +12813,10 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12662
12813
|
"Panel",
|
|
12663
12814
|
"Overlay",
|
|
12664
12815
|
"Shape",
|
|
12816
|
+
"Avatar",
|
|
12817
|
+
"Text",
|
|
12818
|
+
"State",
|
|
12819
|
+
"Zone",
|
|
12665
12820
|
"Override",
|
|
12666
12821
|
"Overrides",
|
|
12667
12822
|
"Items",
|
|
@@ -12678,6 +12833,8 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12678
12833
|
];
|
|
12679
12834
|
const isSlotPropName = (propName) => {
|
|
12680
12835
|
if (KNOWN_SLOT_PROP_NAMES.has(propName)) return true;
|
|
12836
|
+
const decapitalized = propName.charAt(0).toLowerCase() + propName.slice(1);
|
|
12837
|
+
if (decapitalized !== propName && KNOWN_SLOT_PROP_NAMES.has(decapitalized)) return true;
|
|
12681
12838
|
for (const suffix of SLOT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
12682
12839
|
return false;
|
|
12683
12840
|
};
|
|
@@ -14366,14 +14523,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14366
14523
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14367
14524
|
const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
14368
14525
|
const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
|
|
14369
|
-
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14526
|
+
const DEFAULT_CONTROL_COMPONENTS = new Set([
|
|
14370
14527
|
"input",
|
|
14371
14528
|
"meter",
|
|
14372
14529
|
"output",
|
|
14373
14530
|
"progress",
|
|
14374
14531
|
"select",
|
|
14375
14532
|
"textarea"
|
|
14376
|
-
];
|
|
14533
|
+
]);
|
|
14377
14534
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14378
14535
|
"alt",
|
|
14379
14536
|
"aria-label",
|
|
@@ -14385,8 +14542,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14385
14542
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14386
14543
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14387
14544
|
return {
|
|
14388
|
-
labelComponents: ["label", ...ruleSettings.labelComponents ?? []]
|
|
14389
|
-
labelAttributes:
|
|
14545
|
+
labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
|
|
14546
|
+
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14390
14547
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14391
14548
|
assert: ruleSettings.assert ?? "either",
|
|
14392
14549
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14394,7 +14551,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14394
14551
|
};
|
|
14395
14552
|
};
|
|
14396
14553
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14397
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14554
|
+
if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
|
|
14398
14555
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14399
14556
|
};
|
|
14400
14557
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14420,7 +14577,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14420
14577
|
const attributeName = attribute.name;
|
|
14421
14578
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14422
14579
|
const propName = getJsxAttributeName(attributeName);
|
|
14423
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14580
|
+
if (!propName || !searchContext.labelAttributes.has(propName)) continue;
|
|
14424
14581
|
const attributeValue = attribute.value;
|
|
14425
14582
|
if (!attributeValue) continue;
|
|
14426
14583
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14442,7 +14599,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14442
14599
|
const attributeName = attribute.name;
|
|
14443
14600
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14444
14601
|
const propName = getJsxAttributeName(attributeName);
|
|
14445
|
-
if (propName && searchContext.labelAttributes.
|
|
14602
|
+
if (propName && searchContext.labelAttributes.has(propName)) return true;
|
|
14446
14603
|
}
|
|
14447
14604
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14448
14605
|
return false;
|
|
@@ -14465,7 +14622,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14465
14622
|
if (isTestlikeFile) return;
|
|
14466
14623
|
const opening = node.openingElement;
|
|
14467
14624
|
const tagName = getElementType(opening, context.settings);
|
|
14468
|
-
if (!settings.labelComponents.
|
|
14625
|
+
if (!settings.labelComponents.has(tagName)) return;
|
|
14469
14626
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14470
14627
|
const searchContext = {
|
|
14471
14628
|
depth: settings.depth,
|
|
@@ -15054,9 +15211,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15054
15211
|
const reactDoctor = settings?.["react-doctor"];
|
|
15055
15212
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15056
15213
|
return {
|
|
15057
|
-
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15058
|
-
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15059
|
-
track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
|
|
15214
|
+
audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
|
|
15215
|
+
video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
|
|
15216
|
+
track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
|
|
15060
15217
|
};
|
|
15061
15218
|
};
|
|
15062
15219
|
const evaluateMuted = (attribute) => {
|
|
@@ -15085,7 +15242,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15085
15242
|
let rendersCaptionTrack = false;
|
|
15086
15243
|
walkAst(expression, (inner) => {
|
|
15087
15244
|
if (rendersCaptionTrack) return false;
|
|
15088
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15245
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15089
15246
|
rendersCaptionTrack = true;
|
|
15090
15247
|
return false;
|
|
15091
15248
|
}
|
|
@@ -15103,7 +15260,7 @@ const mediaHasCaption = defineRule({
|
|
|
15103
15260
|
const settings = resolveSettings$23(context.settings);
|
|
15104
15261
|
return { JSXOpeningElement(node) {
|
|
15105
15262
|
const tag = getElementType(node, context.settings);
|
|
15106
|
-
if (!(settings.audio.
|
|
15263
|
+
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
15107
15264
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15108
15265
|
const parent = node.parent;
|
|
15109
15266
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15118,7 +15275,7 @@ const mediaHasCaption = defineRule({
|
|
|
15118
15275
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15119
15276
|
const opening = child.openingElement;
|
|
15120
15277
|
const childTag = getElementType(opening, context.settings);
|
|
15121
|
-
if (!settings.track.
|
|
15278
|
+
if (!settings.track.has(childTag)) return false;
|
|
15122
15279
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15123
15280
|
if (!kindAttribute) return false;
|
|
15124
15281
|
let kindValue = kindAttribute.value;
|
|
@@ -15429,16 +15586,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15429
15586
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15430
15587
|
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
15431
15588
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15432
|
-
const
|
|
15589
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15433
15590
|
const strippedSource = stripJsComments(sourceText);
|
|
15434
|
-
|
|
15435
|
-
|
|
15436
|
-
for (const match of strippedSource.matchAll(
|
|
15437
|
-
|
|
15591
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
15592
|
+
if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
|
|
15593
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
|
|
15594
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
|
|
15595
|
+
const specifiersText = match[1] ?? "";
|
|
15596
|
+
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15597
|
+
}
|
|
15598
|
+
return exportedNames;
|
|
15438
15599
|
};
|
|
15600
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15439
15601
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15440
15602
|
try {
|
|
15441
|
-
|
|
15603
|
+
const fileStat = fs.statSync(filePath);
|
|
15604
|
+
const cached = exportNamesCache.get(filePath);
|
|
15605
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15606
|
+
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15607
|
+
exportNamesCache.set(filePath, {
|
|
15608
|
+
mtimeMs: fileStat.mtimeMs,
|
|
15609
|
+
size: fileStat.size,
|
|
15610
|
+
exportedNames
|
|
15611
|
+
});
|
|
15612
|
+
return exportedNames.has(exportedName);
|
|
15442
15613
|
} catch {
|
|
15443
15614
|
return false;
|
|
15444
15615
|
}
|
|
@@ -15477,6 +15648,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
15477
15648
|
const filename = normalizeFilename(context.filename ?? "");
|
|
15478
15649
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
15479
15650
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
15651
|
+
if ((programNode.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client")) return;
|
|
15480
15652
|
if (programNode.body?.some((statement) => {
|
|
15481
15653
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
15482
15654
|
const declaration = statement.declaration;
|
|
@@ -15583,8 +15755,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15583
15755
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15584
15756
|
let didFindFetchCall = false;
|
|
15585
15757
|
walkAst(node, (child) => {
|
|
15758
|
+
if (didFindFetchCall) return false;
|
|
15586
15759
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15587
|
-
if (isFetchCall$1(child))
|
|
15760
|
+
if (isFetchCall$1(child)) {
|
|
15761
|
+
didFindFetchCall = true;
|
|
15762
|
+
return false;
|
|
15763
|
+
}
|
|
15588
15764
|
});
|
|
15589
15765
|
return didFindFetchCall;
|
|
15590
15766
|
};
|
|
@@ -15598,6 +15774,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15598
15774
|
severity: "warn",
|
|
15599
15775
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15600
15776
|
create: (context) => {
|
|
15777
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15778
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15601
15779
|
let fileHasUseClient = false;
|
|
15602
15780
|
return {
|
|
15603
15781
|
Program(programNode) {
|
|
@@ -15607,8 +15785,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15607
15785
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15608
15786
|
const callback = getEffectCallback(node);
|
|
15609
15787
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15610
|
-
|
|
15611
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15788
|
+
context.report({
|
|
15612
15789
|
node,
|
|
15613
15790
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15614
15791
|
});
|
|
@@ -16273,16 +16450,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16273
16450
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16274
16451
|
create: (context) => {
|
|
16275
16452
|
let resolveBinding = () => null;
|
|
16453
|
+
let isRouteHandlerFile = false;
|
|
16454
|
+
let mutatingSegment = null;
|
|
16276
16455
|
return {
|
|
16277
16456
|
Program(node) {
|
|
16278
16457
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16458
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16459
|
+
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16460
|
+
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16279
16461
|
},
|
|
16280
16462
|
ExportNamedDeclaration(node) {
|
|
16281
|
-
|
|
16282
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16283
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16463
|
+
if (!isRouteHandlerFile) return;
|
|
16284
16464
|
if (!isExportedGetHandler(node)) return;
|
|
16285
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16286
16465
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16287
16466
|
for (const handlerBody of handlerBodies) {
|
|
16288
16467
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17199,14 +17378,38 @@ const resolvesToRefFactoryCall = (identifier) => {
|
|
|
17199
17378
|
});
|
|
17200
17379
|
return found;
|
|
17201
17380
|
};
|
|
17202
|
-
const
|
|
17381
|
+
const unwrapExpression$2 = (node) => {
|
|
17382
|
+
if (!node) return null;
|
|
17383
|
+
if (isNodeOfType(node, "ChainExpression")) return unwrapExpression$2(node.expression);
|
|
17384
|
+
if (isNodeOfType(node, "TSNonNullExpression")) return unwrapExpression$2(node.expression);
|
|
17385
|
+
return node;
|
|
17386
|
+
};
|
|
17387
|
+
const resolvesToRefCurrentAlias = (identifier, visitedAliasNames) => {
|
|
17388
|
+
if (visitedAliasNames.has(identifier.name)) return false;
|
|
17389
|
+
const root = findProgramRoot(identifier);
|
|
17390
|
+
if (!root) return false;
|
|
17391
|
+
const nextVisited = new Set([...visitedAliasNames, identifier.name]);
|
|
17392
|
+
let found = false;
|
|
17393
|
+
walkAst(root, (child) => {
|
|
17394
|
+
if (found) return false;
|
|
17395
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.id.name === identifier.name) {
|
|
17396
|
+
const init = unwrapExpression$2(child.init);
|
|
17397
|
+
if (init && isNodeOfType(init, "MemberExpression") && isNodeOfType(init.property, "Identifier") && init.property.name === "current" && isRefLikeReceiver(init.object, nextVisited)) {
|
|
17398
|
+
found = true;
|
|
17399
|
+
return false;
|
|
17400
|
+
}
|
|
17401
|
+
}
|
|
17402
|
+
});
|
|
17403
|
+
return found;
|
|
17404
|
+
};
|
|
17405
|
+
const isRefLikeReceiver = (receiver, visitedAliasNames = /* @__PURE__ */ new Set()) => {
|
|
17203
17406
|
if (!receiver) return false;
|
|
17204
|
-
if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17205
|
-
if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17206
|
-
if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver);
|
|
17407
|
+
if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
|
|
17408
|
+
if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
|
|
17409
|
+
if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver) || resolvesToRefCurrentAlias(receiver, visitedAliasNames);
|
|
17207
17410
|
if (isNodeOfType(receiver, "MemberExpression") && isNodeOfType(receiver.property, "Identifier")) {
|
|
17208
17411
|
if (hasRefLikeName(receiver.property.name)) return true;
|
|
17209
|
-
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object);
|
|
17412
|
+
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object, visitedAliasNames);
|
|
17210
17413
|
}
|
|
17211
17414
|
return false;
|
|
17212
17415
|
};
|
|
@@ -17314,8 +17517,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17314
17517
|
programToAnalysis.set(programNode, analysis);
|
|
17315
17518
|
return analysis;
|
|
17316
17519
|
};
|
|
17520
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17317
17521
|
const getScopeForNode = (node, manager) => {
|
|
17318
17522
|
if (!node.range) return null;
|
|
17523
|
+
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17524
|
+
if (!scopeByNode) {
|
|
17525
|
+
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17526
|
+
scopeByNodeCache.set(manager, scopeByNode);
|
|
17527
|
+
}
|
|
17528
|
+
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17319
17529
|
let bestScope = null;
|
|
17320
17530
|
let bestSize = Infinity;
|
|
17321
17531
|
for (const scope of manager.scopes) {
|
|
@@ -17328,11 +17538,25 @@ const getScopeForNode = (node, manager) => {
|
|
|
17328
17538
|
bestScope = scope;
|
|
17329
17539
|
}
|
|
17330
17540
|
}
|
|
17541
|
+
scopeByNode.set(node, bestScope);
|
|
17331
17542
|
return bestScope;
|
|
17332
17543
|
};
|
|
17333
17544
|
//#endregion
|
|
17334
17545
|
//#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
|
|
17335
17546
|
const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
17547
|
+
const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
|
|
17548
|
+
const isInsideCallbackArgumentOf = (identifier, initializer) => {
|
|
17549
|
+
if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
|
|
17550
|
+
if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN.test(initializer.callee.name)) return false;
|
|
17551
|
+
const callbackArguments = (initializer.arguments ?? []).filter((argument) => isFunctionLike$1(argument));
|
|
17552
|
+
if (callbackArguments.length === 0) return false;
|
|
17553
|
+
let node = identifier;
|
|
17554
|
+
while (node && node !== initializer) {
|
|
17555
|
+
if (callbackArguments.includes(node)) return true;
|
|
17556
|
+
node = node.parent;
|
|
17557
|
+
}
|
|
17558
|
+
return false;
|
|
17559
|
+
};
|
|
17336
17560
|
const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
17337
17561
|
if (visited.has(ref)) return;
|
|
17338
17562
|
const result = visit(ref);
|
|
@@ -17345,7 +17569,10 @@ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17345
17569
|
const defNode = def.node;
|
|
17346
17570
|
const next = defNode.init ?? defNode.body;
|
|
17347
17571
|
if (!next) continue;
|
|
17348
|
-
for (const innerRef of getDownstreamRefs(analysis, next))
|
|
17572
|
+
for (const innerRef of getDownstreamRefs(analysis, next)) {
|
|
17573
|
+
if (isInsideCallbackArgumentOf(innerRef.identifier, next)) continue;
|
|
17574
|
+
ascend(analysis, innerRef, visit, visited);
|
|
17575
|
+
}
|
|
17349
17576
|
}
|
|
17350
17577
|
};
|
|
17351
17578
|
const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
@@ -17362,11 +17589,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17362
17589
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17363
17590
|
}
|
|
17364
17591
|
};
|
|
17592
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17365
17593
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17594
|
+
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17595
|
+
if (!upstreamByRef) {
|
|
17596
|
+
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17597
|
+
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17598
|
+
}
|
|
17599
|
+
const cached = upstreamByRef.get(ref);
|
|
17600
|
+
if (cached) return cached;
|
|
17366
17601
|
const refs = [];
|
|
17367
17602
|
ascend(analysis, ref, (upRef) => {
|
|
17368
17603
|
refs.push(upRef);
|
|
17369
17604
|
});
|
|
17605
|
+
upstreamByRef.set(ref, refs);
|
|
17370
17606
|
return refs;
|
|
17371
17607
|
};
|
|
17372
17608
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17376,11 +17612,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17376
17612
|
});
|
|
17377
17613
|
return nodes;
|
|
17378
17614
|
};
|
|
17615
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17379
17616
|
const getRef = (analysis, identifier) => {
|
|
17617
|
+
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17618
|
+
if (!refByIdentifier) {
|
|
17619
|
+
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17620
|
+
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17621
|
+
}
|
|
17622
|
+
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17623
|
+
let resolvedReference = null;
|
|
17380
17624
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17381
|
-
if (
|
|
17382
|
-
|
|
17383
|
-
|
|
17625
|
+
if (scope) {
|
|
17626
|
+
for (const reference of scope.references) if (reference.identifier === identifier) {
|
|
17627
|
+
resolvedReference = reference;
|
|
17628
|
+
break;
|
|
17629
|
+
}
|
|
17630
|
+
}
|
|
17631
|
+
refByIdentifier.set(identifier, resolvedReference);
|
|
17632
|
+
return resolvedReference;
|
|
17384
17633
|
};
|
|
17385
17634
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17386
17635
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17455,22 +17704,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17455
17704
|
};
|
|
17456
17705
|
//#endregion
|
|
17457
17706
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17458
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17459
|
-
if (!node.range) return null;
|
|
17460
|
-
let best = null;
|
|
17461
|
-
let bestSize = Infinity;
|
|
17462
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17463
|
-
const block = scope.block;
|
|
17464
|
-
if (!block?.range) continue;
|
|
17465
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17466
|
-
const size = block.range[1] - block.range[0];
|
|
17467
|
-
if (size <= bestSize) {
|
|
17468
|
-
bestSize = size;
|
|
17469
|
-
best = scope;
|
|
17470
|
-
}
|
|
17471
|
-
}
|
|
17472
|
-
return best;
|
|
17473
|
-
};
|
|
17474
17707
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17475
17708
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17476
17709
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17501,7 +17734,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17501
17734
|
const isWrappedSeparately = () => {
|
|
17502
17735
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17503
17736
|
const bindingName = node.id.name;
|
|
17504
|
-
const containingScope =
|
|
17737
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17505
17738
|
if (!containingScope) return false;
|
|
17506
17739
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17507
17740
|
if (!variable) return false;
|
|
@@ -17788,7 +18021,8 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
17788
18021
|
if (isNodeOfType(value, "Literal") && value.value !== "true") return;
|
|
17789
18022
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
17790
18023
|
const expression = value.expression;
|
|
17791
|
-
if (isNodeOfType(expression, "Literal")
|
|
18024
|
+
if (!isNodeOfType(expression, "Literal")) return;
|
|
18025
|
+
if (expression.value !== true && expression.value !== "true") return;
|
|
17792
18026
|
}
|
|
17793
18027
|
}
|
|
17794
18028
|
const tag = getElementType(node, context.settings);
|
|
@@ -18043,10 +18277,30 @@ const isArrayFromCall = (node) => {
|
|
|
18043
18277
|
*
|
|
18044
18278
|
* Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
|
|
18045
18279
|
*/
|
|
18046
|
-
const
|
|
18280
|
+
const isBindingReassigned = (referenceNode, bindingName) => {
|
|
18281
|
+
const programRoot = findProgramRoot(referenceNode);
|
|
18282
|
+
if (!programRoot) return false;
|
|
18283
|
+
let didFindReassignment = false;
|
|
18284
|
+
walkAst(programRoot, (child) => {
|
|
18285
|
+
if (didFindReassignment) return false;
|
|
18286
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && child.left.name === bindingName) {
|
|
18287
|
+
didFindReassignment = true;
|
|
18288
|
+
return false;
|
|
18289
|
+
}
|
|
18290
|
+
});
|
|
18291
|
+
return didFindReassignment;
|
|
18292
|
+
};
|
|
18293
|
+
const isStaticPlaceholderReceiver = (receiver, depth = 0) => {
|
|
18047
18294
|
if (isArrayFromCall(receiver)) return true;
|
|
18048
18295
|
if (isArrayConstructorCallWithNumericLength(receiver)) return true;
|
|
18049
18296
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
18297
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
18298
|
+
if (depth >= TYPE_RESOLUTION_DEPTH_LIMIT) return false;
|
|
18299
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
18300
|
+
if (!binding?.initializer) return false;
|
|
18301
|
+
if (isBindingReassigned(receiver, receiver.name)) return false;
|
|
18302
|
+
return isStaticPlaceholderReceiver(binding.initializer, depth + 1);
|
|
18303
|
+
}
|
|
18050
18304
|
if (isNodeOfType(receiver, "CallExpression")) {
|
|
18051
18305
|
const callee = receiver.callee;
|
|
18052
18306
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
|
|
@@ -18072,6 +18326,7 @@ const isArrayFromLengthObjectCall = (node) => {
|
|
|
18072
18326
|
if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
|
|
18073
18327
|
if (isNumericLiteralOrUndefined(prop.value)) return true;
|
|
18074
18328
|
if (isNodeOfType(prop.value, "Identifier")) return true;
|
|
18329
|
+
if (isNodeOfType(prop.value, "MemberExpression") && isNodeOfType(prop.value.property, "Identifier") && prop.value.property.name === "length") return true;
|
|
18075
18330
|
}
|
|
18076
18331
|
return false;
|
|
18077
18332
|
};
|
|
@@ -18180,10 +18435,11 @@ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node,
|
|
|
18180
18435
|
/**
|
|
18181
18436
|
* Walk up from a JSXAttribute node looking for the enclosing iterator
|
|
18182
18437
|
* callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
|
|
18183
|
-
* and return the
|
|
18184
|
-
*
|
|
18438
|
+
* and return the names bound by the first parameter — `item` in
|
|
18439
|
+
* `arr.map((item, index) => …)`, or every destructured field in
|
|
18440
|
+
* `arr.map(({ id, label }, index) => …)`.
|
|
18185
18441
|
*/
|
|
18186
|
-
const
|
|
18442
|
+
const findIteratorItemNames = (node) => {
|
|
18187
18443
|
let current = node;
|
|
18188
18444
|
while (current.parent) {
|
|
18189
18445
|
const parent = current.parent;
|
|
@@ -18193,18 +18449,20 @@ const findIteratorItemName$1 = (node) => {
|
|
|
18193
18449
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
18194
18450
|
if (isIteratorMethodCall || isArrayFromCallback) {
|
|
18195
18451
|
const first = (current.params ?? [])[0];
|
|
18196
|
-
if (first
|
|
18197
|
-
|
|
18452
|
+
if (!first) return null;
|
|
18453
|
+
const names = /* @__PURE__ */ new Set();
|
|
18454
|
+
collectPatternNames(first, names);
|
|
18455
|
+
return names.size > 0 ? names : null;
|
|
18198
18456
|
}
|
|
18199
18457
|
}
|
|
18200
18458
|
current = parent;
|
|
18201
18459
|
}
|
|
18202
18460
|
return null;
|
|
18203
18461
|
};
|
|
18204
|
-
const templateLiteralHasIteratorIdentity = (template,
|
|
18462
|
+
const templateLiteralHasIteratorIdentity = (template, itemNames) => {
|
|
18205
18463
|
for (const expression of template.expressions ?? []) {
|
|
18206
|
-
|
|
18207
|
-
if (
|
|
18464
|
+
const rootName = getRootIdentifierName(expression, { followCallChains: true });
|
|
18465
|
+
if (rootName !== null && itemNames.has(rootName)) return true;
|
|
18208
18466
|
}
|
|
18209
18467
|
return false;
|
|
18210
18468
|
};
|
|
@@ -18217,9 +18475,32 @@ const templateLiteralHasIteratorIdentity = (template, itemName) => {
|
|
|
18217
18475
|
const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
|
|
18218
18476
|
if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
|
|
18219
18477
|
if ((keyExpression.expressions ?? []).length < 2) return false;
|
|
18220
|
-
const
|
|
18221
|
-
if (!
|
|
18222
|
-
return templateLiteralHasIteratorIdentity(keyExpression,
|
|
18478
|
+
const itemNames = findIteratorItemNames(attributeNode);
|
|
18479
|
+
if (!itemNames) return false;
|
|
18480
|
+
return templateLiteralHasIteratorIdentity(keyExpression, itemNames);
|
|
18481
|
+
};
|
|
18482
|
+
const forLoopTestReadsDataLength = (test) => {
|
|
18483
|
+
let didFindLengthRead = false;
|
|
18484
|
+
walkAst(test, (child) => {
|
|
18485
|
+
if (didFindLengthRead) return false;
|
|
18486
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.property, "Identifier") && child.property.name === "length") {
|
|
18487
|
+
didFindLengthRead = true;
|
|
18488
|
+
return false;
|
|
18489
|
+
}
|
|
18490
|
+
});
|
|
18491
|
+
return didFindLengthRead;
|
|
18492
|
+
};
|
|
18493
|
+
const isNumericForLoopCounter = (attributeNode, indexName) => {
|
|
18494
|
+
const binding = findVariableInitializer(attributeNode, indexName);
|
|
18495
|
+
if (!binding) return false;
|
|
18496
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
18497
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
18498
|
+
const declaration = declarator.parent;
|
|
18499
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
18500
|
+
const forStatement = declaration.parent;
|
|
18501
|
+
if (!forStatement || !isNodeOfType(forStatement, "ForStatement") || forStatement.init !== declaration || !declarator.init || !isNodeOfType(declarator.init, "Literal") || typeof declarator.init.value !== "number") return false;
|
|
18502
|
+
if (forStatement.test && forLoopTestReadsDataLength(forStatement.test)) return false;
|
|
18503
|
+
return true;
|
|
18223
18504
|
};
|
|
18224
18505
|
const noArrayIndexAsKey = defineRule({
|
|
18225
18506
|
id: "no-array-index-as-key",
|
|
@@ -18231,6 +18512,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
18231
18512
|
if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
18232
18513
|
const indexName = extractIndexName(node.value.expression);
|
|
18233
18514
|
if (!indexName) return;
|
|
18515
|
+
if (isNumericForLoopCounter(node, indexName)) return;
|
|
18234
18516
|
if (isInsideStaticPlaceholderMap(node)) return;
|
|
18235
18517
|
if (isInsideStringDerivedMap(node)) return;
|
|
18236
18518
|
if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
|
|
@@ -18851,7 +19133,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18851
19133
|
}
|
|
18852
19134
|
return false;
|
|
18853
19135
|
};
|
|
18854
|
-
const
|
|
19136
|
+
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
19137
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
19138
|
+
const cachedEntry = renderOutputCache.get(functionNode);
|
|
19139
|
+
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
19140
|
+
const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
|
|
19141
|
+
renderOutputCache.set(functionNode, {
|
|
19142
|
+
scopes,
|
|
19143
|
+
hasRenderOutput
|
|
19144
|
+
});
|
|
19145
|
+
return hasRenderOutput;
|
|
19146
|
+
};
|
|
18855
19147
|
//#endregion
|
|
18856
19148
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18857
19149
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19356,8 +19648,8 @@ const CONTEXT_MODULES = [
|
|
|
19356
19648
|
];
|
|
19357
19649
|
const isCreateContextCallee = (callee) => {
|
|
19358
19650
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19359
|
-
|
|
19360
|
-
return
|
|
19651
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19652
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19361
19653
|
}
|
|
19362
19654
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19363
19655
|
const namespaceIdentifier = callee.object;
|
|
@@ -19367,8 +19659,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19367
19659
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19368
19660
|
const namespaceName = namespaceIdentifier.name;
|
|
19369
19661
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19370
|
-
|
|
19371
|
-
return
|
|
19662
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19663
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19372
19664
|
}
|
|
19373
19665
|
return false;
|
|
19374
19666
|
};
|
|
@@ -19770,38 +20062,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19770
20062
|
};
|
|
19771
20063
|
const parseColorToRgb = (value) => {
|
|
19772
20064
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19773
|
-
|
|
19774
|
-
|
|
19775
|
-
|
|
19776
|
-
|
|
19777
|
-
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
20065
|
+
if (trimmed.startsWith("#")) {
|
|
20066
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
20067
|
+
if (hex8Match) return {
|
|
20068
|
+
red: parseInt(hex8Match[1], 16),
|
|
20069
|
+
green: parseInt(hex8Match[2], 16),
|
|
20070
|
+
blue: parseInt(hex8Match[3], 16)
|
|
20071
|
+
};
|
|
20072
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
20073
|
+
if (hex6Match) return {
|
|
20074
|
+
red: parseInt(hex6Match[1], 16),
|
|
20075
|
+
green: parseInt(hex6Match[2], 16),
|
|
20076
|
+
blue: parseInt(hex6Match[3], 16)
|
|
20077
|
+
};
|
|
20078
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
20079
|
+
if (hex4Match) return {
|
|
20080
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
20081
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
20082
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
20083
|
+
};
|
|
20084
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
20085
|
+
if (hex3Match) return {
|
|
20086
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
20087
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
20088
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
20089
|
+
};
|
|
20090
|
+
}
|
|
20091
|
+
if (trimmed.includes("rgb")) {
|
|
20092
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
20093
|
+
if (rgbMatch) return {
|
|
20094
|
+
red: parseInt(rgbMatch[1], 10),
|
|
20095
|
+
green: parseInt(rgbMatch[2], 10),
|
|
20096
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
20097
|
+
};
|
|
20098
|
+
}
|
|
20099
|
+
if (trimmed.includes("hsl")) {
|
|
20100
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
20101
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
20102
|
+
}
|
|
19805
20103
|
return null;
|
|
19806
20104
|
};
|
|
19807
20105
|
//#endregion
|
|
@@ -19829,9 +20127,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19829
20127
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19830
20128
|
return null;
|
|
19831
20129
|
};
|
|
20130
|
+
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
20131
|
+
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
20132
|
+
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
20133
|
+
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19832
20134
|
const parseShadowLayerBlur = (layer) => {
|
|
19833
|
-
const
|
|
19834
|
-
|
|
20135
|
+
const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
|
|
20136
|
+
let tokenIndex = 0;
|
|
20137
|
+
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
20138
|
+
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
20139
|
+
tokenIndex += 1;
|
|
20140
|
+
}
|
|
20141
|
+
return 0;
|
|
19835
20142
|
};
|
|
19836
20143
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19837
20144
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19951,7 +20258,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19951
20258
|
};
|
|
19952
20259
|
//#endregion
|
|
19953
20260
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20261
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19954
20262
|
const collectComponentPropNames = (componentFunction) => {
|
|
20263
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20264
|
+
if (cached) return cached;
|
|
19955
20265
|
const propNames = /* @__PURE__ */ new Set();
|
|
19956
20266
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19957
20267
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19965,27 +20275,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19965
20275
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19966
20276
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19967
20277
|
});
|
|
20278
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19968
20279
|
return propNames;
|
|
19969
20280
|
};
|
|
19970
|
-
const
|
|
20281
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20282
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20283
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20284
|
+
if (cached) return cached;
|
|
19971
20285
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19972
20286
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19973
|
-
if (boundNames.has(bindingName)) return true;
|
|
19974
|
-
let declaresName = false;
|
|
19975
20287
|
walkAst(functionNode, (child) => {
|
|
19976
|
-
if (declaresName) return false;
|
|
19977
20288
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
19978
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
19979
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
19980
|
-
collectPatternNames(child.id, declaratorNames);
|
|
19981
|
-
if (declaratorNames.has(bindingName)) {
|
|
19982
|
-
declaresName = true;
|
|
19983
|
-
return false;
|
|
19984
|
-
}
|
|
19985
|
-
}
|
|
20289
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
19986
20290
|
});
|
|
19987
|
-
|
|
20291
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20292
|
+
return boundNames;
|
|
19988
20293
|
};
|
|
20294
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
19989
20295
|
const isPropertyNamePosition = (identifier) => {
|
|
19990
20296
|
const parent = identifier.parent;
|
|
19991
20297
|
if (!parent) return false;
|
|
@@ -20408,36 +20714,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20408
20714
|
}
|
|
20409
20715
|
return hasNestedFunction;
|
|
20410
20716
|
};
|
|
20411
|
-
const
|
|
20412
|
-
|
|
20413
|
-
|
|
20414
|
-
|
|
20415
|
-
|
|
20416
|
-
|
|
20417
|
-
|
|
20418
|
-
if (isReseeded) return false;
|
|
20419
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20420
|
-
isReseeded = true;
|
|
20421
|
-
return false;
|
|
20422
|
-
}
|
|
20423
|
-
});
|
|
20424
|
-
return isReseeded;
|
|
20717
|
+
const isInRenderScope = (node, componentFunction) => {
|
|
20718
|
+
let cursor = node.parent ?? null;
|
|
20719
|
+
while (cursor && cursor !== componentFunction) {
|
|
20720
|
+
if (isFunctionLike$1(cursor)) return false;
|
|
20721
|
+
cursor = cursor.parent ?? null;
|
|
20722
|
+
}
|
|
20723
|
+
return true;
|
|
20425
20724
|
};
|
|
20426
|
-
const
|
|
20725
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20427
20726
|
const setterName = getStateSetterName(useStateCall);
|
|
20428
20727
|
if (!setterName) return false;
|
|
20429
20728
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20430
20729
|
if (!componentFunction) return false;
|
|
20431
|
-
let
|
|
20730
|
+
let isExempt = false;
|
|
20432
20731
|
walkAst(componentFunction, (child) => {
|
|
20433
|
-
if (
|
|
20434
|
-
if (child
|
|
20435
|
-
|
|
20436
|
-
isAdjusted = true;
|
|
20732
|
+
if (isExempt) return false;
|
|
20733
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
|
|
20734
|
+
isExempt = true;
|
|
20437
20735
|
return false;
|
|
20438
20736
|
}
|
|
20439
20737
|
});
|
|
20440
|
-
return
|
|
20738
|
+
return isExempt;
|
|
20441
20739
|
};
|
|
20442
20740
|
const noDerivedUseState = defineRule({
|
|
20443
20741
|
id: "no-derived-useState",
|
|
@@ -20454,8 +20752,7 @@ const noDerivedUseState = defineRule({
|
|
|
20454
20752
|
const initializer = node.arguments[0];
|
|
20455
20753
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20456
20754
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20457
|
-
if (
|
|
20458
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20755
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20459
20756
|
context.report({
|
|
20460
20757
|
node,
|
|
20461
20758
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20466,8 +20763,7 @@ const noDerivedUseState = defineRule({
|
|
|
20466
20763
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20467
20764
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20468
20765
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20469
|
-
if (
|
|
20470
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20766
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20471
20767
|
context.report({
|
|
20472
20768
|
node,
|
|
20473
20769
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -20582,7 +20878,7 @@ const isStateMemberExpression = (node) => {
|
|
|
20582
20878
|
};
|
|
20583
20879
|
//#endregion
|
|
20584
20880
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
20585
|
-
const MESSAGE$24 = "
|
|
20881
|
+
const MESSAGE$24 = "Mutating `this.state` by hand never triggers a redraw on its own & a later setState can overwrite it, so use `this.setState` instead.";
|
|
20586
20882
|
const shouldIgnoreMutation = (node) => {
|
|
20587
20883
|
let isConstructor = false;
|
|
20588
20884
|
let isInsideCallExpression = false;
|
|
@@ -20676,6 +20972,18 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
20676
20972
|
}
|
|
20677
20973
|
return producesPlainStateValue(unwrapped);
|
|
20678
20974
|
};
|
|
20975
|
+
const collectCallbackRefSetterNames = (componentBody) => {
|
|
20976
|
+
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
20977
|
+
walkAst(componentBody, (node) => {
|
|
20978
|
+
if (!isNodeOfType(node, "JSXAttribute")) return;
|
|
20979
|
+
const attributeName = node.name;
|
|
20980
|
+
if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
|
|
20981
|
+
const expression = stripParenExpression(node.value.expression);
|
|
20982
|
+
if (isNodeOfType(expression, "Identifier")) callbackRefSetterNames.add(expression.name);
|
|
20983
|
+
}
|
|
20984
|
+
});
|
|
20985
|
+
return callbackRefSetterNames;
|
|
20986
|
+
};
|
|
20679
20987
|
const collectFunctionLocalBindings = (functionNode) => {
|
|
20680
20988
|
const localBindings = /* @__PURE__ */ new Set();
|
|
20681
20989
|
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
|
|
@@ -20718,8 +21026,10 @@ const noDirectStateMutation = defineRule({
|
|
|
20718
21026
|
const bindings = collectUseStateBindings(componentBody);
|
|
20719
21027
|
if (bindings.length === 0) return;
|
|
20720
21028
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
21029
|
+
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
20721
21030
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
20722
21031
|
for (const binding of bindings) {
|
|
21032
|
+
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
20723
21033
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
20724
21034
|
if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
|
|
20725
21035
|
}
|
|
@@ -20732,7 +21042,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20732
21042
|
if (currentlyShadowed.has(rootName)) return;
|
|
20733
21043
|
context.report({
|
|
20734
21044
|
node: child,
|
|
20735
|
-
message: `
|
|
21045
|
+
message: `React can't tell you changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20736
21046
|
});
|
|
20737
21047
|
return;
|
|
20738
21048
|
}
|
|
@@ -20748,7 +21058,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20748
21058
|
if (currentlyShadowed.has(rootName)) return;
|
|
20749
21059
|
context.report({
|
|
20750
21060
|
node: child,
|
|
20751
|
-
message: `
|
|
21061
|
+
message: `React can't tell .${methodName}() changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20752
21062
|
});
|
|
20753
21063
|
}
|
|
20754
21064
|
});
|
|
@@ -22166,20 +22476,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22166
22476
|
return null;
|
|
22167
22477
|
};
|
|
22168
22478
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22479
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22480
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22481
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22482
|
+
walkAst(componentBody, (child) => {
|
|
22483
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22484
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22485
|
+
const setterName = child.callee.name;
|
|
22486
|
+
if (!setterNames.has(setterName)) return;
|
|
22487
|
+
settersWithAnyCall.add(setterName);
|
|
22488
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22489
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22490
|
+
});
|
|
22169
22491
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22170
|
-
for (const binding of useStateBindings)
|
|
22171
|
-
let didFindAnySetterCall = false;
|
|
22172
|
-
let areAllSetterCallsInHandlers = true;
|
|
22173
|
-
walkAst(componentBody, (child) => {
|
|
22174
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22175
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22176
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22177
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22178
|
-
didFindAnySetterCall = true;
|
|
22179
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22180
|
-
});
|
|
22181
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22182
|
-
}
|
|
22492
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22183
22493
|
return handlerOnlyWriteStateNames;
|
|
22184
22494
|
};
|
|
22185
22495
|
const noEventTriggerState = defineRule({
|
|
@@ -22519,6 +22829,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22519
22829
|
const TEXT_COLOR_PATTERN = /^text-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22520
22830
|
const BG_COLOR_PATTERN = /^bg-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22521
22831
|
const splitVariantScope = (token) => {
|
|
22832
|
+
if (!token.includes(":")) return {
|
|
22833
|
+
scope: "",
|
|
22834
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22835
|
+
};
|
|
22522
22836
|
const segments = token.split(":");
|
|
22523
22837
|
const rawUtility = segments[segments.length - 1];
|
|
22524
22838
|
return {
|
|
@@ -22542,6 +22856,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22542
22856
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22543
22857
|
for (const token of classStr.split(/\s+/)) {
|
|
22544
22858
|
if (!token) continue;
|
|
22859
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22545
22860
|
const { scope, utility } = splitVariantScope(token);
|
|
22546
22861
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22547
22862
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -22612,11 +22927,16 @@ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
|
|
|
22612
22927
|
"ulid",
|
|
22613
22928
|
"createId"
|
|
22614
22929
|
]);
|
|
22930
|
+
const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
|
|
22615
22931
|
const containsNonDeterministicSource = (root) => {
|
|
22616
22932
|
let found = false;
|
|
22617
22933
|
walkAst(root, (child) => {
|
|
22618
22934
|
if (found) return false;
|
|
22619
22935
|
if (isFunctionLike$1(child)) return false;
|
|
22936
|
+
if (isZeroArgDateConstruction(child)) {
|
|
22937
|
+
found = true;
|
|
22938
|
+
return false;
|
|
22939
|
+
}
|
|
22620
22940
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22621
22941
|
const callee = child.callee;
|
|
22622
22942
|
if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
|
|
@@ -22677,6 +22997,53 @@ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames
|
|
|
22677
22997
|
});
|
|
22678
22998
|
return found;
|
|
22679
22999
|
};
|
|
23000
|
+
const isResourceLikeInitializer = (initializer) => {
|
|
23001
|
+
if (isNodeOfType(initializer, "AwaitExpression")) return isResourceLikeInitializer(initializer.argument);
|
|
23002
|
+
return isNodeOfType(initializer, "NewExpression") || isNodeOfType(initializer, "CallExpression");
|
|
23003
|
+
};
|
|
23004
|
+
const collectArgumentSourceLocalNames = (argument, effectFn, sourceLocalNames = /* @__PURE__ */ new Set()) => {
|
|
23005
|
+
walkAst(argument, (child) => {
|
|
23006
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
23007
|
+
if (sourceLocalNames.has(child.name)) return;
|
|
23008
|
+
const localInitializer = findEffectLocalInitializer(effectFn, child.name);
|
|
23009
|
+
if (!localInitializer || !isResourceLikeInitializer(localInitializer)) return;
|
|
23010
|
+
sourceLocalNames.add(child.name);
|
|
23011
|
+
collectArgumentSourceLocalNames(localInitializer, effectFn, sourceLocalNames);
|
|
23012
|
+
});
|
|
23013
|
+
return sourceLocalNames;
|
|
23014
|
+
};
|
|
23015
|
+
const isFunctionExpressionLike$1 = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression");
|
|
23016
|
+
const findCleanupFunction = (effectFn) => {
|
|
23017
|
+
if (!isNodeOfType(effectFn, "ArrowFunctionExpression") && !isNodeOfType(effectFn, "FunctionExpression")) return null;
|
|
23018
|
+
const body = effectFn.body;
|
|
23019
|
+
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
23020
|
+
let cleanupFunction = null;
|
|
23021
|
+
walkAst(body, (child) => {
|
|
23022
|
+
if (cleanupFunction) return false;
|
|
23023
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
23024
|
+
if (child.argument && isFunctionExpressionLike$1(child.argument)) cleanupFunction = child.argument;
|
|
23025
|
+
return false;
|
|
23026
|
+
}
|
|
23027
|
+
if (child !== body && isFunctionExpressionLike$1(child)) return false;
|
|
23028
|
+
if (isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
23029
|
+
});
|
|
23030
|
+
return cleanupFunction;
|
|
23031
|
+
};
|
|
23032
|
+
const cleanupDisposesArgumentSource = (argument, effectFn) => {
|
|
23033
|
+
const cleanupFunction = findCleanupFunction(effectFn);
|
|
23034
|
+
if (!cleanupFunction) return false;
|
|
23035
|
+
const sourceLocalNames = collectArgumentSourceLocalNames(argument, effectFn);
|
|
23036
|
+
if (sourceLocalNames.size === 0) return false;
|
|
23037
|
+
let referencesSource = false;
|
|
23038
|
+
walkAst(cleanupFunction, (child) => {
|
|
23039
|
+
if (referencesSource) return false;
|
|
23040
|
+
if (isNodeOfType(child, "Identifier") && sourceLocalNames.has(child.name)) {
|
|
23041
|
+
referencesSource = true;
|
|
23042
|
+
return false;
|
|
23043
|
+
}
|
|
23044
|
+
});
|
|
23045
|
+
return referencesSource;
|
|
23046
|
+
};
|
|
22680
23047
|
const noInitializeState = defineRule({
|
|
22681
23048
|
id: "no-initialize-state",
|
|
22682
23049
|
title: "State initialized from a mount effect",
|
|
@@ -22699,6 +23066,7 @@ const noInitializeState = defineRule({
|
|
|
22699
23066
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
22700
23067
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
|
|
22701
23068
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
|
|
23069
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
|
|
22702
23070
|
const useStateDecl = getUseStateDecl(analysis, ref);
|
|
22703
23071
|
if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
|
|
22704
23072
|
if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
|
|
@@ -23271,6 +23639,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23271
23639
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23272
23640
|
});
|
|
23273
23641
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23642
|
+
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23643
|
+
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23274
23644
|
const noLongTransitionDuration = defineRule({
|
|
23275
23645
|
id: "no-long-transition-duration",
|
|
23276
23646
|
title: "Transition duration too long",
|
|
@@ -23294,11 +23664,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23294
23664
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23295
23665
|
let longestDurationPropertyMs = 0;
|
|
23296
23666
|
for (const segment of value.split(",")) {
|
|
23297
|
-
const
|
|
23298
|
-
|
|
23299
|
-
const
|
|
23300
|
-
|
|
23301
|
-
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23667
|
+
const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
|
|
23668
|
+
if (!durationMatch) continue;
|
|
23669
|
+
const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
|
|
23670
|
+
longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
|
|
23302
23671
|
}
|
|
23303
23672
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23304
23673
|
}
|
|
@@ -23306,7 +23675,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23306
23675
|
let longestDurationMs = 0;
|
|
23307
23676
|
for (const segment of value.split(",")) {
|
|
23308
23677
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23309
|
-
const firstTimeMatch = segment.match(
|
|
23678
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23310
23679
|
if (!firstTimeMatch) continue;
|
|
23311
23680
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23312
23681
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24478,14 +24847,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24478
24847
|
out.hasOpaqueBranch = true;
|
|
24479
24848
|
};
|
|
24480
24849
|
const buildMessage$12 = (tag) => `Keyboard & screen reader users can't trigger this \`<${tag}>\` because it isn't interactive, so use a button or link or add an interactive role.`;
|
|
24481
|
-
const
|
|
24850
|
+
const INTERACTIVE_HANDLERS_LOWER = new Set([
|
|
24482
24851
|
"onClick",
|
|
24483
24852
|
"onMouseDown",
|
|
24484
24853
|
"onMouseUp",
|
|
24485
24854
|
"onKeyDown",
|
|
24486
24855
|
"onKeyPress",
|
|
24487
24856
|
"onKeyUp"
|
|
24488
|
-
];
|
|
24857
|
+
].map((handlerName) => handlerName.toLowerCase()));
|
|
24489
24858
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24490
24859
|
id: "no-noninteractive-element-interactions",
|
|
24491
24860
|
title: "Handler on non-interactive element",
|
|
@@ -24500,7 +24869,16 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24500
24869
|
const tag = getElementType(node, context.settings);
|
|
24501
24870
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24502
24871
|
if (tag === "label") return;
|
|
24503
|
-
|
|
24872
|
+
let hasHandler = false;
|
|
24873
|
+
for (const attribute of node.attributes) {
|
|
24874
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24875
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
24876
|
+
if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
24877
|
+
hasHandler = true;
|
|
24878
|
+
break;
|
|
24879
|
+
}
|
|
24880
|
+
}
|
|
24881
|
+
if (!hasHandler) return;
|
|
24504
24882
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24505
24883
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24506
24884
|
if (roleAttr) {
|
|
@@ -24606,6 +24984,18 @@ const KEYBOARD_HANDLER_PROP_NAMES = [
|
|
|
24606
24984
|
"onKeyPress"
|
|
24607
24985
|
];
|
|
24608
24986
|
const isKeyboardOperable = (node) => KEYBOARD_HANDLER_PROP_NAMES.some((propName) => Boolean(hasJsxPropIgnoreCase(node.attributes, propName)));
|
|
24987
|
+
const parseNumericBranch = (expression) => {
|
|
24988
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "number") return expression.value;
|
|
24989
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-" && isNodeOfType(expression.argument, "Literal") && typeof expression.argument.value === "number") return -expression.argument.value;
|
|
24990
|
+
return null;
|
|
24991
|
+
};
|
|
24992
|
+
const isRovingTabindexValue = (value) => {
|
|
24993
|
+
if (!isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
24994
|
+
const expression = value.expression;
|
|
24995
|
+
if (!isNodeOfType(expression, "ConditionalExpression")) return false;
|
|
24996
|
+
if (isNodeOfType(expression.test, "Literal")) return false;
|
|
24997
|
+
return [parseNumericBranch(expression.consequent), parseNumericBranch(expression.alternate)].some((branchValue) => branchValue !== null && branchValue < 0);
|
|
24998
|
+
};
|
|
24609
24999
|
const resolveSettings$14 = (settings) => {
|
|
24610
25000
|
const reactDoctor = settings?.["react-doctor"];
|
|
24611
25001
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -24629,6 +25019,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
24629
25019
|
if (!tabIndex) return;
|
|
24630
25020
|
const tabIndexValue = tabIndex.value;
|
|
24631
25021
|
if (!tabIndexValue) return;
|
|
25022
|
+
if (isRovingTabindexValue(tabIndexValue)) return;
|
|
24632
25023
|
const numeric = parseJsxValue(tabIndexValue);
|
|
24633
25024
|
if (numeric === null) {
|
|
24634
25025
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node)) context.report({
|
|
@@ -25813,16 +26204,22 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25813
26204
|
["tr", "row"],
|
|
25814
26205
|
["ul", "list"]
|
|
25815
26206
|
];
|
|
25816
|
-
const
|
|
25817
|
-
|
|
25818
|
-
|
|
25819
|
-
|
|
25820
|
-
|
|
25821
|
-
const
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
26207
|
+
const EMPTY_ROLE_LIST = [];
|
|
26208
|
+
const buildLookup = (pairs, keyIndex) => {
|
|
26209
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
26210
|
+
for (const pair of pairs) {
|
|
26211
|
+
const key = pair[keyIndex];
|
|
26212
|
+
const value = pair[keyIndex === 0 ? 1 : 0];
|
|
26213
|
+
const values = lookup.get(key);
|
|
26214
|
+
if (!values) lookup.set(key, [value]);
|
|
26215
|
+
else if (!values.includes(value)) values.push(value);
|
|
26216
|
+
}
|
|
26217
|
+
return lookup;
|
|
25825
26218
|
};
|
|
26219
|
+
const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
|
|
26220
|
+
const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
|
|
26221
|
+
const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
|
|
26222
|
+
const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
|
|
25826
26223
|
//#endregion
|
|
25827
26224
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
25828
26225
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26059,22 +26456,22 @@ const noRenderInRender = defineRule({
|
|
|
26059
26456
|
title: "Component rendered by inline function call",
|
|
26060
26457
|
severity: "warn",
|
|
26061
26458
|
tags: ["test-noise"],
|
|
26062
|
-
recommendation: "Make it a named component so React
|
|
26459
|
+
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
26063
26460
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
26064
26461
|
const expression = node.expression;
|
|
26065
26462
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26066
26463
|
let calleeName = null;
|
|
26464
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26465
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26466
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26067
26467
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26068
26468
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26069
|
-
|
|
26070
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26469
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26071
26470
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26072
|
-
calleeName = expression.callee.property.name;
|
|
26073
26471
|
}
|
|
26074
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26075
26472
|
context.report({
|
|
26076
26473
|
node: expression,
|
|
26077
|
-
message: `
|
|
26474
|
+
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
26078
26475
|
});
|
|
26079
26476
|
} })
|
|
26080
26477
|
});
|
|
@@ -26177,8 +26574,8 @@ const countUseStates = (analysis, componentNode) => {
|
|
|
26177
26574
|
for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
|
|
26178
26575
|
return stateVariables.size;
|
|
26179
26576
|
};
|
|
26180
|
-
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode) => {
|
|
26181
|
-
const stateSetterRefs = effectFnRefs.filter((ref) =>
|
|
26577
|
+
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
|
|
26578
|
+
const stateSetterRefs = effectFnRefs.filter((ref) => isSyncStateSetterCall(analysis, ref, effectFn));
|
|
26182
26579
|
if (stateSetterRefs.length === 0) return null;
|
|
26183
26580
|
if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
|
|
26184
26581
|
const containing = findContainingNode(analysis, useEffectNode);
|
|
@@ -26201,7 +26598,9 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
26201
26598
|
if (!effectFnRefs || !depsRefs) return;
|
|
26202
26599
|
const containing = findContainingNode(analysis, node);
|
|
26203
26600
|
if (containing && isCustomHook(containing)) return;
|
|
26204
|
-
|
|
26601
|
+
const effectFn = getEffectFn(analysis, node);
|
|
26602
|
+
if (!effectFn) return;
|
|
26603
|
+
if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node, effectFn)) return;
|
|
26205
26604
|
context.report({
|
|
26206
26605
|
node,
|
|
26207
26606
|
message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
|
|
@@ -26322,6 +26721,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26322
26721
|
"headers",
|
|
26323
26722
|
"remountDeps"
|
|
26324
26723
|
];
|
|
26724
|
+
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26325
26725
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26326
26726
|
"createFileRoute",
|
|
26327
26727
|
"createRoute",
|
|
@@ -26337,6 +26737,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26337
26737
|
"server",
|
|
26338
26738
|
"handler"
|
|
26339
26739
|
];
|
|
26740
|
+
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26340
26741
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26341
26742
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26342
26743
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27041,14 +27442,21 @@ const noStaticElementInteractions = defineRule({
|
|
|
27041
27442
|
category: "Accessibility",
|
|
27042
27443
|
create: (context) => {
|
|
27043
27444
|
const settings = resolveSettings$12(context.settings);
|
|
27445
|
+
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27044
27446
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27045
27447
|
return { JSXOpeningElement(node) {
|
|
27046
27448
|
if (isTestlikeFile) return;
|
|
27047
27449
|
let hasNonBlockerHandler = false;
|
|
27048
27450
|
let hasAnyHandler = false;
|
|
27049
|
-
|
|
27050
|
-
|
|
27051
|
-
if (!attribute) continue;
|
|
27451
|
+
let seenHandlerNames = null;
|
|
27452
|
+
for (const attribute of node.attributes) {
|
|
27453
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
27454
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
27455
|
+
if (!attributeName) continue;
|
|
27456
|
+
const handlerNameLower = attributeName.toLowerCase();
|
|
27457
|
+
if (!handlersLower.has(handlerNameLower)) continue;
|
|
27458
|
+
if (seenHandlerNames?.has(handlerNameLower)) continue;
|
|
27459
|
+
(seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
27052
27460
|
if (isNullValue(attribute)) continue;
|
|
27053
27461
|
hasAnyHandler = true;
|
|
27054
27462
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -27060,6 +27468,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
27060
27468
|
if (!hasNonBlockerHandler) return;
|
|
27061
27469
|
const elementType = getElementType(node, context.settings);
|
|
27062
27470
|
if (!HTML_TAGS.has(elementType)) return;
|
|
27471
|
+
if (elementType === "svg") return;
|
|
27063
27472
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
27064
27473
|
if (isPresentationRole(node)) return;
|
|
27065
27474
|
if (isInteractiveElement(elementType, node)) return;
|
|
@@ -27073,7 +27482,8 @@ const noStaticElementInteractions = defineRule({
|
|
|
27073
27482
|
});
|
|
27074
27483
|
return;
|
|
27075
27484
|
}
|
|
27076
|
-
|
|
27485
|
+
let attributeValue = roleAttribute.value;
|
|
27486
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal") && typeof attributeValue.expression.value === "string") attributeValue = attributeValue.expression;
|
|
27077
27487
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
27078
27488
|
const firstRole = attributeValue.value.toLowerCase().trim().split(/\s+/)[0];
|
|
27079
27489
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
@@ -28229,13 +28639,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28229
28639
|
["xml:lang", "xmlLang"],
|
|
28230
28640
|
["xml:space", "xmlSpace"]
|
|
28231
28641
|
]);
|
|
28232
|
-
const
|
|
28642
|
+
const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
|
|
28233
28643
|
"charset",
|
|
28234
28644
|
"allowFullScreen",
|
|
28235
28645
|
"webkitAllowFullScreen",
|
|
28236
28646
|
"mozAllowFullScreen",
|
|
28237
28647
|
"webkitDirectory"
|
|
28238
|
-
];
|
|
28648
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
28239
28649
|
//#endregion
|
|
28240
28650
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28241
28651
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28439,10 +28849,7 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28439
28849
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28440
28850
|
return !tagName.includes("-");
|
|
28441
28851
|
};
|
|
28442
|
-
const normalizeAttributeCase = (name) =>
|
|
28443
|
-
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28444
|
-
return name;
|
|
28445
|
-
};
|
|
28852
|
+
const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
|
|
28446
28853
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28447
28854
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28448
28855
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28786,32 +29193,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28786
29193
|
if (!isHocCallee$1(parent)) return false;
|
|
28787
29194
|
return parent.arguments[0] === node;
|
|
28788
29195
|
};
|
|
29196
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
29197
|
+
"map",
|
|
29198
|
+
"forEach",
|
|
29199
|
+
"filter",
|
|
29200
|
+
"flatMap",
|
|
29201
|
+
"reduce",
|
|
29202
|
+
"reduceRight"
|
|
29203
|
+
]);
|
|
28789
29204
|
const isReturnOfMapCallback = (node) => {
|
|
28790
29205
|
const parent = node.parent;
|
|
28791
29206
|
if (!parent) return false;
|
|
28792
29207
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28793
29208
|
const callee = parent.callee;
|
|
28794
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28795
|
-
"map",
|
|
28796
|
-
"forEach",
|
|
28797
|
-
"filter",
|
|
28798
|
-
"flatMap",
|
|
28799
|
-
"reduce",
|
|
28800
|
-
"reduceRight"
|
|
28801
|
-
].includes(callee.property.name);
|
|
29209
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28802
29210
|
}
|
|
28803
29211
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28804
29212
|
const callbackParent = parent.parent;
|
|
28805
29213
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28806
29214
|
const callee = callbackParent.callee;
|
|
28807
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28808
|
-
"map",
|
|
28809
|
-
"forEach",
|
|
28810
|
-
"filter",
|
|
28811
|
-
"flatMap",
|
|
28812
|
-
"reduce",
|
|
28813
|
-
"reduceRight"
|
|
28814
|
-
].includes(callee.property.name);
|
|
29215
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28815
29216
|
}
|
|
28816
29217
|
}
|
|
28817
29218
|
return false;
|
|
@@ -28847,10 +29248,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28847
29248
|
valueNode = parent;
|
|
28848
29249
|
parent = parent.parent;
|
|
28849
29250
|
continue;
|
|
28850
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
29251
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
|
|
28851
29252
|
case "AssignmentExpression": {
|
|
28852
29253
|
const assignmentTarget = parent.left;
|
|
28853
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
29254
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28854
29255
|
}
|
|
28855
29256
|
case "Property":
|
|
28856
29257
|
if (parent.value !== valueNode) return false;
|
|
@@ -28943,14 +29344,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28943
29344
|
};
|
|
28944
29345
|
return {
|
|
28945
29346
|
JSXOpeningElement(node) {
|
|
28946
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29347
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28947
29348
|
recordInstantiation(node.name, node.name.name);
|
|
28948
29349
|
return;
|
|
28949
29350
|
}
|
|
28950
29351
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28951
29352
|
},
|
|
28952
29353
|
Identifier(node) {
|
|
28953
|
-
if (!
|
|
29354
|
+
if (!isReactComponentName(node.name)) return;
|
|
28954
29355
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28955
29356
|
recordInstantiation(node, node.name);
|
|
28956
29357
|
},
|
|
@@ -29521,21 +29922,18 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
29521
29922
|
reportNode
|
|
29522
29923
|
};
|
|
29523
29924
|
};
|
|
29524
|
-
const
|
|
29525
|
-
const
|
|
29526
|
-
const
|
|
29527
|
-
|
|
29528
|
-
const
|
|
29529
|
-
|
|
29530
|
-
|
|
29531
|
-
|
|
29532
|
-
|
|
29533
|
-
|
|
29534
|
-
|
|
29535
|
-
}
|
|
29925
|
+
const collectRelevantNodes = (programRoot) => {
|
|
29926
|
+
const exportNodes = [];
|
|
29927
|
+
const componentCandidates = [];
|
|
29928
|
+
walkAst(programRoot, (child) => {
|
|
29929
|
+
const childType = child.type;
|
|
29930
|
+
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
29931
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
29932
|
+
});
|
|
29933
|
+
return {
|
|
29934
|
+
exportNodes,
|
|
29935
|
+
componentCandidates
|
|
29536
29936
|
};
|
|
29537
|
-
visit(programRoot);
|
|
29538
|
-
return out;
|
|
29539
29937
|
};
|
|
29540
29938
|
const isEntryPointFile = (filename) => {
|
|
29541
29939
|
const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
@@ -29582,13 +29980,13 @@ const onlyExportComponents = defineRule({
|
|
|
29582
29980
|
};
|
|
29583
29981
|
return { Program(node) {
|
|
29584
29982
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
29585
|
-
const
|
|
29983
|
+
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
29586
29984
|
const exports = [];
|
|
29587
29985
|
let hasReactExport = false;
|
|
29588
29986
|
let hasAnyExports = false;
|
|
29589
29987
|
const localComponents = [];
|
|
29590
29988
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
29591
|
-
for (const child of
|
|
29989
|
+
for (const child of exportNodes) {
|
|
29592
29990
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
29593
29991
|
if (child.exportKind === "type") continue;
|
|
29594
29992
|
hasAnyExports = true;
|
|
@@ -29724,12 +30122,12 @@ const onlyExportComponents = defineRule({
|
|
|
29724
30122
|
}
|
|
29725
30123
|
return false;
|
|
29726
30124
|
};
|
|
29727
|
-
for (const child of
|
|
30125
|
+
for (const child of componentCandidates) {
|
|
29728
30126
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
29729
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
|
|
30127
|
+
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29730
30128
|
}
|
|
29731
30129
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
29732
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child)) localComponents.push(child.id);
|
|
30130
|
+
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29733
30131
|
}
|
|
29734
30132
|
}
|
|
29735
30133
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
@@ -29884,7 +30282,7 @@ const containsVnodeFactoryCall = (root) => {
|
|
|
29884
30282
|
};
|
|
29885
30283
|
const isComponentLikeFunction = (functionNode) => {
|
|
29886
30284
|
const bindingName = getFunctionBindingName(functionNode);
|
|
29887
|
-
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN.test(bindingName))) return true;
|
|
30285
|
+
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$1.test(bindingName))) return true;
|
|
29888
30286
|
const body = "body" in functionNode ? functionNode.body : null;
|
|
29889
30287
|
if (!body || !isAstNode(body)) return false;
|
|
29890
30288
|
return containsJsxElement(body) || containsVnodeFactoryCall(body);
|
|
@@ -30763,13 +31161,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30763
31161
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30764
31162
|
},
|
|
30765
31163
|
JSXAttribute(node) {
|
|
30766
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30767
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30768
31164
|
if (!node.value) return;
|
|
30769
31165
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30770
31166
|
const innerExpression = node.value.expression;
|
|
30771
31167
|
if (!innerExpression) return;
|
|
30772
31168
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
31169
|
+
if (!isInsideFunctionScope(node)) return;
|
|
31170
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30773
31171
|
const parentJsxOpening = node.parent;
|
|
30774
31172
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30775
31173
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -31341,7 +31739,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
31341
31739
|
});
|
|
31342
31740
|
if (!hasCacheUpdate) context.report({
|
|
31343
31741
|
node,
|
|
31344
|
-
message: "useMutation with no cache update
|
|
31742
|
+
message: "useMutation with no cache update here can leave your users looking at stale data after it runs."
|
|
31345
31743
|
});
|
|
31346
31744
|
} })
|
|
31347
31745
|
});
|
|
@@ -31990,6 +32388,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
31990
32388
|
return { JSXExpressionContainer(node) {
|
|
31991
32389
|
if (isTestlikeFile) return;
|
|
31992
32390
|
if (!node.expression) return;
|
|
32391
|
+
if (isGeneratedImageRenderContext(context, findOpeningElementOfChild(node) ?? node)) return;
|
|
31993
32392
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
31994
32393
|
if (matched) {
|
|
31995
32394
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
@@ -32141,41 +32540,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32141
32540
|
});
|
|
32142
32541
|
return found;
|
|
32143
32542
|
};
|
|
32144
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32145
|
-
if (!componentBody) return false;
|
|
32146
|
-
let found = false;
|
|
32147
|
-
walkAst(componentBody, (child) => {
|
|
32148
|
-
if (found) return;
|
|
32149
|
-
if (!isFunctionLike$1(child)) return;
|
|
32150
|
-
const functionBody = child.body;
|
|
32151
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32152
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32153
|
-
});
|
|
32154
|
-
return found;
|
|
32155
|
-
};
|
|
32156
32543
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32157
32544
|
"then",
|
|
32158
32545
|
"catch",
|
|
32159
32546
|
"finally"
|
|
32160
32547
|
]);
|
|
32161
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32162
|
-
if (!componentBody) return false;
|
|
32163
|
-
let found = false;
|
|
32164
|
-
walkAst(componentBody, (child) => {
|
|
32165
|
-
if (found) return;
|
|
32166
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32167
|
-
const callee = child.callee;
|
|
32168
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32169
|
-
for (const argument of child.arguments ?? []) {
|
|
32170
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32171
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32172
|
-
found = true;
|
|
32173
|
-
return;
|
|
32174
|
-
}
|
|
32175
|
-
}
|
|
32176
|
-
});
|
|
32177
|
-
return found;
|
|
32178
|
-
};
|
|
32179
32548
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32180
32549
|
"useApolloClient",
|
|
32181
32550
|
"useMutation",
|
|
@@ -32188,20 +32557,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32188
32557
|
"fetch",
|
|
32189
32558
|
"axios"
|
|
32190
32559
|
]);
|
|
32191
|
-
const
|
|
32192
|
-
if (!body) return false;
|
|
32560
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32193
32561
|
let found = false;
|
|
32194
|
-
walkAst(
|
|
32195
|
-
if (found) return;
|
|
32562
|
+
walkAst(fnBody, (child) => {
|
|
32563
|
+
if (found) return false;
|
|
32196
32564
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32197
32565
|
const callee = child.callee;
|
|
32198
32566
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32199
32567
|
found = true;
|
|
32200
|
-
return;
|
|
32568
|
+
return false;
|
|
32569
|
+
}
|
|
32570
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32571
|
+
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32572
|
+
found = true;
|
|
32573
|
+
return false;
|
|
32574
|
+
}
|
|
32575
|
+
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32576
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32577
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32578
|
+
found = true;
|
|
32579
|
+
return false;
|
|
32580
|
+
}
|
|
32581
|
+
}
|
|
32201
32582
|
}
|
|
32202
|
-
|
|
32583
|
+
return;
|
|
32584
|
+
}
|
|
32585
|
+
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32586
|
+
const functionBody = child.body;
|
|
32587
|
+
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32203
32588
|
found = true;
|
|
32204
|
-
return;
|
|
32589
|
+
return false;
|
|
32205
32590
|
}
|
|
32206
32591
|
}
|
|
32207
32592
|
});
|
|
@@ -32226,7 +32611,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32226
32611
|
const secondBinding = node.id.elements[1];
|
|
32227
32612
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32228
32613
|
const fnBody = enclosingFunctionBody(node);
|
|
32229
|
-
if (fnBody && (
|
|
32614
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32230
32615
|
context.report({
|
|
32231
32616
|
node: node.init,
|
|
32232
32617
|
message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
|
|
@@ -33020,17 +33405,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33020
33405
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33021
33406
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33022
33407
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33408
|
+
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33409
|
+
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33410
|
+
walkAst(componentBody, (child) => {
|
|
33411
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33412
|
+
});
|
|
33023
33413
|
for (const binding of bindings) {
|
|
33024
33414
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33025
33415
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33026
33416
|
const setterSuffix = binding.setterName.slice(3);
|
|
33027
33417
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33028
|
-
|
|
33029
|
-
walkAst(componentBody, (child) => {
|
|
33030
|
-
if (setterCalled) return;
|
|
33031
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33032
|
-
});
|
|
33033
|
-
if (!setterCalled) continue;
|
|
33418
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33034
33419
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33035
33420
|
context.report({
|
|
33036
33421
|
node: binding.declarator,
|
|
@@ -33362,6 +33747,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33362
33747
|
FlashList: ["@shopify/flash-list"],
|
|
33363
33748
|
LegendList: ["@legendapp/list"]
|
|
33364
33749
|
};
|
|
33750
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33365
33751
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33366
33752
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33367
33753
|
"renderItem",
|
|
@@ -33604,33 +33990,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33604
33990
|
requires: ["react-native"],
|
|
33605
33991
|
severity: "warn",
|
|
33606
33992
|
recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
|
|
33607
|
-
create: (context) =>
|
|
33608
|
-
|
|
33609
|
-
|
|
33610
|
-
|
|
33611
|
-
|
|
33612
|
-
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
hasDataProp =
|
|
33623
|
-
|
|
33993
|
+
create: (context) => {
|
|
33994
|
+
let fileImportsRecycler = false;
|
|
33995
|
+
return {
|
|
33996
|
+
Program(node) {
|
|
33997
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33998
|
+
},
|
|
33999
|
+
JSXOpeningElement(node) {
|
|
34000
|
+
if (!fileImportsRecycler) return;
|
|
34001
|
+
const localElementName = resolveJsxElementName(node);
|
|
34002
|
+
if (!localElementName) return;
|
|
34003
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
34004
|
+
if (canonicalRecyclerName === null) return;
|
|
34005
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
34006
|
+
let hasSizingHint = false;
|
|
34007
|
+
let dataIsEmptyLiteral = false;
|
|
34008
|
+
let hasDataProp = false;
|
|
34009
|
+
for (const attribute of node.attributes ?? []) {
|
|
34010
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
34011
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
34012
|
+
const attributeName = attribute.name.name;
|
|
34013
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
34014
|
+
if (attributeName === "data") {
|
|
34015
|
+
hasDataProp = true;
|
|
34016
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
34017
|
+
}
|
|
34018
|
+
}
|
|
34019
|
+
if (hasSizingHint) return;
|
|
34020
|
+
if (dataIsEmptyLiteral) return;
|
|
34021
|
+
if (!hasDataProp) return;
|
|
34022
|
+
context.report({
|
|
34023
|
+
node,
|
|
34024
|
+
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
34025
|
+
});
|
|
33624
34026
|
}
|
|
33625
|
-
}
|
|
33626
|
-
|
|
33627
|
-
if (dataIsEmptyLiteral) return;
|
|
33628
|
-
if (!hasDataProp) return;
|
|
33629
|
-
context.report({
|
|
33630
|
-
node,
|
|
33631
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33632
|
-
});
|
|
33633
|
-
} })
|
|
34027
|
+
};
|
|
34028
|
+
}
|
|
33634
34029
|
});
|
|
33635
34030
|
//#endregion
|
|
33636
34031
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33641,25 +34036,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33641
34036
|
requires: ["react-native"],
|
|
33642
34037
|
severity: "warn",
|
|
33643
34038
|
recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
|
|
33644
|
-
create: (context) =>
|
|
33645
|
-
|
|
33646
|
-
|
|
33647
|
-
|
|
33648
|
-
|
|
33649
|
-
|
|
33650
|
-
|
|
33651
|
-
|
|
33652
|
-
|
|
33653
|
-
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
34039
|
+
create: (context) => {
|
|
34040
|
+
let fileImportsRecycler = false;
|
|
34041
|
+
return {
|
|
34042
|
+
Program(node) {
|
|
34043
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
34044
|
+
},
|
|
34045
|
+
JSXOpeningElement(node) {
|
|
34046
|
+
if (!fileImportsRecycler) return;
|
|
34047
|
+
const elementName = resolveJsxElementName(node);
|
|
34048
|
+
if (!elementName) return;
|
|
34049
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
34050
|
+
let hasRecycleItemsEnabled = false;
|
|
34051
|
+
let hasGetItemType = false;
|
|
34052
|
+
for (const attr of node.attributes ?? []) {
|
|
34053
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
34054
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
34055
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
34056
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
34057
|
+
else hasRecycleItemsEnabled = true;
|
|
34058
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
34059
|
+
}
|
|
34060
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
34061
|
+
node,
|
|
34062
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
34063
|
+
});
|
|
34064
|
+
}
|
|
34065
|
+
};
|
|
34066
|
+
}
|
|
33663
34067
|
});
|
|
33664
34068
|
//#endregion
|
|
33665
34069
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -33800,7 +34204,7 @@ const rnNoDimensionsGet = defineRule({
|
|
|
33800
34204
|
if (binding !== null && !isBindingReactNativeDimensions(node, binding)) return;
|
|
33801
34205
|
if (isMemberProperty(node.callee, "get")) context.report({
|
|
33802
34206
|
node,
|
|
33803
|
-
message: "
|
|
34207
|
+
message: "Dimensions.get() reads the size once and never updates, so layouts built from it go stale on rotation or resize."
|
|
33804
34208
|
});
|
|
33805
34209
|
if (isMemberProperty(node.callee, "addEventListener")) context.report({
|
|
33806
34210
|
node,
|
|
@@ -34622,7 +35026,10 @@ const isExpoUiComponentElement = (openingElement, contextNode, componentName) =>
|
|
|
34622
35026
|
};
|
|
34623
35027
|
//#endregion
|
|
34624
35028
|
//#region src/plugin/rules/react-native/rn-no-raw-text.ts
|
|
34625
|
-
const truncateText = (text) =>
|
|
35029
|
+
const truncateText = (text) => {
|
|
35030
|
+
const collapsedText = text.replace(/\s+/g, " ");
|
|
35031
|
+
return collapsedText.length > 30 ? `${collapsedText.slice(0, 30)}...` : collapsedText;
|
|
35032
|
+
};
|
|
34626
35033
|
const isRawTextContent = (child) => {
|
|
34627
35034
|
if (isNodeOfType(child, "JSXText")) return Boolean(child.value?.trim());
|
|
34628
35035
|
if (!isNodeOfType(child, "JSXExpressionContainer") || !child.expression) return false;
|
|
@@ -34643,9 +35050,10 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34643
35050
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34644
35051
|
return resolveJsxElementName(openingElement);
|
|
34645
35052
|
};
|
|
35053
|
+
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34646
35054
|
const isTextHandlingComponent = (elementName) => {
|
|
34647
35055
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34648
|
-
return
|
|
35056
|
+
return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
|
|
34649
35057
|
};
|
|
34650
35058
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34651
35059
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34689,6 +35097,7 @@ const rnNoRawText = defineRule({
|
|
|
34689
35097
|
return {
|
|
34690
35098
|
Program(programNode) {
|
|
34691
35099
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
35100
|
+
if (!containsJsxElement(programNode)) return;
|
|
34692
35101
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34693
35102
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34694
35103
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38643,14 +39052,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38643
39052
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38644
39053
|
category: "Accessibility",
|
|
38645
39054
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38646
|
-
|
|
38647
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38648
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38649
|
-
if (!role) return;
|
|
38650
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38651
|
-
const isImplicit = !roleAttribute;
|
|
38652
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38653
|
-
if (!supported) return;
|
|
39055
|
+
let ariaAttributes = null;
|
|
38654
39056
|
for (const attribute of node.attributes) {
|
|
38655
39057
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38656
39058
|
const attributeNode = attribute;
|
|
@@ -38660,6 +39062,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38660
39062
|
const propName = propRawName.toLowerCase();
|
|
38661
39063
|
if (!propName.startsWith("aria-")) continue;
|
|
38662
39064
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
39065
|
+
(ariaAttributes ??= []).push({
|
|
39066
|
+
attribute,
|
|
39067
|
+
propName
|
|
39068
|
+
});
|
|
39069
|
+
}
|
|
39070
|
+
if (!ariaAttributes) return;
|
|
39071
|
+
const elementType = getElementType(node, context.settings);
|
|
39072
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
39073
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
39074
|
+
if (!role) return;
|
|
39075
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
39076
|
+
const isImplicit = !roleAttribute;
|
|
39077
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
39078
|
+
if (!supported) return;
|
|
39079
|
+
for (const { attribute, propName } of ariaAttributes) {
|
|
38663
39080
|
if (supported.has(propName)) continue;
|
|
38664
39081
|
context.report({
|
|
38665
39082
|
node: attribute,
|
|
@@ -38881,7 +39298,7 @@ const isInsideClassComponent = (node) => {
|
|
|
38881
39298
|
const hasShortCircuitAncestor = (descendant, ancestor) => {
|
|
38882
39299
|
let current = descendant.parent;
|
|
38883
39300
|
while (current && current !== ancestor) {
|
|
38884
|
-
if (isNodeOfType(current, "ConditionalExpression")) return true;
|
|
39301
|
+
if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
|
|
38885
39302
|
if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
|
|
38886
39303
|
current = current.parent ?? null;
|
|
38887
39304
|
}
|
|
@@ -39007,6 +39424,7 @@ const rulesOfHooks = defineRule({
|
|
|
39007
39424
|
if (parentInfo.isComponentOrHook) isInsideComponentOrHook = true;
|
|
39008
39425
|
}
|
|
39009
39426
|
if (!isInsideComponentOrHook) {
|
|
39427
|
+
if (!enclosing.hasResolvedName) return;
|
|
39010
39428
|
if (isLocalNonHookFunctionCallee(node, context.scopes, settings)) return;
|
|
39011
39429
|
context.report({
|
|
39012
39430
|
node: node.callee,
|
|
@@ -39497,6 +39915,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39497
39915
|
cachedFunctionNames.add(node.id.name);
|
|
39498
39916
|
},
|
|
39499
39917
|
CallExpression(node) {
|
|
39918
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39500
39919
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39501
39920
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39502
39921
|
const firstArg = node.arguments?.[0];
|
|
@@ -39580,6 +39999,14 @@ const objectExpressionHasCachingConfig = (objectExpression) => (objectExpression
|
|
|
39580
39999
|
const objectExpressionHasSpread = (objectExpression) => (objectExpression.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement"));
|
|
39581
40000
|
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
39582
40001
|
const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
|
|
40002
|
+
const REMIX_IMPORT_SOURCE_PATTERN = /^(?:@remix-run\/|@react-router\/|react-router(?:-dom)?$)/;
|
|
40003
|
+
const programImportsRemixRouter = (programNode) => (programNode.body ?? []).some((statement) => isNodeOfType(statement, "ImportDeclaration") && !isTypeOnlyImport(statement) && typeof statement.source?.value === "string" && REMIX_IMPORT_SOURCE_PATTERN.test(statement.source.value));
|
|
40004
|
+
const isImportMetaUrlAssetArgument = (urlArg) => {
|
|
40005
|
+
if (!isNodeOfType(urlArg, "NewExpression")) return false;
|
|
40006
|
+
if (!isNodeOfType(urlArg.callee, "Identifier") || urlArg.callee.name !== "URL") return false;
|
|
40007
|
+
const baseArg = urlArg.arguments?.[1];
|
|
40008
|
+
return isNodeOfType(baseArg, "MemberExpression") && isNodeOfType(baseArg.object, "MetaProperty") && isNodeOfType(baseArg.property, "Identifier") && baseArg.property.name === "url";
|
|
40009
|
+
};
|
|
39583
40010
|
const serverFetchWithoutRevalidate = defineRule({
|
|
39584
40011
|
id: "server-fetch-without-revalidate",
|
|
39585
40012
|
title: "Fetch without revalidate",
|
|
@@ -39599,7 +40026,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39599
40026
|
isServerSideFile = false;
|
|
39600
40027
|
return;
|
|
39601
40028
|
}
|
|
39602
|
-
isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client");
|
|
40029
|
+
isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client") && !programImportsRemixRouter(node);
|
|
39603
40030
|
},
|
|
39604
40031
|
CallExpression(node) {
|
|
39605
40032
|
if (!isServerSideFile) return;
|
|
@@ -39612,6 +40039,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39612
40039
|
if (objectExpressionHasSpread(optionsArg)) return;
|
|
39613
40040
|
}
|
|
39614
40041
|
const urlArg = node.arguments?.[0];
|
|
40042
|
+
if (isImportMetaUrlAssetArgument(urlArg)) return;
|
|
39615
40043
|
const urlText = isNodeOfType(urlArg, "Literal") && typeof urlArg.value === "string" ? `"${urlArg.value}"` : "url";
|
|
39616
40044
|
context.report({
|
|
39617
40045
|
node,
|
|
@@ -39840,7 +40268,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
39840
40268
|
if (node.kind === "let" || node.kind === "var") {
|
|
39841
40269
|
context.report({
|
|
39842
40270
|
node: declarator,
|
|
39843
|
-
message: `Module-scoped ${node.kind} "${variableName}"
|
|
40271
|
+
message: `Module-scoped ${node.kind} "${variableName}" is shared by every request, so any write to it leaks state between your users.`
|
|
39844
40272
|
});
|
|
39845
40273
|
continue;
|
|
39846
40274
|
}
|
|
@@ -39901,25 +40329,29 @@ const GATE_LEADING_VERBS = new Set([
|
|
|
39901
40329
|
"authorize",
|
|
39902
40330
|
"authenticate"
|
|
39903
40331
|
]);
|
|
39904
|
-
const
|
|
39905
|
-
|
|
39906
|
-
|
|
39907
|
-
|
|
39908
|
-
|
|
39909
|
-
|
|
39910
|
-
|
|
39911
|
-
if (declarator.init && !isNodeOfType(declarator.init, "AwaitExpression")) return true;
|
|
39912
|
-
}
|
|
40332
|
+
const declarationAwaitsExistingPromise = (declaration) => {
|
|
40333
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
40334
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
40335
|
+
const init = declarator.init;
|
|
40336
|
+
if (!isNodeOfType(init, "AwaitExpression")) continue;
|
|
40337
|
+
const argument = init.argument;
|
|
40338
|
+
if (isNodeOfType(argument, "Identifier") || isNodeOfType(argument, "MemberExpression")) return true;
|
|
39913
40339
|
}
|
|
39914
40340
|
return false;
|
|
39915
40341
|
};
|
|
39916
|
-
const
|
|
40342
|
+
const REQUEST_SCOPED_IMPORT_SOURCES = ["next/headers", "next-intl/server"];
|
|
40343
|
+
const isRequestScopedCallee = (callee) => {
|
|
40344
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
40345
|
+
if (REQUEST_SCOPED_IMPORT_SOURCES.some((moduleSource) => getImportedNameFromModule(callee, callee.name, moduleSource) !== null)) return true;
|
|
40346
|
+
return getImportedNameFromModule(callee, callee.name, "next/server") === "connection";
|
|
40347
|
+
};
|
|
40348
|
+
const declarationAwaitsRequestScopedCall = (declaration) => {
|
|
39917
40349
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
39918
40350
|
for (const declarator of declaration.declarations ?? []) {
|
|
39919
40351
|
const init = declarator.init;
|
|
39920
40352
|
if (!isNodeOfType(init, "AwaitExpression")) continue;
|
|
39921
40353
|
const argument = init.argument;
|
|
39922
|
-
if (isNodeOfType(argument, "
|
|
40354
|
+
if (isNodeOfType(argument, "CallExpression") && isRequestScopedCallee(argument.callee)) return true;
|
|
39923
40355
|
}
|
|
39924
40356
|
return false;
|
|
39925
40357
|
};
|
|
@@ -39954,7 +40386,8 @@ const serverSequentialIndependentAwait = defineRule({
|
|
|
39954
40386
|
if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
|
|
39955
40387
|
if (!declarationStartsWithAwait(nextStatement)) continue;
|
|
39956
40388
|
if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
|
|
39957
|
-
if (
|
|
40389
|
+
if (declarationAwaitsExistingPromise(nextStatement)) continue;
|
|
40390
|
+
if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
|
|
39958
40391
|
if (declarationAwaitsGate(currentStatement)) continue;
|
|
39959
40392
|
context.report({
|
|
39960
40393
|
node: nextStatement,
|
|
@@ -40423,7 +40856,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40423
40856
|
//#endregion
|
|
40424
40857
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40425
40858
|
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
40426
|
-
const
|
|
40859
|
+
const ENABLE_RLS_PATTERN = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi;
|
|
40860
|
+
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40861
|
+
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40862
|
+
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40863
|
+
const tableName = match[1];
|
|
40864
|
+
if (tableName === void 0) continue;
|
|
40865
|
+
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40866
|
+
}
|
|
40867
|
+
return lastEnableIndexByTable;
|
|
40868
|
+
};
|
|
40427
40869
|
const supabaseTableMissingRls = defineRule({
|
|
40428
40870
|
id: "supabase-table-missing-rls",
|
|
40429
40871
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40434,11 +40876,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40434
40876
|
const content = sanitizeSqlForScan(file.content);
|
|
40435
40877
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40436
40878
|
const findings = [];
|
|
40879
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40437
40880
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40438
40881
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40439
40882
|
const tableName = match[1];
|
|
40440
40883
|
if (tableName === void 0) continue;
|
|
40441
|
-
|
|
40884
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40885
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40442
40886
|
const location = getLocationAtIndex(content, match.index);
|
|
40443
40887
|
findings.push({
|
|
40444
40888
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40714,6 +41158,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40714
41158
|
severity: "warn",
|
|
40715
41159
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40716
41160
|
create: (context) => {
|
|
41161
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40717
41162
|
let hasHeadContentElement = false;
|
|
40718
41163
|
let hasDocumentHeadElement = false;
|
|
40719
41164
|
let hasCustomHeadChildElement = false;
|
|
@@ -40750,8 +41195,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40750
41195
|
};
|
|
40751
41196
|
return {
|
|
40752
41197
|
Program(node) {
|
|
40753
|
-
const filename = context.filename ?? "";
|
|
40754
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40755
41198
|
const statements = node.body ?? [];
|
|
40756
41199
|
for (const statement of statements) collectImportBindings(statement);
|
|
40757
41200
|
for (const statement of statements) {
|
|
@@ -40760,18 +41203,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40760
41203
|
}
|
|
40761
41204
|
},
|
|
40762
41205
|
ImportDeclaration(node) {
|
|
40763
|
-
const filename = context.filename ?? "";
|
|
40764
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40765
41206
|
collectImportBindings(node);
|
|
40766
41207
|
},
|
|
40767
41208
|
VariableDeclarator(node) {
|
|
40768
|
-
const filename = context.filename ?? "";
|
|
40769
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40770
41209
|
collectVariableAlias(node);
|
|
40771
41210
|
},
|
|
40772
41211
|
JSXOpeningElement(node) {
|
|
40773
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40774
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40775
41212
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40776
41213
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40777
41214
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40785,8 +41222,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40785
41222
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40786
41223
|
},
|
|
40787
41224
|
"Program:exit"(programNode) {
|
|
40788
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40789
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40790
41225
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40791
41226
|
node: programNode,
|
|
40792
41227
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40810,27 +41245,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40810
41245
|
requires: ["tanstack-start"],
|
|
40811
41246
|
severity: "warn",
|
|
40812
41247
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40813
|
-
create: (context) =>
|
|
40814
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40815
|
-
|
|
40816
|
-
|
|
40817
|
-
|
|
40818
|
-
|
|
40819
|
-
|
|
40820
|
-
|
|
40821
|
-
|
|
40822
|
-
|
|
40823
|
-
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
40830
|
-
|
|
40831
|
-
|
|
40832
|
-
|
|
40833
|
-
|
|
41248
|
+
create: (context) => {
|
|
41249
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41250
|
+
return { JSXOpeningElement(node) {
|
|
41251
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
41252
|
+
const attributes = node.attributes ?? [];
|
|
41253
|
+
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
41254
|
+
if (!hrefAttribute?.value) return;
|
|
41255
|
+
let hrefValue = null;
|
|
41256
|
+
if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
|
|
41257
|
+
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
41258
|
+
if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
|
|
41259
|
+
if (hrefValue.startsWith("//")) return;
|
|
41260
|
+
const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
|
|
41261
|
+
if (pathname.startsWith("/api/")) return;
|
|
41262
|
+
if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
|
|
41263
|
+
if (findJsxAttribute(attributes, "download")) return;
|
|
41264
|
+
if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
|
|
41265
|
+
context.report({
|
|
41266
|
+
node,
|
|
41267
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
41268
|
+
});
|
|
41269
|
+
} };
|
|
41270
|
+
}
|
|
40834
41271
|
});
|
|
40835
41272
|
//#endregion
|
|
40836
41273
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40894,7 +41331,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40894
41331
|
severity: "warn",
|
|
40895
41332
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40896
41333
|
create: (context) => {
|
|
40897
|
-
|
|
41334
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40898
41335
|
let deferredCallbackDepth = 0;
|
|
40899
41336
|
let eventHandlerDepth = 0;
|
|
40900
41337
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40903,7 +41340,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40903
41340
|
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
40904
41341
|
if (callParent.callee === functionNode) return false;
|
|
40905
41342
|
if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
|
|
40906
|
-
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
|
|
41343
|
+
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$1.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
|
|
40907
41344
|
};
|
|
40908
41345
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
|
|
40909
41346
|
const isEventHandlerNamedProperty = (node) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
|
|
@@ -40931,11 +41368,11 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40931
41368
|
if (isNodeOfType(parent, "ReturnStatement")) {
|
|
40932
41369
|
const outerFunction = findEnclosingFunction(parent);
|
|
40933
41370
|
const hookName = outerFunction ? getFunctionBindingName(outerFunction) : null;
|
|
40934
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41371
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
40935
41372
|
}
|
|
40936
41373
|
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
|
|
40937
41374
|
const hookName = getFunctionBindingName(parent);
|
|
40938
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41375
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
40939
41376
|
}
|
|
40940
41377
|
return false;
|
|
40941
41378
|
};
|
|
@@ -40958,7 +41395,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40958
41395
|
};
|
|
40959
41396
|
return {
|
|
40960
41397
|
CallExpression(node) {
|
|
40961
|
-
if (!isRouteFile) return;
|
|
40962
41398
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40963
41399
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40964
41400
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40970,39 +41406,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40970
41406
|
}
|
|
40971
41407
|
},
|
|
40972
41408
|
"CallExpression:exit"(node) {
|
|
40973
|
-
if (!isRouteFile) return;
|
|
40974
41409
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40975
41410
|
},
|
|
40976
41411
|
JSXAttribute(node) {
|
|
40977
|
-
if (!isRouteFile) return;
|
|
40978
41412
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40979
41413
|
},
|
|
40980
41414
|
"JSXAttribute:exit"(node) {
|
|
40981
|
-
if (!isRouteFile) return;
|
|
40982
41415
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40983
41416
|
},
|
|
40984
41417
|
Property(node) {
|
|
40985
|
-
if (!isRouteFile) return;
|
|
40986
41418
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40987
41419
|
},
|
|
40988
41420
|
"Property:exit"(node) {
|
|
40989
|
-
if (!isRouteFile) return;
|
|
40990
41421
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40991
41422
|
},
|
|
40992
41423
|
VariableDeclarator(node) {
|
|
40993
|
-
if (!isRouteFile) return;
|
|
40994
41424
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
40995
41425
|
},
|
|
40996
41426
|
"VariableDeclarator:exit"(node) {
|
|
40997
|
-
if (!isRouteFile) return;
|
|
40998
41427
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40999
41428
|
},
|
|
41000
41429
|
FunctionDeclaration(node) {
|
|
41001
|
-
if (!isRouteFile) return;
|
|
41002
41430
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41003
41431
|
},
|
|
41004
41432
|
"FunctionDeclaration:exit"(node) {
|
|
41005
|
-
if (!isRouteFile) return;
|
|
41006
41433
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41007
41434
|
}
|
|
41008
41435
|
};
|
|
@@ -41086,23 +41513,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41086
41513
|
requires: ["tanstack-start"],
|
|
41087
41514
|
severity: "warn",
|
|
41088
41515
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41089
|
-
create: (context) =>
|
|
41090
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41091
|
-
|
|
41092
|
-
|
|
41093
|
-
|
|
41094
|
-
|
|
41095
|
-
|
|
41096
|
-
|
|
41097
|
-
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41516
|
+
create: (context) => {
|
|
41517
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41518
|
+
return { CallExpression(node) {
|
|
41519
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41520
|
+
const callback = node.arguments?.[0];
|
|
41521
|
+
if (!callback) return;
|
|
41522
|
+
let hasFetchCall = false;
|
|
41523
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
41524
|
+
walkAst(callback, (child) => {
|
|
41525
|
+
if (hasFetchCall) return false;
|
|
41526
|
+
if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
41527
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
|
|
41528
|
+
});
|
|
41529
|
+
if (hasFetchCall) context.report({
|
|
41530
|
+
node,
|
|
41531
|
+
message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
|
|
41532
|
+
});
|
|
41533
|
+
} };
|
|
41534
|
+
}
|
|
41106
41535
|
});
|
|
41107
41536
|
//#endregion
|
|
41108
41537
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41143,10 +41572,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41143
41572
|
const propertyName = getPropertyKeyName(property);
|
|
41144
41573
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41145
41574
|
}
|
|
41146
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41575
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
|
|
41147
41576
|
let lastIndex = -1;
|
|
41148
41577
|
for (const propertyName of sensitiveProperties) {
|
|
41149
|
-
const currentIndex =
|
|
41578
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
|
|
41150
41579
|
if (currentIndex < lastIndex) {
|
|
41151
41580
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41152
41581
|
context.report({
|
|
@@ -41183,10 +41612,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41183
41612
|
} else return;
|
|
41184
41613
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41185
41614
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41186
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41615
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
|
|
41187
41616
|
let lastIndex = -1;
|
|
41188
41617
|
for (const methodName of orderSensitiveMethods) {
|
|
41189
|
-
const currentIndex =
|
|
41618
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
|
|
41190
41619
|
if (currentIndex < lastIndex) {
|
|
41191
41620
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41192
41621
|
context.report({
|
|
@@ -41470,13 +41899,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41470
41899
|
//#endregion
|
|
41471
41900
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41472
41901
|
const ZOD_MODULE = "zod";
|
|
41902
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41473
41903
|
const getStaticPropertyName = (member) => {
|
|
41474
41904
|
const property = member.property;
|
|
41475
41905
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41476
41906
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41477
41907
|
return null;
|
|
41478
41908
|
};
|
|
41909
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41479
41910
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41911
|
+
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41912
|
+
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41913
|
+
importInfoCache.set(identifier, importInfo);
|
|
41914
|
+
return importInfo;
|
|
41915
|
+
};
|
|
41916
|
+
const computeImportInfoForIdentifier = (identifier) => {
|
|
41480
41917
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41481
41918
|
if (!specifier) return null;
|
|
41482
41919
|
const declaration = specifier.parent;
|
|
@@ -41613,25 +42050,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41613
42050
|
tags: ["migration-hint"],
|
|
41614
42051
|
severity: "warn",
|
|
41615
42052
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41616
|
-
create: (context) =>
|
|
41617
|
-
|
|
41618
|
-
|
|
41619
|
-
|
|
41620
|
-
|
|
41621
|
-
|
|
41622
|
-
|
|
41623
|
-
|
|
41624
|
-
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
42053
|
+
create: (context) => {
|
|
42054
|
+
let fileImportsZod = false;
|
|
42055
|
+
return {
|
|
42056
|
+
Program(node) {
|
|
42057
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42058
|
+
},
|
|
42059
|
+
CallExpression(node) {
|
|
42060
|
+
if (!fileImportsZod) return;
|
|
42061
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
42062
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
42063
|
+
context.report({
|
|
42064
|
+
node,
|
|
42065
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
42066
|
+
});
|
|
42067
|
+
},
|
|
42068
|
+
MemberExpression(node) {
|
|
42069
|
+
if (!fileImportsZod) return;
|
|
42070
|
+
const parent = node.parent;
|
|
42071
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42072
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
42073
|
+
context.report({
|
|
42074
|
+
node,
|
|
42075
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
42076
|
+
});
|
|
42077
|
+
}
|
|
42078
|
+
};
|
|
42079
|
+
}
|
|
41635
42080
|
});
|
|
41636
42081
|
//#endregion
|
|
41637
42082
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41823,16 +42268,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41823
42268
|
tags: ["migration-hint"],
|
|
41824
42269
|
severity: "warn",
|
|
41825
42270
|
recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
|
|
41826
|
-
create: (context) =>
|
|
41827
|
-
|
|
41828
|
-
|
|
41829
|
-
|
|
41830
|
-
|
|
41831
|
-
|
|
41832
|
-
|
|
41833
|
-
|
|
41834
|
-
|
|
41835
|
-
|
|
42271
|
+
create: (context) => {
|
|
42272
|
+
let fileImportsZod = false;
|
|
42273
|
+
return {
|
|
42274
|
+
Program(node) {
|
|
42275
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42276
|
+
},
|
|
42277
|
+
CallExpression(node) {
|
|
42278
|
+
if (!fileImportsZod) return;
|
|
42279
|
+
if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
|
|
42280
|
+
},
|
|
42281
|
+
MemberExpression(node) {
|
|
42282
|
+
if (!fileImportsZod) return;
|
|
42283
|
+
const parent = node.parent;
|
|
42284
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42285
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
42286
|
+
}
|
|
42287
|
+
};
|
|
42288
|
+
}
|
|
41836
42289
|
});
|
|
41837
42290
|
//#endregion
|
|
41838
42291
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41867,13 +42320,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41867
42320
|
tags: ["migration-hint"],
|
|
41868
42321
|
severity: "warn",
|
|
41869
42322
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41870
|
-
create: (context) =>
|
|
41871
|
-
|
|
41872
|
-
|
|
41873
|
-
node
|
|
41874
|
-
|
|
41875
|
-
|
|
41876
|
-
|
|
42323
|
+
create: (context) => {
|
|
42324
|
+
let fileImportsZod = false;
|
|
42325
|
+
return {
|
|
42326
|
+
Program(node) {
|
|
42327
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42328
|
+
},
|
|
42329
|
+
CallExpression(node) {
|
|
42330
|
+
if (!fileImportsZod) return;
|
|
42331
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42332
|
+
context.report({
|
|
42333
|
+
node,
|
|
42334
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42335
|
+
});
|
|
42336
|
+
}
|
|
42337
|
+
};
|
|
42338
|
+
}
|
|
41877
42339
|
});
|
|
41878
42340
|
//#endregion
|
|
41879
42341
|
//#region src/plugin/rule-registry.ts
|
|
@@ -46841,23 +47303,29 @@ const FALLBACK_CFG = {
|
|
|
46841
47303
|
enclosingFunction: () => null,
|
|
46842
47304
|
isUnconditionalFromEntry: () => false
|
|
46843
47305
|
};
|
|
47306
|
+
const scopesByProgram = /* @__PURE__ */ new WeakMap();
|
|
47307
|
+
const cfgByProgram = /* @__PURE__ */ new WeakMap();
|
|
46844
47308
|
const wrapWithSemanticContext = (rule) => ({
|
|
46845
47309
|
...rule,
|
|
46846
47310
|
create: (baseContext) => {
|
|
46847
47311
|
let programRoot = null;
|
|
46848
|
-
let cachedScopes = null;
|
|
46849
|
-
let cachedCfg = null;
|
|
46850
47312
|
const getScopes = () => {
|
|
46851
|
-
if (cachedScopes) return cachedScopes;
|
|
46852
47313
|
if (!programRoot) return buildFallbackScopes();
|
|
46853
|
-
|
|
46854
|
-
|
|
47314
|
+
let scopes = scopesByProgram.get(programRoot);
|
|
47315
|
+
if (!scopes) {
|
|
47316
|
+
scopes = analyzeScopes(programRoot);
|
|
47317
|
+
scopesByProgram.set(programRoot, scopes);
|
|
47318
|
+
}
|
|
47319
|
+
return scopes;
|
|
46855
47320
|
};
|
|
46856
47321
|
const getCfg = () => {
|
|
46857
|
-
if (cachedCfg) return cachedCfg;
|
|
46858
47322
|
if (!programRoot) return FALLBACK_CFG;
|
|
46859
|
-
|
|
46860
|
-
|
|
47323
|
+
let cfg = cfgByProgram.get(programRoot);
|
|
47324
|
+
if (!cfg) {
|
|
47325
|
+
cfg = analyzeControlFlow(programRoot);
|
|
47326
|
+
cfgByProgram.set(programRoot, cfg);
|
|
47327
|
+
}
|
|
47328
|
+
return cfg;
|
|
46861
47329
|
};
|
|
46862
47330
|
const enrichedContext = {
|
|
46863
47331
|
report: baseContext.report,
|
|
@@ -46872,23 +47340,18 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
46872
47340
|
return getCfg();
|
|
46873
47341
|
}
|
|
46874
47342
|
};
|
|
46875
|
-
const captureRootIfNeeded = (node) => {
|
|
46876
|
-
if (programRoot) return;
|
|
46877
|
-
programRoot = findProgramRoot(node);
|
|
46878
|
-
};
|
|
46879
47343
|
const visitors = rule.create(enrichedContext);
|
|
46880
|
-
const
|
|
47344
|
+
const passthroughVisitors = {};
|
|
46881
47345
|
for (const [nodeType, handler] of Object.entries(visitors)) {
|
|
46882
47346
|
if (typeof handler !== "function") continue;
|
|
46883
|
-
|
|
46884
|
-
captureRootIfNeeded(node);
|
|
46885
|
-
handler(node);
|
|
46886
|
-
});
|
|
47347
|
+
passthroughVisitors[nodeType] = handler;
|
|
46887
47348
|
}
|
|
46888
|
-
|
|
46889
|
-
|
|
46890
|
-
|
|
46891
|
-
|
|
47349
|
+
const innerProgramHandler = passthroughVisitors.Program;
|
|
47350
|
+
passthroughVisitors.Program = ((node) => {
|
|
47351
|
+
programRoot = node;
|
|
47352
|
+
if (innerProgramHandler) innerProgramHandler(node);
|
|
47353
|
+
});
|
|
47354
|
+
return passthroughVisitors;
|
|
46892
47355
|
}
|
|
46893
47356
|
});
|
|
46894
47357
|
//#endregion
|