oxlint-plugin-react-doctor 0.6.2-dev.072d37e → 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 +1067 -627
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -294,15 +294,38 @@ const defineRule = (rule) => {
|
|
|
294
294
|
};
|
|
295
295
|
//#endregion
|
|
296
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
|
+
};
|
|
297
312
|
const getLocationAtIndex = (content, matchIndex) => {
|
|
298
|
-
if (matchIndex < 0) return {
|
|
313
|
+
if (Number.isNaN(matchIndex) || matchIndex < 0) return {
|
|
299
314
|
line: 1,
|
|
300
315
|
column: 1
|
|
301
316
|
};
|
|
302
|
-
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
|
+
}
|
|
303
326
|
return {
|
|
304
|
-
line:
|
|
305
|
-
column:
|
|
327
|
+
line: lowLineIndex + 1,
|
|
328
|
+
column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
|
|
306
329
|
};
|
|
307
330
|
};
|
|
308
331
|
//#endregion
|
|
@@ -355,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
|
355
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);
|
|
356
379
|
//#endregion
|
|
357
380
|
//#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
|
|
381
|
+
const classificationCacheByPattern = /* @__PURE__ */ new Map();
|
|
358
382
|
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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;
|
|
365
393
|
};
|
|
366
394
|
//#endregion
|
|
367
395
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
@@ -560,7 +588,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
|
|
|
560
588
|
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
561
589
|
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
562
590
|
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
563
|
-
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
591
|
+
const HOOK_NAME_PATTERN$1 = /^use[A-Z]/;
|
|
564
592
|
const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
|
|
565
593
|
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
566
594
|
const HOOKS_WITH_DEPS = new Set([
|
|
@@ -760,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
|
|
|
760
788
|
into.add(name);
|
|
761
789
|
});
|
|
762
790
|
};
|
|
763
|
-
const findSameFileComponentBody = (programRoot, componentName) => {
|
|
764
|
-
let foundBody = null;
|
|
765
|
-
walkAst(programRoot, (node) => {
|
|
766
|
-
if (foundBody) return false;
|
|
767
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
|
|
768
|
-
foundBody = node.body;
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
|
|
772
|
-
const initializer = node.init;
|
|
773
|
-
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
|
|
774
|
-
foundBody = initializer.body;
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
return foundBody;
|
|
780
|
-
};
|
|
781
791
|
const countEffectHookCalls = (body) => {
|
|
782
792
|
if (!body) return 0;
|
|
783
793
|
let count = 0;
|
|
@@ -787,6 +797,36 @@ const countEffectHookCalls = (body) => {
|
|
|
787
797
|
});
|
|
788
798
|
return count;
|
|
789
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
|
+
};
|
|
790
830
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
791
831
|
id: "activity-wraps-effect-heavy-subtree",
|
|
792
832
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -843,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
843
883
|
let totalEffects = 0;
|
|
844
884
|
const effectfulChildren = [];
|
|
845
885
|
for (const componentName of childComponentNames) {
|
|
846
|
-
const
|
|
847
|
-
if (!body) continue;
|
|
848
|
-
const effectCount = countEffectHookCalls(body);
|
|
886
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
849
887
|
if (effectCount === 0) continue;
|
|
850
888
|
totalEffects += effectCount;
|
|
851
889
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -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,6 +1461,23 @@ 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) => {
|
|
@@ -1623,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1623
1682
|
};
|
|
1624
1683
|
//#endregion
|
|
1625
1684
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1626
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1685
|
+
const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
|
|
1627
1686
|
//#endregion
|
|
1628
1687
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1629
1688
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -1972,7 +2031,12 @@ const altText = defineRule({
|
|
|
1972
2031
|
const objectAliases = new Set(settings.object ?? []);
|
|
1973
2032
|
const areaAliases = new Set(settings.area ?? []);
|
|
1974
2033
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2034
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
1975
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
|
+
}
|
|
1976
2040
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1977
2041
|
const tag = getElementType(node, context.settings);
|
|
1978
2042
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2155,7 +2219,9 @@ const anchorIsValid = defineRule({
|
|
|
2155
2219
|
category: "Accessibility",
|
|
2156
2220
|
create: (context) => {
|
|
2157
2221
|
const settings = resolveSettings$50(context.settings);
|
|
2222
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2158
2223
|
return { JSXOpeningElement(node) {
|
|
2224
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2159
2225
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2160
2226
|
let hrefAttribute;
|
|
2161
2227
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3581,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3581
3647
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3582
3648
|
//#endregion
|
|
3583
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");
|
|
3584
3651
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3585
|
-
for (const match of content.matchAll(
|
|
3652
|
+
for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
|
|
3586
3653
|
const value = match[0] ?? "";
|
|
3587
3654
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3588
3655
|
}
|
|
@@ -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
|
}
|
|
@@ -7288,7 +7367,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
7288
7367
|
"ParenthesizedExpression",
|
|
7289
7368
|
"ChainExpression"
|
|
7290
7369
|
]);
|
|
7291
|
-
const unwrapExpression$
|
|
7370
|
+
const unwrapExpression$3 = (node) => {
|
|
7292
7371
|
let current = node;
|
|
7293
7372
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
7294
7373
|
const inner = current.expression;
|
|
@@ -7414,7 +7493,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7414
7493
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
7415
7494
|
const initializerRaw = declarator.init;
|
|
7416
7495
|
if (!initializerRaw) return false;
|
|
7417
|
-
const initializer = unwrapExpression$
|
|
7496
|
+
const initializer = unwrapExpression$3(initializerRaw);
|
|
7418
7497
|
if (symbol.kind === "const") {
|
|
7419
7498
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
7420
7499
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -7434,13 +7513,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7434
7513
|
return false;
|
|
7435
7514
|
};
|
|
7436
7515
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
7437
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7516
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7438
7517
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
7439
7518
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
7440
7519
|
};
|
|
7441
7520
|
const getFunctionValueNode = (symbol) => {
|
|
7442
7521
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
7443
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7522
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7444
7523
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
7445
7524
|
return null;
|
|
7446
7525
|
};
|
|
@@ -7536,6 +7615,14 @@ const flattenReferenceRootName = (reference) => {
|
|
|
7536
7615
|
if (isNodeOfType(referencedIdentifier, "JSXIdentifier")) return referencedIdentifier.name;
|
|
7537
7616
|
return "";
|
|
7538
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
|
+
};
|
|
7539
7626
|
const computeDepKey = (reference) => {
|
|
7540
7627
|
const referencedIdentifier = reference.identifier;
|
|
7541
7628
|
let parent = referencedIdentifier.parent ?? null;
|
|
@@ -7566,21 +7653,22 @@ const computeDepKey = (reference) => {
|
|
|
7566
7653
|
const declarator = outermost.parent;
|
|
7567
7654
|
if (declarator && isNodeOfType(declarator, "VariableDeclarator") && declarator.init === outermost) {
|
|
7568
7655
|
const destructuredPath = getDestructuredPropertyPath(declarator.id);
|
|
7569
|
-
if (destructuredPath) return `${fullName}.${destructuredPath}
|
|
7656
|
+
if (destructuredPath) return truncateAtRefCurrent(`${fullName}.${destructuredPath}`);
|
|
7570
7657
|
}
|
|
7658
|
+
const truncatedName = truncateAtRefCurrent(fullName);
|
|
7659
|
+
if (truncatedName !== fullName) return truncatedName;
|
|
7571
7660
|
if (reference.flag !== "read") {
|
|
7572
7661
|
const lastDotIndex = fullName.lastIndexOf(".");
|
|
7573
7662
|
if (lastDotIndex !== -1) return fullName.slice(0, lastDotIndex);
|
|
7574
7663
|
}
|
|
7575
|
-
if (fullName.endsWith(".current")) return fullName.slice(0, -8);
|
|
7576
7664
|
return fullName;
|
|
7577
7665
|
};
|
|
7578
7666
|
const computeDeclaredDepKey = (entry) => {
|
|
7579
|
-
const stripped = unwrapExpression$
|
|
7667
|
+
const stripped = unwrapExpression$3(entry);
|
|
7580
7668
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7581
7669
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
7582
7670
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) {
|
|
7583
|
-
const callee = unwrapExpression$
|
|
7671
|
+
const callee = unwrapExpression$3(stripped.callee);
|
|
7584
7672
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7585
7673
|
if (isNodeOfType(callee, "MemberExpression") && !hasComputedMemberExpression(callee)) return stringifyMemberChain(callee);
|
|
7586
7674
|
}
|
|
@@ -7588,16 +7676,16 @@ const computeDeclaredDepKey = (entry) => {
|
|
|
7588
7676
|
};
|
|
7589
7677
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
7590
7678
|
if (!depsArgument) return false;
|
|
7591
|
-
const strippedDepsArgument = unwrapExpression$
|
|
7679
|
+
const strippedDepsArgument = unwrapExpression$3(depsArgument);
|
|
7592
7680
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
7593
7681
|
return strippedDepsArgument.elements.some((element) => {
|
|
7594
7682
|
if (!element) return false;
|
|
7595
|
-
const strippedElement = unwrapExpression$
|
|
7683
|
+
const strippedElement = unwrapExpression$3(element);
|
|
7596
7684
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
7597
7685
|
});
|
|
7598
7686
|
};
|
|
7599
7687
|
const stringifyMemberChain = (node) => {
|
|
7600
|
-
const stripped = unwrapExpression$
|
|
7688
|
+
const stripped = unwrapExpression$3(node);
|
|
7601
7689
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7602
7690
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
7603
7691
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -7639,13 +7727,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
7639
7727
|
return false;
|
|
7640
7728
|
};
|
|
7641
7729
|
const getMemberRootIdentifier = (node) => {
|
|
7642
|
-
const stripped = unwrapExpression$
|
|
7730
|
+
const stripped = unwrapExpression$3(node);
|
|
7643
7731
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
7644
7732
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
7645
7733
|
return null;
|
|
7646
7734
|
};
|
|
7647
7735
|
const hasComputedMemberExpression = (node) => {
|
|
7648
|
-
const stripped = unwrapExpression$
|
|
7736
|
+
const stripped = unwrapExpression$3(node);
|
|
7649
7737
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
7650
7738
|
if (stripped.computed) return true;
|
|
7651
7739
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -7661,7 +7749,7 @@ const getRootSymbol = (node, scopes) => {
|
|
|
7661
7749
|
return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7662
7750
|
};
|
|
7663
7751
|
const getDeclaredDepSymbolSource = (node) => {
|
|
7664
|
-
const stripped = unwrapExpression$
|
|
7752
|
+
const stripped = unwrapExpression$3(node);
|
|
7665
7753
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) return stripped.callee;
|
|
7666
7754
|
return node;
|
|
7667
7755
|
};
|
|
@@ -7671,7 +7759,7 @@ const isRegExpLiteral = (node) => {
|
|
|
7671
7759
|
};
|
|
7672
7760
|
const isUnstableInitializer = (node) => {
|
|
7673
7761
|
if (!node) return false;
|
|
7674
|
-
const stripped = unwrapExpression$
|
|
7762
|
+
const stripped = unwrapExpression$3(node);
|
|
7675
7763
|
if (isRegExpLiteral(stripped)) return true;
|
|
7676
7764
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
7677
7765
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -7763,9 +7851,9 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7763
7851
|
};
|
|
7764
7852
|
findReturn(callback);
|
|
7765
7853
|
if (!cleanupFunction) return null;
|
|
7766
|
-
let
|
|
7854
|
+
let found = null;
|
|
7767
7855
|
const visitCleanup = (node) => {
|
|
7768
|
-
if (
|
|
7856
|
+
if (found) return;
|
|
7769
7857
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
7770
7858
|
const candidateName = getRefCurrentNameFromMemberExpression(node);
|
|
7771
7859
|
if (candidateName) {
|
|
@@ -7773,7 +7861,10 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7773
7861
|
const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7774
7862
|
const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
|
|
7775
7863
|
if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
|
|
7776
|
-
|
|
7864
|
+
found = {
|
|
7865
|
+
refCurrentName: candidateName,
|
|
7866
|
+
refSymbol: symbol
|
|
7867
|
+
};
|
|
7777
7868
|
return;
|
|
7778
7869
|
}
|
|
7779
7870
|
}
|
|
@@ -7788,7 +7879,19 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7788
7879
|
}
|
|
7789
7880
|
};
|
|
7790
7881
|
visitCleanup(cleanupFunction);
|
|
7791
|
-
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;
|
|
7792
7895
|
};
|
|
7793
7896
|
const hasRefCurrentAssignment = (callback, refCurrentName) => {
|
|
7794
7897
|
let didAssignRefCurrent = false;
|
|
@@ -7827,9 +7930,21 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
7827
7930
|
const visit = (current) => {
|
|
7828
7931
|
if (didFindMemberCall) return;
|
|
7829
7932
|
if (isNodeOfType(current, "CallExpression")) {
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
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
|
+
}
|
|
7833
7948
|
}
|
|
7834
7949
|
}
|
|
7835
7950
|
const record = current;
|
|
@@ -7890,7 +8005,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7890
8005
|
return;
|
|
7891
8006
|
}
|
|
7892
8007
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
7893
|
-
const callbackExpression = unwrapExpression$
|
|
8008
|
+
const callbackExpression = unwrapExpression$3(callbackArgument);
|
|
7894
8009
|
let callbackToAnalyze = null;
|
|
7895
8010
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
7896
8011
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -7898,7 +8013,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7898
8013
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
7899
8014
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
7900
8015
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
7901
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$
|
|
8016
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$3(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
7902
8017
|
else if (depsArgumentRaw) {
|
|
7903
8018
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
7904
8019
|
context.report({
|
|
@@ -7931,11 +8046,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7931
8046
|
message: buildAssignmentMessage(assignment.name)
|
|
7932
8047
|
});
|
|
7933
8048
|
if (outerAssignments.length > 0) return;
|
|
7934
|
-
const
|
|
8049
|
+
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
7935
8050
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
7936
|
-
if (
|
|
8051
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol)) context.report({
|
|
7937
8052
|
node: callbackToAnalyze,
|
|
7938
|
-
message: buildRefCleanupMessage(refCurrentName)
|
|
8053
|
+
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
7939
8054
|
});
|
|
7940
8055
|
}
|
|
7941
8056
|
if (!depsArgumentRaw) {
|
|
@@ -7955,8 +8070,16 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7955
8070
|
});
|
|
7956
8071
|
return;
|
|
7957
8072
|
}
|
|
7958
|
-
const depsArgument = unwrapExpression$
|
|
7959
|
-
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) {
|
|
7960
8083
|
if (isAutoDependenciesHook(hookName)) return;
|
|
7961
8084
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
7962
8085
|
context.report({
|
|
@@ -7965,19 +8088,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7965
8088
|
});
|
|
7966
8089
|
return;
|
|
7967
8090
|
}
|
|
7968
|
-
const nonArrayCaptureKeys = callbackToAnalyze !== null ? new Set(collectCaptureDepKeys(callbackToAnalyze, context.scopes).keys) : /* @__PURE__ */ new Set();
|
|
7969
|
-
for (const forcedCaptureKey of forcedCaptureKeys) nonArrayCaptureKeys.add(forcedCaptureKey);
|
|
7970
|
-
if (nonArrayCaptureKeys.size > 0) {
|
|
7971
|
-
context.report({
|
|
7972
|
-
node: depsArgument,
|
|
7973
|
-
message: buildNonArrayDepsMessage(hookName)
|
|
7974
|
-
});
|
|
7975
|
-
context.report({
|
|
7976
|
-
node: depsArgument,
|
|
7977
|
-
message: buildMissingDepMessage(hookName, [...nonArrayCaptureKeys].join(", "))
|
|
7978
|
-
});
|
|
7979
|
-
}
|
|
7980
|
-
return;
|
|
7981
8091
|
}
|
|
7982
8092
|
if (!isNodeOfType(depsArgument, "ArrayExpression")) {
|
|
7983
8093
|
context.report({
|
|
@@ -7996,11 +8106,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7996
8106
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
7997
8107
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
7998
8108
|
if (!element) return false;
|
|
7999
|
-
return isLiteralOrEmptyTemplate(unwrapExpression$
|
|
8109
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
|
|
8000
8110
|
});
|
|
8001
8111
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
8002
8112
|
if (!element) return false;
|
|
8003
|
-
return isNonStringLiteral(unwrapExpression$
|
|
8113
|
+
return isNonStringLiteral(unwrapExpression$3(element));
|
|
8004
8114
|
});
|
|
8005
8115
|
if (hasNonStringLiteralDep) context.report({
|
|
8006
8116
|
node: depsArgument,
|
|
@@ -8021,7 +8131,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8021
8131
|
});
|
|
8022
8132
|
continue;
|
|
8023
8133
|
}
|
|
8024
|
-
const stripped = unwrapExpression$
|
|
8134
|
+
const stripped = unwrapExpression$3(elementNode);
|
|
8025
8135
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
8026
8136
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
8027
8137
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -8163,7 +8273,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8163
8273
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
8164
8274
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
8165
8275
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
8166
|
-
if (isNodeOfType(unwrapExpression$
|
|
8276
|
+
if (isNodeOfType(unwrapExpression$3(reportNode), "CallExpression")) {
|
|
8167
8277
|
context.report({
|
|
8168
8278
|
node: reportNode,
|
|
8169
8279
|
message: buildComplexDepMessage(hookName)
|
|
@@ -8279,9 +8389,14 @@ const firebaseQueryFilterAsAuth = defineRule({
|
|
|
8279
8389
|
* `*Handler`), so the cheap escape-then-replace shape is enough
|
|
8280
8390
|
* and avoids pulling picomatch into the per-file rule path.
|
|
8281
8391
|
*/
|
|
8392
|
+
const compiledGlobs = /* @__PURE__ */ new Map();
|
|
8282
8393
|
const compileGlob = (pattern) => {
|
|
8394
|
+
const cached = compiledGlobs.get(pattern);
|
|
8395
|
+
if (cached) return cached;
|
|
8283
8396
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
8284
|
-
|
|
8397
|
+
const compiled = new RegExp(`^${escaped}$`);
|
|
8398
|
+
compiledGlobs.set(pattern, compiled);
|
|
8399
|
+
return compiled;
|
|
8285
8400
|
};
|
|
8286
8401
|
//#endregion
|
|
8287
8402
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
@@ -8571,7 +8686,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
8571
8686
|
const resolveSettings$40 = (settings) => {
|
|
8572
8687
|
const reactDoctor = settings?.["react-doctor"];
|
|
8573
8688
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
8574
|
-
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
8689
|
+
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
8575
8690
|
};
|
|
8576
8691
|
const headingHasContent = defineRule({
|
|
8577
8692
|
id: "heading-has-content",
|
|
@@ -8584,7 +8699,7 @@ const headingHasContent = defineRule({
|
|
|
8584
8699
|
const settings = resolveSettings$40(context.settings);
|
|
8585
8700
|
return { JSXOpeningElement(node) {
|
|
8586
8701
|
const elementType = getElementType(node, context.settings);
|
|
8587
|
-
if (!settings.headingTags.
|
|
8702
|
+
if (!settings.headingTags.has(elementType)) return;
|
|
8588
8703
|
const parent = node.parent;
|
|
8589
8704
|
if (parent && isNodeOfType(parent, "JSXElement")) {
|
|
8590
8705
|
if (objectHasAccessibleChild(parent, context.settings)) return;
|
|
@@ -9182,9 +9297,9 @@ const resolveSettings$37 = (settings) => {
|
|
|
9182
9297
|
};
|
|
9183
9298
|
};
|
|
9184
9299
|
const isWordBoundary = (text, start, end) => {
|
|
9185
|
-
const
|
|
9186
|
-
const startsBoundary = start === 0 || !
|
|
9187
|
-
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));
|
|
9188
9303
|
return startsBoundary && endsBoundary;
|
|
9189
9304
|
};
|
|
9190
9305
|
const containsRedundantWord = (altText, words) => {
|
|
@@ -9223,10 +9338,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9223
9338
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9224
9339
|
const settings = resolveSettings$37(context.settings);
|
|
9225
9340
|
return { JSXOpeningElement(node) {
|
|
9226
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9227
9341
|
const tag = getElementType(node, context.settings);
|
|
9228
9342
|
if (!settings.components.includes(tag)) return;
|
|
9229
9343
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9344
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9230
9345
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9231
9346
|
if (!altAttribute) return;
|
|
9232
9347
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9281,6 +9396,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9281
9396
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9282
9397
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9283
9398
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9399
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9284
9400
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9285
9401
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9286
9402
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9310,6 +9426,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9310
9426
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9311
9427
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9312
9428
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9429
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9313
9430
|
const content = getScannableContent(file);
|
|
9314
9431
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9315
9432
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9488,6 +9605,7 @@ const KEYBOARD_EVENT_HANDLERS = [
|
|
|
9488
9605
|
"onKeyUp"
|
|
9489
9606
|
];
|
|
9490
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()));
|
|
9491
9609
|
//#endregion
|
|
9492
9610
|
//#region src/plugin/utils/is-disabled-element.ts
|
|
9493
9611
|
const isDisabledElement = (openingElement) => {
|
|
@@ -9542,15 +9660,25 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9542
9660
|
const settings = resolveSettings$36(context.settings);
|
|
9543
9661
|
const tabbableSet = new Set(settings.tabbable);
|
|
9544
9662
|
return { JSXOpeningElement(node) {
|
|
9663
|
+
if (node.attributes.length === 0) return;
|
|
9545
9664
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9546
9665
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9547
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;
|
|
9548
9678
|
const elementType = getElementType(node, context.settings);
|
|
9549
9679
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9550
|
-
|
|
9680
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9551
9681
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9552
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9553
|
-
if (!role) return;
|
|
9554
9682
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9555
9683
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9556
9684
|
context.report({
|
|
@@ -9568,7 +9696,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
9568
9696
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
9569
9697
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
9570
9698
|
};
|
|
9571
|
-
const isFunctionExpressionLike$
|
|
9699
|
+
const isFunctionExpressionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
9572
9700
|
const getFirstParameterName = (fn) => {
|
|
9573
9701
|
const parameters = fn.params ?? [];
|
|
9574
9702
|
if (parameters.length !== 1) return null;
|
|
@@ -9701,7 +9829,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
9701
9829
|
const args = node.arguments ?? [];
|
|
9702
9830
|
if (args.length === 0) return;
|
|
9703
9831
|
const reader = args[0];
|
|
9704
|
-
if (!isFunctionExpressionLike$
|
|
9832
|
+
if (!isFunctionExpressionLike$2(reader)) return;
|
|
9705
9833
|
const getParameterName = getFirstParameterName(reader);
|
|
9706
9834
|
if (!getParameterName) return;
|
|
9707
9835
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -9799,14 +9927,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
9799
9927
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
9800
9928
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
9801
9929
|
const declaredName = functionNode.id.name;
|
|
9802
|
-
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
|
|
9930
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$1.test(declaredName);
|
|
9803
9931
|
}
|
|
9804
9932
|
let cursor = functionNode.parent ?? null;
|
|
9805
9933
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
9806
9934
|
if (!cursor) return false;
|
|
9807
9935
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
9808
9936
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
9809
|
-
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);
|
|
9810
9938
|
};
|
|
9811
9939
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
9812
9940
|
let didFindRenderPathInvocation = false;
|
|
@@ -10189,8 +10317,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10189
10317
|
walkAst(loopBody, (child) => {
|
|
10190
10318
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10191
10319
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10192
|
-
});
|
|
10193
|
-
walkAst(loopBody, (child) => {
|
|
10194
10320
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10195
10321
|
if (child.computed) return;
|
|
10196
10322
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10650,7 +10776,6 @@ const jsHoistIntl = defineRule({
|
|
|
10650
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",
|
|
10651
10777
|
create: (context) => ({ NewExpression(node) {
|
|
10652
10778
|
if (!isIntlNewExpression(node)) return;
|
|
10653
|
-
if (isInsideCacheMemo(node)) return;
|
|
10654
10779
|
let cursor = node.parent ?? null;
|
|
10655
10780
|
let inFunctionBody = false;
|
|
10656
10781
|
while (cursor) {
|
|
@@ -10667,6 +10792,7 @@ const jsHoistIntl = defineRule({
|
|
|
10667
10792
|
cursor = cursor.parent ?? null;
|
|
10668
10793
|
}
|
|
10669
10794
|
if (!inFunctionBody) return;
|
|
10795
|
+
if (isInsideCacheMemo(node)) return;
|
|
10670
10796
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10671
10797
|
context.report({
|
|
10672
10798
|
node,
|
|
@@ -10741,7 +10867,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10741
10867
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10742
10868
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10743
10869
|
};
|
|
10744
|
-
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();
|
|
10745
10875
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10746
10876
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10747
10877
|
});
|
|
@@ -10758,6 +10888,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10758
10888
|
if (targetRoot) names.add(targetRoot);
|
|
10759
10889
|
}
|
|
10760
10890
|
});
|
|
10891
|
+
loopBoundNamesCache.set(loop, names);
|
|
10892
|
+
return names;
|
|
10761
10893
|
};
|
|
10762
10894
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10763
10895
|
let cursor = receiver;
|
|
@@ -10783,7 +10915,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10783
10915
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10784
10916
|
let ancestor = node.parent;
|
|
10785
10917
|
while (ancestor) {
|
|
10786
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10918
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10787
10919
|
ancestor = ancestor.parent;
|
|
10788
10920
|
}
|
|
10789
10921
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -14391,14 +14523,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14391
14523
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14392
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`.";
|
|
14393
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.";
|
|
14394
|
-
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14526
|
+
const DEFAULT_CONTROL_COMPONENTS = new Set([
|
|
14395
14527
|
"input",
|
|
14396
14528
|
"meter",
|
|
14397
14529
|
"output",
|
|
14398
14530
|
"progress",
|
|
14399
14531
|
"select",
|
|
14400
14532
|
"textarea"
|
|
14401
|
-
];
|
|
14533
|
+
]);
|
|
14402
14534
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14403
14535
|
"alt",
|
|
14404
14536
|
"aria-label",
|
|
@@ -14410,8 +14542,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14410
14542
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14411
14543
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14412
14544
|
return {
|
|
14413
|
-
labelComponents: ["label", ...ruleSettings.labelComponents ?? []]
|
|
14414
|
-
labelAttributes:
|
|
14545
|
+
labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
|
|
14546
|
+
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14415
14547
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14416
14548
|
assert: ruleSettings.assert ?? "either",
|
|
14417
14549
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14419,7 +14551,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14419
14551
|
};
|
|
14420
14552
|
};
|
|
14421
14553
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14422
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14554
|
+
if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
|
|
14423
14555
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14424
14556
|
};
|
|
14425
14557
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14445,7 +14577,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14445
14577
|
const attributeName = attribute.name;
|
|
14446
14578
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14447
14579
|
const propName = getJsxAttributeName(attributeName);
|
|
14448
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14580
|
+
if (!propName || !searchContext.labelAttributes.has(propName)) continue;
|
|
14449
14581
|
const attributeValue = attribute.value;
|
|
14450
14582
|
if (!attributeValue) continue;
|
|
14451
14583
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14467,7 +14599,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14467
14599
|
const attributeName = attribute.name;
|
|
14468
14600
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14469
14601
|
const propName = getJsxAttributeName(attributeName);
|
|
14470
|
-
if (propName && searchContext.labelAttributes.
|
|
14602
|
+
if (propName && searchContext.labelAttributes.has(propName)) return true;
|
|
14471
14603
|
}
|
|
14472
14604
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14473
14605
|
return false;
|
|
@@ -14490,7 +14622,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14490
14622
|
if (isTestlikeFile) return;
|
|
14491
14623
|
const opening = node.openingElement;
|
|
14492
14624
|
const tagName = getElementType(opening, context.settings);
|
|
14493
|
-
if (!settings.labelComponents.
|
|
14625
|
+
if (!settings.labelComponents.has(tagName)) return;
|
|
14494
14626
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14495
14627
|
const searchContext = {
|
|
14496
14628
|
depth: settings.depth,
|
|
@@ -15079,9 +15211,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15079
15211
|
const reactDoctor = settings?.["react-doctor"];
|
|
15080
15212
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15081
15213
|
return {
|
|
15082
|
-
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15083
|
-
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15084
|
-
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 ?? []])
|
|
15085
15217
|
};
|
|
15086
15218
|
};
|
|
15087
15219
|
const evaluateMuted = (attribute) => {
|
|
@@ -15110,7 +15242,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15110
15242
|
let rendersCaptionTrack = false;
|
|
15111
15243
|
walkAst(expression, (inner) => {
|
|
15112
15244
|
if (rendersCaptionTrack) return false;
|
|
15113
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15245
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15114
15246
|
rendersCaptionTrack = true;
|
|
15115
15247
|
return false;
|
|
15116
15248
|
}
|
|
@@ -15128,7 +15260,7 @@ const mediaHasCaption = defineRule({
|
|
|
15128
15260
|
const settings = resolveSettings$23(context.settings);
|
|
15129
15261
|
return { JSXOpeningElement(node) {
|
|
15130
15262
|
const tag = getElementType(node, context.settings);
|
|
15131
|
-
if (!(settings.audio.
|
|
15263
|
+
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
15132
15264
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15133
15265
|
const parent = node.parent;
|
|
15134
15266
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15143,7 +15275,7 @@ const mediaHasCaption = defineRule({
|
|
|
15143
15275
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15144
15276
|
const opening = child.openingElement;
|
|
15145
15277
|
const childTag = getElementType(opening, context.settings);
|
|
15146
|
-
if (!settings.track.
|
|
15278
|
+
if (!settings.track.has(childTag)) return false;
|
|
15147
15279
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15148
15280
|
if (!kindAttribute) return false;
|
|
15149
15281
|
let kindValue = kindAttribute.value;
|
|
@@ -15454,16 +15586,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15454
15586
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15455
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;
|
|
15456
15588
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15457
|
-
const
|
|
15589
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15458
15590
|
const strippedSource = stripJsComments(sourceText);
|
|
15459
|
-
|
|
15460
|
-
|
|
15461
|
-
for (const match of strippedSource.matchAll(
|
|
15462
|
-
|
|
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;
|
|
15463
15599
|
};
|
|
15600
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15464
15601
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15465
15602
|
try {
|
|
15466
|
-
|
|
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);
|
|
15467
15613
|
} catch {
|
|
15468
15614
|
return false;
|
|
15469
15615
|
}
|
|
@@ -15502,6 +15648,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
15502
15648
|
const filename = normalizeFilename(context.filename ?? "");
|
|
15503
15649
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
15504
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;
|
|
15505
15652
|
if (programNode.body?.some((statement) => {
|
|
15506
15653
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
15507
15654
|
const declaration = statement.declaration;
|
|
@@ -15608,8 +15755,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15608
15755
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15609
15756
|
let didFindFetchCall = false;
|
|
15610
15757
|
walkAst(node, (child) => {
|
|
15758
|
+
if (didFindFetchCall) return false;
|
|
15611
15759
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15612
|
-
if (isFetchCall$1(child))
|
|
15760
|
+
if (isFetchCall$1(child)) {
|
|
15761
|
+
didFindFetchCall = true;
|
|
15762
|
+
return false;
|
|
15763
|
+
}
|
|
15613
15764
|
});
|
|
15614
15765
|
return didFindFetchCall;
|
|
15615
15766
|
};
|
|
@@ -15623,6 +15774,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15623
15774
|
severity: "warn",
|
|
15624
15775
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15625
15776
|
create: (context) => {
|
|
15777
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15778
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15626
15779
|
let fileHasUseClient = false;
|
|
15627
15780
|
return {
|
|
15628
15781
|
Program(programNode) {
|
|
@@ -15632,8 +15785,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15632
15785
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15633
15786
|
const callback = getEffectCallback(node);
|
|
15634
15787
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15635
|
-
|
|
15636
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15788
|
+
context.report({
|
|
15637
15789
|
node,
|
|
15638
15790
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15639
15791
|
});
|
|
@@ -16298,16 +16450,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16298
16450
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16299
16451
|
create: (context) => {
|
|
16300
16452
|
let resolveBinding = () => null;
|
|
16453
|
+
let isRouteHandlerFile = false;
|
|
16454
|
+
let mutatingSegment = null;
|
|
16301
16455
|
return {
|
|
16302
16456
|
Program(node) {
|
|
16303
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;
|
|
16304
16461
|
},
|
|
16305
16462
|
ExportNamedDeclaration(node) {
|
|
16306
|
-
|
|
16307
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16308
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16463
|
+
if (!isRouteHandlerFile) return;
|
|
16309
16464
|
if (!isExportedGetHandler(node)) return;
|
|
16310
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16311
16465
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16312
16466
|
for (const handlerBody of handlerBodies) {
|
|
16313
16467
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17224,14 +17378,38 @@ const resolvesToRefFactoryCall = (identifier) => {
|
|
|
17224
17378
|
});
|
|
17225
17379
|
return found;
|
|
17226
17380
|
};
|
|
17227
|
-
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()) => {
|
|
17228
17406
|
if (!receiver) return false;
|
|
17229
|
-
if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17230
|
-
if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17231
|
-
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);
|
|
17232
17410
|
if (isNodeOfType(receiver, "MemberExpression") && isNodeOfType(receiver.property, "Identifier")) {
|
|
17233
17411
|
if (hasRefLikeName(receiver.property.name)) return true;
|
|
17234
|
-
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object);
|
|
17412
|
+
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object, visitedAliasNames);
|
|
17235
17413
|
}
|
|
17236
17414
|
return false;
|
|
17237
17415
|
};
|
|
@@ -17339,8 +17517,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17339
17517
|
programToAnalysis.set(programNode, analysis);
|
|
17340
17518
|
return analysis;
|
|
17341
17519
|
};
|
|
17520
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17342
17521
|
const getScopeForNode = (node, manager) => {
|
|
17343
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;
|
|
17344
17529
|
let bestScope = null;
|
|
17345
17530
|
let bestSize = Infinity;
|
|
17346
17531
|
for (const scope of manager.scopes) {
|
|
@@ -17353,11 +17538,25 @@ const getScopeForNode = (node, manager) => {
|
|
|
17353
17538
|
bestScope = scope;
|
|
17354
17539
|
}
|
|
17355
17540
|
}
|
|
17541
|
+
scopeByNode.set(node, bestScope);
|
|
17356
17542
|
return bestScope;
|
|
17357
17543
|
};
|
|
17358
17544
|
//#endregion
|
|
17359
17545
|
//#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
|
|
17360
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
|
+
};
|
|
17361
17560
|
const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
17362
17561
|
if (visited.has(ref)) return;
|
|
17363
17562
|
const result = visit(ref);
|
|
@@ -17370,7 +17569,10 @@ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17370
17569
|
const defNode = def.node;
|
|
17371
17570
|
const next = defNode.init ?? defNode.body;
|
|
17372
17571
|
if (!next) continue;
|
|
17373
|
-
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
|
+
}
|
|
17374
17576
|
}
|
|
17375
17577
|
};
|
|
17376
17578
|
const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
@@ -17387,11 +17589,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17387
17589
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17388
17590
|
}
|
|
17389
17591
|
};
|
|
17592
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17390
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;
|
|
17391
17601
|
const refs = [];
|
|
17392
17602
|
ascend(analysis, ref, (upRef) => {
|
|
17393
17603
|
refs.push(upRef);
|
|
17394
17604
|
});
|
|
17605
|
+
upstreamByRef.set(ref, refs);
|
|
17395
17606
|
return refs;
|
|
17396
17607
|
};
|
|
17397
17608
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17401,11 +17612,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17401
17612
|
});
|
|
17402
17613
|
return nodes;
|
|
17403
17614
|
};
|
|
17615
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17404
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;
|
|
17405
17624
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17406
|
-
if (
|
|
17407
|
-
|
|
17408
|
-
|
|
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;
|
|
17409
17633
|
};
|
|
17410
17634
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17411
17635
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17480,22 +17704,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17480
17704
|
};
|
|
17481
17705
|
//#endregion
|
|
17482
17706
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17483
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17484
|
-
if (!node.range) return null;
|
|
17485
|
-
let best = null;
|
|
17486
|
-
let bestSize = Infinity;
|
|
17487
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17488
|
-
const block = scope.block;
|
|
17489
|
-
if (!block?.range) continue;
|
|
17490
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17491
|
-
const size = block.range[1] - block.range[0];
|
|
17492
|
-
if (size <= bestSize) {
|
|
17493
|
-
bestSize = size;
|
|
17494
|
-
best = scope;
|
|
17495
|
-
}
|
|
17496
|
-
}
|
|
17497
|
-
return best;
|
|
17498
|
-
};
|
|
17499
17707
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17500
17708
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17501
17709
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17526,7 +17734,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17526
17734
|
const isWrappedSeparately = () => {
|
|
17527
17735
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17528
17736
|
const bindingName = node.id.name;
|
|
17529
|
-
const containingScope =
|
|
17737
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17530
17738
|
if (!containingScope) return false;
|
|
17531
17739
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17532
17740
|
if (!variable) return false;
|
|
@@ -17813,7 +18021,8 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
17813
18021
|
if (isNodeOfType(value, "Literal") && value.value !== "true") return;
|
|
17814
18022
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
17815
18023
|
const expression = value.expression;
|
|
17816
|
-
if (isNodeOfType(expression, "Literal")
|
|
18024
|
+
if (!isNodeOfType(expression, "Literal")) return;
|
|
18025
|
+
if (expression.value !== true && expression.value !== "true") return;
|
|
17817
18026
|
}
|
|
17818
18027
|
}
|
|
17819
18028
|
const tag = getElementType(node, context.settings);
|
|
@@ -18068,10 +18277,30 @@ const isArrayFromCall = (node) => {
|
|
|
18068
18277
|
*
|
|
18069
18278
|
* Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
|
|
18070
18279
|
*/
|
|
18071
|
-
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) => {
|
|
18072
18294
|
if (isArrayFromCall(receiver)) return true;
|
|
18073
18295
|
if (isArrayConstructorCallWithNumericLength(receiver)) return true;
|
|
18074
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
|
+
}
|
|
18075
18304
|
if (isNodeOfType(receiver, "CallExpression")) {
|
|
18076
18305
|
const callee = receiver.callee;
|
|
18077
18306
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
|
|
@@ -18097,6 +18326,7 @@ const isArrayFromLengthObjectCall = (node) => {
|
|
|
18097
18326
|
if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
|
|
18098
18327
|
if (isNumericLiteralOrUndefined(prop.value)) return true;
|
|
18099
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;
|
|
18100
18330
|
}
|
|
18101
18331
|
return false;
|
|
18102
18332
|
};
|
|
@@ -18205,10 +18435,11 @@ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node,
|
|
|
18205
18435
|
/**
|
|
18206
18436
|
* Walk up from a JSXAttribute node looking for the enclosing iterator
|
|
18207
18437
|
* callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
|
|
18208
|
-
* and return the
|
|
18209
|
-
*
|
|
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) => …)`.
|
|
18210
18441
|
*/
|
|
18211
|
-
const
|
|
18442
|
+
const findIteratorItemNames = (node) => {
|
|
18212
18443
|
let current = node;
|
|
18213
18444
|
while (current.parent) {
|
|
18214
18445
|
const parent = current.parent;
|
|
@@ -18218,18 +18449,20 @@ const findIteratorItemName$1 = (node) => {
|
|
|
18218
18449
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
18219
18450
|
if (isIteratorMethodCall || isArrayFromCallback) {
|
|
18220
18451
|
const first = (current.params ?? [])[0];
|
|
18221
|
-
if (first
|
|
18222
|
-
|
|
18452
|
+
if (!first) return null;
|
|
18453
|
+
const names = /* @__PURE__ */ new Set();
|
|
18454
|
+
collectPatternNames(first, names);
|
|
18455
|
+
return names.size > 0 ? names : null;
|
|
18223
18456
|
}
|
|
18224
18457
|
}
|
|
18225
18458
|
current = parent;
|
|
18226
18459
|
}
|
|
18227
18460
|
return null;
|
|
18228
18461
|
};
|
|
18229
|
-
const templateLiteralHasIteratorIdentity = (template,
|
|
18462
|
+
const templateLiteralHasIteratorIdentity = (template, itemNames) => {
|
|
18230
18463
|
for (const expression of template.expressions ?? []) {
|
|
18231
|
-
|
|
18232
|
-
if (
|
|
18464
|
+
const rootName = getRootIdentifierName(expression, { followCallChains: true });
|
|
18465
|
+
if (rootName !== null && itemNames.has(rootName)) return true;
|
|
18233
18466
|
}
|
|
18234
18467
|
return false;
|
|
18235
18468
|
};
|
|
@@ -18242,9 +18475,32 @@ const templateLiteralHasIteratorIdentity = (template, itemName) => {
|
|
|
18242
18475
|
const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
|
|
18243
18476
|
if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
|
|
18244
18477
|
if ((keyExpression.expressions ?? []).length < 2) return false;
|
|
18245
|
-
const
|
|
18246
|
-
if (!
|
|
18247
|
-
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;
|
|
18248
18504
|
};
|
|
18249
18505
|
const noArrayIndexAsKey = defineRule({
|
|
18250
18506
|
id: "no-array-index-as-key",
|
|
@@ -18256,6 +18512,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
18256
18512
|
if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
18257
18513
|
const indexName = extractIndexName(node.value.expression);
|
|
18258
18514
|
if (!indexName) return;
|
|
18515
|
+
if (isNumericForLoopCounter(node, indexName)) return;
|
|
18259
18516
|
if (isInsideStaticPlaceholderMap(node)) return;
|
|
18260
18517
|
if (isInsideStringDerivedMap(node)) return;
|
|
18261
18518
|
if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
|
|
@@ -18876,7 +19133,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18876
19133
|
}
|
|
18877
19134
|
return false;
|
|
18878
19135
|
};
|
|
18879
|
-
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
|
+
};
|
|
18880
19147
|
//#endregion
|
|
18881
19148
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18882
19149
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19381,8 +19648,8 @@ const CONTEXT_MODULES = [
|
|
|
19381
19648
|
];
|
|
19382
19649
|
const isCreateContextCallee = (callee) => {
|
|
19383
19650
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19384
|
-
|
|
19385
|
-
return
|
|
19651
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19652
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19386
19653
|
}
|
|
19387
19654
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19388
19655
|
const namespaceIdentifier = callee.object;
|
|
@@ -19392,8 +19659,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19392
19659
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19393
19660
|
const namespaceName = namespaceIdentifier.name;
|
|
19394
19661
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19395
|
-
|
|
19396
|
-
return
|
|
19662
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19663
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19397
19664
|
}
|
|
19398
19665
|
return false;
|
|
19399
19666
|
};
|
|
@@ -19795,38 +20062,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19795
20062
|
};
|
|
19796
20063
|
const parseColorToRgb = (value) => {
|
|
19797
20064
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19817
|
-
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19825
|
-
|
|
19826
|
-
|
|
19827
|
-
|
|
19828
|
-
|
|
19829
|
-
|
|
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
|
+
}
|
|
19830
20103
|
return null;
|
|
19831
20104
|
};
|
|
19832
20105
|
//#endregion
|
|
@@ -19854,9 +20127,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19854
20127
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19855
20128
|
return null;
|
|
19856
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;
|
|
19857
20134
|
const parseShadowLayerBlur = (layer) => {
|
|
19858
|
-
const
|
|
19859
|
-
|
|
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;
|
|
19860
20142
|
};
|
|
19861
20143
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19862
20144
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19976,7 +20258,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19976
20258
|
};
|
|
19977
20259
|
//#endregion
|
|
19978
20260
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20261
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19979
20262
|
const collectComponentPropNames = (componentFunction) => {
|
|
20263
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20264
|
+
if (cached) return cached;
|
|
19980
20265
|
const propNames = /* @__PURE__ */ new Set();
|
|
19981
20266
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19982
20267
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19990,27 +20275,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19990
20275
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19991
20276
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19992
20277
|
});
|
|
20278
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19993
20279
|
return propNames;
|
|
19994
20280
|
};
|
|
19995
|
-
const
|
|
20281
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20282
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20283
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20284
|
+
if (cached) return cached;
|
|
19996
20285
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19997
20286
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19998
|
-
if (boundNames.has(bindingName)) return true;
|
|
19999
|
-
let declaresName = false;
|
|
20000
20287
|
walkAst(functionNode, (child) => {
|
|
20001
|
-
if (declaresName) return false;
|
|
20002
20288
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
20003
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
20004
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
20005
|
-
collectPatternNames(child.id, declaratorNames);
|
|
20006
|
-
if (declaratorNames.has(bindingName)) {
|
|
20007
|
-
declaresName = true;
|
|
20008
|
-
return false;
|
|
20009
|
-
}
|
|
20010
|
-
}
|
|
20289
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
20011
20290
|
});
|
|
20012
|
-
|
|
20291
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20292
|
+
return boundNames;
|
|
20013
20293
|
};
|
|
20294
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20014
20295
|
const isPropertyNamePosition = (identifier) => {
|
|
20015
20296
|
const parent = identifier.parent;
|
|
20016
20297
|
if (!parent) return false;
|
|
@@ -20433,36 +20714,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20433
20714
|
}
|
|
20434
20715
|
return hasNestedFunction;
|
|
20435
20716
|
};
|
|
20436
|
-
const
|
|
20437
|
-
|
|
20438
|
-
|
|
20439
|
-
|
|
20440
|
-
|
|
20441
|
-
|
|
20442
|
-
|
|
20443
|
-
if (isReseeded) return false;
|
|
20444
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20445
|
-
isReseeded = true;
|
|
20446
|
-
return false;
|
|
20447
|
-
}
|
|
20448
|
-
});
|
|
20449
|
-
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;
|
|
20450
20724
|
};
|
|
20451
|
-
const
|
|
20725
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20452
20726
|
const setterName = getStateSetterName(useStateCall);
|
|
20453
20727
|
if (!setterName) return false;
|
|
20454
20728
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20455
20729
|
if (!componentFunction) return false;
|
|
20456
|
-
let
|
|
20730
|
+
let isExempt = false;
|
|
20457
20731
|
walkAst(componentFunction, (child) => {
|
|
20458
|
-
if (
|
|
20459
|
-
if (child
|
|
20460
|
-
|
|
20461
|
-
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;
|
|
20462
20735
|
return false;
|
|
20463
20736
|
}
|
|
20464
20737
|
});
|
|
20465
|
-
return
|
|
20738
|
+
return isExempt;
|
|
20466
20739
|
};
|
|
20467
20740
|
const noDerivedUseState = defineRule({
|
|
20468
20741
|
id: "no-derived-useState",
|
|
@@ -20479,8 +20752,7 @@ const noDerivedUseState = defineRule({
|
|
|
20479
20752
|
const initializer = node.arguments[0];
|
|
20480
20753
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20481
20754
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20482
|
-
if (
|
|
20483
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20755
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20484
20756
|
context.report({
|
|
20485
20757
|
node,
|
|
20486
20758
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20491,8 +20763,7 @@ const noDerivedUseState = defineRule({
|
|
|
20491
20763
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20492
20764
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20493
20765
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20494
|
-
if (
|
|
20495
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20766
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20496
20767
|
context.report({
|
|
20497
20768
|
node,
|
|
20498
20769
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -20607,7 +20878,7 @@ const isStateMemberExpression = (node) => {
|
|
|
20607
20878
|
};
|
|
20608
20879
|
//#endregion
|
|
20609
20880
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
20610
|
-
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.";
|
|
20611
20882
|
const shouldIgnoreMutation = (node) => {
|
|
20612
20883
|
let isConstructor = false;
|
|
20613
20884
|
let isInsideCallExpression = false;
|
|
@@ -20701,6 +20972,18 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
20701
20972
|
}
|
|
20702
20973
|
return producesPlainStateValue(unwrapped);
|
|
20703
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
|
+
};
|
|
20704
20987
|
const collectFunctionLocalBindings = (functionNode) => {
|
|
20705
20988
|
const localBindings = /* @__PURE__ */ new Set();
|
|
20706
20989
|
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
|
|
@@ -20743,8 +21026,10 @@ const noDirectStateMutation = defineRule({
|
|
|
20743
21026
|
const bindings = collectUseStateBindings(componentBody);
|
|
20744
21027
|
if (bindings.length === 0) return;
|
|
20745
21028
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
21029
|
+
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
20746
21030
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
20747
21031
|
for (const binding of bindings) {
|
|
21032
|
+
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
20748
21033
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
20749
21034
|
if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
|
|
20750
21035
|
}
|
|
@@ -20757,7 +21042,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20757
21042
|
if (currentlyShadowed.has(rootName)) return;
|
|
20758
21043
|
context.report({
|
|
20759
21044
|
node: child,
|
|
20760
|
-
message: `
|
|
21045
|
+
message: `React can't tell you changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20761
21046
|
});
|
|
20762
21047
|
return;
|
|
20763
21048
|
}
|
|
@@ -20773,7 +21058,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20773
21058
|
if (currentlyShadowed.has(rootName)) return;
|
|
20774
21059
|
context.report({
|
|
20775
21060
|
node: child,
|
|
20776
|
-
message: `
|
|
21061
|
+
message: `React can't tell .${methodName}() changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20777
21062
|
});
|
|
20778
21063
|
}
|
|
20779
21064
|
});
|
|
@@ -22191,20 +22476,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22191
22476
|
return null;
|
|
22192
22477
|
};
|
|
22193
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
|
+
});
|
|
22194
22491
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22195
|
-
for (const binding of useStateBindings)
|
|
22196
|
-
let didFindAnySetterCall = false;
|
|
22197
|
-
let areAllSetterCallsInHandlers = true;
|
|
22198
|
-
walkAst(componentBody, (child) => {
|
|
22199
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22200
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22201
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22202
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22203
|
-
didFindAnySetterCall = true;
|
|
22204
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22205
|
-
});
|
|
22206
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22207
|
-
}
|
|
22492
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22208
22493
|
return handlerOnlyWriteStateNames;
|
|
22209
22494
|
};
|
|
22210
22495
|
const noEventTriggerState = defineRule({
|
|
@@ -22544,6 +22829,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22544
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)-)/;
|
|
22545
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)-)/;
|
|
22546
22831
|
const splitVariantScope = (token) => {
|
|
22832
|
+
if (!token.includes(":")) return {
|
|
22833
|
+
scope: "",
|
|
22834
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22835
|
+
};
|
|
22547
22836
|
const segments = token.split(":");
|
|
22548
22837
|
const rawUtility = segments[segments.length - 1];
|
|
22549
22838
|
return {
|
|
@@ -22567,6 +22856,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22567
22856
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22568
22857
|
for (const token of classStr.split(/\s+/)) {
|
|
22569
22858
|
if (!token) continue;
|
|
22859
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22570
22860
|
const { scope, utility } = splitVariantScope(token);
|
|
22571
22861
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22572
22862
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -22637,11 +22927,16 @@ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
|
|
|
22637
22927
|
"ulid",
|
|
22638
22928
|
"createId"
|
|
22639
22929
|
]);
|
|
22930
|
+
const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
|
|
22640
22931
|
const containsNonDeterministicSource = (root) => {
|
|
22641
22932
|
let found = false;
|
|
22642
22933
|
walkAst(root, (child) => {
|
|
22643
22934
|
if (found) return false;
|
|
22644
22935
|
if (isFunctionLike$1(child)) return false;
|
|
22936
|
+
if (isZeroArgDateConstruction(child)) {
|
|
22937
|
+
found = true;
|
|
22938
|
+
return false;
|
|
22939
|
+
}
|
|
22645
22940
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22646
22941
|
const callee = child.callee;
|
|
22647
22942
|
if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
|
|
@@ -22702,6 +22997,53 @@ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames
|
|
|
22702
22997
|
});
|
|
22703
22998
|
return found;
|
|
22704
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
|
+
};
|
|
22705
23047
|
const noInitializeState = defineRule({
|
|
22706
23048
|
id: "no-initialize-state",
|
|
22707
23049
|
title: "State initialized from a mount effect",
|
|
@@ -22724,6 +23066,7 @@ const noInitializeState = defineRule({
|
|
|
22724
23066
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
22725
23067
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
|
|
22726
23068
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
|
|
23069
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
|
|
22727
23070
|
const useStateDecl = getUseStateDecl(analysis, ref);
|
|
22728
23071
|
if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
|
|
22729
23072
|
if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
|
|
@@ -23296,6 +23639,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23296
23639
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23297
23640
|
});
|
|
23298
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-])/;
|
|
23299
23644
|
const noLongTransitionDuration = defineRule({
|
|
23300
23645
|
id: "no-long-transition-duration",
|
|
23301
23646
|
title: "Transition duration too long",
|
|
@@ -23319,11 +23664,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23319
23664
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23320
23665
|
let longestDurationPropertyMs = 0;
|
|
23321
23666
|
for (const segment of value.split(",")) {
|
|
23322
|
-
const
|
|
23323
|
-
|
|
23324
|
-
const
|
|
23325
|
-
|
|
23326
|
-
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);
|
|
23327
23671
|
}
|
|
23328
23672
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23329
23673
|
}
|
|
@@ -23331,7 +23675,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23331
23675
|
let longestDurationMs = 0;
|
|
23332
23676
|
for (const segment of value.split(",")) {
|
|
23333
23677
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23334
|
-
const firstTimeMatch = segment.match(
|
|
23678
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23335
23679
|
if (!firstTimeMatch) continue;
|
|
23336
23680
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23337
23681
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24503,14 +24847,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24503
24847
|
out.hasOpaqueBranch = true;
|
|
24504
24848
|
};
|
|
24505
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.`;
|
|
24506
|
-
const
|
|
24850
|
+
const INTERACTIVE_HANDLERS_LOWER = new Set([
|
|
24507
24851
|
"onClick",
|
|
24508
24852
|
"onMouseDown",
|
|
24509
24853
|
"onMouseUp",
|
|
24510
24854
|
"onKeyDown",
|
|
24511
24855
|
"onKeyPress",
|
|
24512
24856
|
"onKeyUp"
|
|
24513
|
-
];
|
|
24857
|
+
].map((handlerName) => handlerName.toLowerCase()));
|
|
24514
24858
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24515
24859
|
id: "no-noninteractive-element-interactions",
|
|
24516
24860
|
title: "Handler on non-interactive element",
|
|
@@ -24525,7 +24869,16 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24525
24869
|
const tag = getElementType(node, context.settings);
|
|
24526
24870
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24527
24871
|
if (tag === "label") return;
|
|
24528
|
-
|
|
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;
|
|
24529
24882
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24530
24883
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24531
24884
|
if (roleAttr) {
|
|
@@ -24631,6 +24984,18 @@ const KEYBOARD_HANDLER_PROP_NAMES = [
|
|
|
24631
24984
|
"onKeyPress"
|
|
24632
24985
|
];
|
|
24633
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
|
+
};
|
|
24634
24999
|
const resolveSettings$14 = (settings) => {
|
|
24635
25000
|
const reactDoctor = settings?.["react-doctor"];
|
|
24636
25001
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -24654,6 +25019,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
24654
25019
|
if (!tabIndex) return;
|
|
24655
25020
|
const tabIndexValue = tabIndex.value;
|
|
24656
25021
|
if (!tabIndexValue) return;
|
|
25022
|
+
if (isRovingTabindexValue(tabIndexValue)) return;
|
|
24657
25023
|
const numeric = parseJsxValue(tabIndexValue);
|
|
24658
25024
|
if (numeric === null) {
|
|
24659
25025
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node)) context.report({
|
|
@@ -25838,16 +26204,22 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25838
26204
|
["tr", "row"],
|
|
25839
26205
|
["ul", "list"]
|
|
25840
26206
|
];
|
|
25841
|
-
const
|
|
25842
|
-
|
|
25843
|
-
|
|
25844
|
-
|
|
25845
|
-
|
|
25846
|
-
const
|
|
25847
|
-
|
|
25848
|
-
|
|
25849
|
-
|
|
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;
|
|
25850
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;
|
|
25851
26223
|
//#endregion
|
|
25852
26224
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
25853
26225
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26084,22 +26456,22 @@ const noRenderInRender = defineRule({
|
|
|
26084
26456
|
title: "Component rendered by inline function call",
|
|
26085
26457
|
severity: "warn",
|
|
26086
26458
|
tags: ["test-noise"],
|
|
26087
|
-
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.",
|
|
26088
26460
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
26089
26461
|
const expression = node.expression;
|
|
26090
26462
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26091
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;
|
|
26092
26467
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26093
26468
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26094
|
-
|
|
26095
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26469
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26096
26470
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26097
|
-
calleeName = expression.callee.property.name;
|
|
26098
26471
|
}
|
|
26099
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26100
26472
|
context.report({
|
|
26101
26473
|
node: expression,
|
|
26102
|
-
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.`
|
|
26103
26475
|
});
|
|
26104
26476
|
} })
|
|
26105
26477
|
});
|
|
@@ -26202,8 +26574,8 @@ const countUseStates = (analysis, componentNode) => {
|
|
|
26202
26574
|
for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
|
|
26203
26575
|
return stateVariables.size;
|
|
26204
26576
|
};
|
|
26205
|
-
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode) => {
|
|
26206
|
-
const stateSetterRefs = effectFnRefs.filter((ref) =>
|
|
26577
|
+
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
|
|
26578
|
+
const stateSetterRefs = effectFnRefs.filter((ref) => isSyncStateSetterCall(analysis, ref, effectFn));
|
|
26207
26579
|
if (stateSetterRefs.length === 0) return null;
|
|
26208
26580
|
if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
|
|
26209
26581
|
const containing = findContainingNode(analysis, useEffectNode);
|
|
@@ -26226,7 +26598,9 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
26226
26598
|
if (!effectFnRefs || !depsRefs) return;
|
|
26227
26599
|
const containing = findContainingNode(analysis, node);
|
|
26228
26600
|
if (containing && isCustomHook(containing)) return;
|
|
26229
|
-
|
|
26601
|
+
const effectFn = getEffectFn(analysis, node);
|
|
26602
|
+
if (!effectFn) return;
|
|
26603
|
+
if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node, effectFn)) return;
|
|
26230
26604
|
context.report({
|
|
26231
26605
|
node,
|
|
26232
26606
|
message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
|
|
@@ -26347,6 +26721,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26347
26721
|
"headers",
|
|
26348
26722
|
"remountDeps"
|
|
26349
26723
|
];
|
|
26724
|
+
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26350
26725
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26351
26726
|
"createFileRoute",
|
|
26352
26727
|
"createRoute",
|
|
@@ -26362,6 +26737,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26362
26737
|
"server",
|
|
26363
26738
|
"handler"
|
|
26364
26739
|
];
|
|
26740
|
+
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26365
26741
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26366
26742
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26367
26743
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27066,14 +27442,21 @@ const noStaticElementInteractions = defineRule({
|
|
|
27066
27442
|
category: "Accessibility",
|
|
27067
27443
|
create: (context) => {
|
|
27068
27444
|
const settings = resolveSettings$12(context.settings);
|
|
27445
|
+
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27069
27446
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27070
27447
|
return { JSXOpeningElement(node) {
|
|
27071
27448
|
if (isTestlikeFile) return;
|
|
27072
27449
|
let hasNonBlockerHandler = false;
|
|
27073
27450
|
let hasAnyHandler = false;
|
|
27074
|
-
|
|
27075
|
-
|
|
27076
|
-
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);
|
|
27077
27460
|
if (isNullValue(attribute)) continue;
|
|
27078
27461
|
hasAnyHandler = true;
|
|
27079
27462
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -27085,6 +27468,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
27085
27468
|
if (!hasNonBlockerHandler) return;
|
|
27086
27469
|
const elementType = getElementType(node, context.settings);
|
|
27087
27470
|
if (!HTML_TAGS.has(elementType)) return;
|
|
27471
|
+
if (elementType === "svg") return;
|
|
27088
27472
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
27089
27473
|
if (isPresentationRole(node)) return;
|
|
27090
27474
|
if (isInteractiveElement(elementType, node)) return;
|
|
@@ -27098,7 +27482,8 @@ const noStaticElementInteractions = defineRule({
|
|
|
27098
27482
|
});
|
|
27099
27483
|
return;
|
|
27100
27484
|
}
|
|
27101
|
-
|
|
27485
|
+
let attributeValue = roleAttribute.value;
|
|
27486
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal") && typeof attributeValue.expression.value === "string") attributeValue = attributeValue.expression;
|
|
27102
27487
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
27103
27488
|
const firstRole = attributeValue.value.toLowerCase().trim().split(/\s+/)[0];
|
|
27104
27489
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
@@ -28254,13 +28639,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28254
28639
|
["xml:lang", "xmlLang"],
|
|
28255
28640
|
["xml:space", "xmlSpace"]
|
|
28256
28641
|
]);
|
|
28257
|
-
const
|
|
28642
|
+
const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
|
|
28258
28643
|
"charset",
|
|
28259
28644
|
"allowFullScreen",
|
|
28260
28645
|
"webkitAllowFullScreen",
|
|
28261
28646
|
"mozAllowFullScreen",
|
|
28262
28647
|
"webkitDirectory"
|
|
28263
|
-
];
|
|
28648
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
28264
28649
|
//#endregion
|
|
28265
28650
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28266
28651
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28464,10 +28849,7 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28464
28849
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28465
28850
|
return !tagName.includes("-");
|
|
28466
28851
|
};
|
|
28467
|
-
const normalizeAttributeCase = (name) =>
|
|
28468
|
-
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28469
|
-
return name;
|
|
28470
|
-
};
|
|
28852
|
+
const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
|
|
28471
28853
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28472
28854
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28473
28855
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28811,32 +29193,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28811
29193
|
if (!isHocCallee$1(parent)) return false;
|
|
28812
29194
|
return parent.arguments[0] === node;
|
|
28813
29195
|
};
|
|
29196
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
29197
|
+
"map",
|
|
29198
|
+
"forEach",
|
|
29199
|
+
"filter",
|
|
29200
|
+
"flatMap",
|
|
29201
|
+
"reduce",
|
|
29202
|
+
"reduceRight"
|
|
29203
|
+
]);
|
|
28814
29204
|
const isReturnOfMapCallback = (node) => {
|
|
28815
29205
|
const parent = node.parent;
|
|
28816
29206
|
if (!parent) return false;
|
|
28817
29207
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28818
29208
|
const callee = parent.callee;
|
|
28819
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28820
|
-
"map",
|
|
28821
|
-
"forEach",
|
|
28822
|
-
"filter",
|
|
28823
|
-
"flatMap",
|
|
28824
|
-
"reduce",
|
|
28825
|
-
"reduceRight"
|
|
28826
|
-
].includes(callee.property.name);
|
|
29209
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28827
29210
|
}
|
|
28828
29211
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28829
29212
|
const callbackParent = parent.parent;
|
|
28830
29213
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28831
29214
|
const callee = callbackParent.callee;
|
|
28832
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28833
|
-
"map",
|
|
28834
|
-
"forEach",
|
|
28835
|
-
"filter",
|
|
28836
|
-
"flatMap",
|
|
28837
|
-
"reduce",
|
|
28838
|
-
"reduceRight"
|
|
28839
|
-
].includes(callee.property.name);
|
|
29215
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28840
29216
|
}
|
|
28841
29217
|
}
|
|
28842
29218
|
return false;
|
|
@@ -28872,10 +29248,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28872
29248
|
valueNode = parent;
|
|
28873
29249
|
parent = parent.parent;
|
|
28874
29250
|
continue;
|
|
28875
|
-
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);
|
|
28876
29252
|
case "AssignmentExpression": {
|
|
28877
29253
|
const assignmentTarget = parent.left;
|
|
28878
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
29254
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28879
29255
|
}
|
|
28880
29256
|
case "Property":
|
|
28881
29257
|
if (parent.value !== valueNode) return false;
|
|
@@ -28968,14 +29344,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28968
29344
|
};
|
|
28969
29345
|
return {
|
|
28970
29346
|
JSXOpeningElement(node) {
|
|
28971
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29347
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28972
29348
|
recordInstantiation(node.name, node.name.name);
|
|
28973
29349
|
return;
|
|
28974
29350
|
}
|
|
28975
29351
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28976
29352
|
},
|
|
28977
29353
|
Identifier(node) {
|
|
28978
|
-
if (!
|
|
29354
|
+
if (!isReactComponentName(node.name)) return;
|
|
28979
29355
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28980
29356
|
recordInstantiation(node, node.name);
|
|
28981
29357
|
},
|
|
@@ -29906,7 +30282,7 @@ const containsVnodeFactoryCall = (root) => {
|
|
|
29906
30282
|
};
|
|
29907
30283
|
const isComponentLikeFunction = (functionNode) => {
|
|
29908
30284
|
const bindingName = getFunctionBindingName(functionNode);
|
|
29909
|
-
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN.test(bindingName))) return true;
|
|
30285
|
+
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$1.test(bindingName))) return true;
|
|
29910
30286
|
const body = "body" in functionNode ? functionNode.body : null;
|
|
29911
30287
|
if (!body || !isAstNode(body)) return false;
|
|
29912
30288
|
return containsJsxElement(body) || containsVnodeFactoryCall(body);
|
|
@@ -30785,13 +31161,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30785
31161
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30786
31162
|
},
|
|
30787
31163
|
JSXAttribute(node) {
|
|
30788
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30789
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30790
31164
|
if (!node.value) return;
|
|
30791
31165
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30792
31166
|
const innerExpression = node.value.expression;
|
|
30793
31167
|
if (!innerExpression) return;
|
|
30794
31168
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
31169
|
+
if (!isInsideFunctionScope(node)) return;
|
|
31170
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30795
31171
|
const parentJsxOpening = node.parent;
|
|
30796
31172
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30797
31173
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -31363,7 +31739,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
31363
31739
|
});
|
|
31364
31740
|
if (!hasCacheUpdate) context.report({
|
|
31365
31741
|
node,
|
|
31366
|
-
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."
|
|
31367
31743
|
});
|
|
31368
31744
|
} })
|
|
31369
31745
|
});
|
|
@@ -32012,6 +32388,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
32012
32388
|
return { JSXExpressionContainer(node) {
|
|
32013
32389
|
if (isTestlikeFile) return;
|
|
32014
32390
|
if (!node.expression) return;
|
|
32391
|
+
if (isGeneratedImageRenderContext(context, findOpeningElementOfChild(node) ?? node)) return;
|
|
32015
32392
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
32016
32393
|
if (matched) {
|
|
32017
32394
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
@@ -32163,41 +32540,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32163
32540
|
});
|
|
32164
32541
|
return found;
|
|
32165
32542
|
};
|
|
32166
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32167
|
-
if (!componentBody) return false;
|
|
32168
|
-
let found = false;
|
|
32169
|
-
walkAst(componentBody, (child) => {
|
|
32170
|
-
if (found) return;
|
|
32171
|
-
if (!isFunctionLike$1(child)) return;
|
|
32172
|
-
const functionBody = child.body;
|
|
32173
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32174
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32175
|
-
});
|
|
32176
|
-
return found;
|
|
32177
|
-
};
|
|
32178
32543
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32179
32544
|
"then",
|
|
32180
32545
|
"catch",
|
|
32181
32546
|
"finally"
|
|
32182
32547
|
]);
|
|
32183
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32184
|
-
if (!componentBody) return false;
|
|
32185
|
-
let found = false;
|
|
32186
|
-
walkAst(componentBody, (child) => {
|
|
32187
|
-
if (found) return;
|
|
32188
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32189
|
-
const callee = child.callee;
|
|
32190
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32191
|
-
for (const argument of child.arguments ?? []) {
|
|
32192
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32193
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32194
|
-
found = true;
|
|
32195
|
-
return;
|
|
32196
|
-
}
|
|
32197
|
-
}
|
|
32198
|
-
});
|
|
32199
|
-
return found;
|
|
32200
|
-
};
|
|
32201
32548
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32202
32549
|
"useApolloClient",
|
|
32203
32550
|
"useMutation",
|
|
@@ -32210,20 +32557,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32210
32557
|
"fetch",
|
|
32211
32558
|
"axios"
|
|
32212
32559
|
]);
|
|
32213
|
-
const
|
|
32214
|
-
if (!body) return false;
|
|
32560
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32215
32561
|
let found = false;
|
|
32216
|
-
walkAst(
|
|
32217
|
-
if (found) return;
|
|
32562
|
+
walkAst(fnBody, (child) => {
|
|
32563
|
+
if (found) return false;
|
|
32218
32564
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32219
32565
|
const callee = child.callee;
|
|
32220
32566
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32221
32567
|
found = true;
|
|
32222
|
-
return;
|
|
32568
|
+
return false;
|
|
32223
32569
|
}
|
|
32224
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")
|
|
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
|
+
}
|
|
32582
|
+
}
|
|
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)) {
|
|
32225
32588
|
found = true;
|
|
32226
|
-
return;
|
|
32589
|
+
return false;
|
|
32227
32590
|
}
|
|
32228
32591
|
}
|
|
32229
32592
|
});
|
|
@@ -32248,7 +32611,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32248
32611
|
const secondBinding = node.id.elements[1];
|
|
32249
32612
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32250
32613
|
const fnBody = enclosingFunctionBody(node);
|
|
32251
|
-
if (fnBody && (
|
|
32614
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32252
32615
|
context.report({
|
|
32253
32616
|
node: node.init,
|
|
32254
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`
|
|
@@ -33042,17 +33405,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33042
33405
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33043
33406
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33044
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
|
+
});
|
|
33045
33413
|
for (const binding of bindings) {
|
|
33046
33414
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33047
33415
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33048
33416
|
const setterSuffix = binding.setterName.slice(3);
|
|
33049
33417
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33050
|
-
|
|
33051
|
-
walkAst(componentBody, (child) => {
|
|
33052
|
-
if (setterCalled) return;
|
|
33053
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33054
|
-
});
|
|
33055
|
-
if (!setterCalled) continue;
|
|
33418
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33056
33419
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33057
33420
|
context.report({
|
|
33058
33421
|
node: binding.declarator,
|
|
@@ -33384,6 +33747,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33384
33747
|
FlashList: ["@shopify/flash-list"],
|
|
33385
33748
|
LegendList: ["@legendapp/list"]
|
|
33386
33749
|
};
|
|
33750
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33387
33751
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33388
33752
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33389
33753
|
"renderItem",
|
|
@@ -33626,33 +33990,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33626
33990
|
requires: ["react-native"],
|
|
33627
33991
|
severity: "warn",
|
|
33628
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.",
|
|
33629
|
-
create: (context) =>
|
|
33630
|
-
|
|
33631
|
-
|
|
33632
|
-
|
|
33633
|
-
|
|
33634
|
-
|
|
33635
|
-
|
|
33636
|
-
|
|
33637
|
-
|
|
33638
|
-
|
|
33639
|
-
|
|
33640
|
-
|
|
33641
|
-
|
|
33642
|
-
|
|
33643
|
-
|
|
33644
|
-
hasDataProp =
|
|
33645
|
-
|
|
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
|
+
});
|
|
33646
34026
|
}
|
|
33647
|
-
}
|
|
33648
|
-
|
|
33649
|
-
if (dataIsEmptyLiteral) return;
|
|
33650
|
-
if (!hasDataProp) return;
|
|
33651
|
-
context.report({
|
|
33652
|
-
node,
|
|
33653
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33654
|
-
});
|
|
33655
|
-
} })
|
|
34027
|
+
};
|
|
34028
|
+
}
|
|
33656
34029
|
});
|
|
33657
34030
|
//#endregion
|
|
33658
34031
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33663,25 +34036,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33663
34036
|
requires: ["react-native"],
|
|
33664
34037
|
severity: "warn",
|
|
33665
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.",
|
|
33666
|
-
create: (context) =>
|
|
33667
|
-
|
|
33668
|
-
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
33672
|
-
|
|
33673
|
-
|
|
33674
|
-
|
|
33675
|
-
|
|
33676
|
-
|
|
33677
|
-
|
|
33678
|
-
|
|
33679
|
-
|
|
33680
|
-
|
|
33681
|
-
|
|
33682
|
-
|
|
33683
|
-
|
|
33684
|
-
|
|
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
|
+
}
|
|
33685
34067
|
});
|
|
33686
34068
|
//#endregion
|
|
33687
34069
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -33822,7 +34204,7 @@ const rnNoDimensionsGet = defineRule({
|
|
|
33822
34204
|
if (binding !== null && !isBindingReactNativeDimensions(node, binding)) return;
|
|
33823
34205
|
if (isMemberProperty(node.callee, "get")) context.report({
|
|
33824
34206
|
node,
|
|
33825
|
-
message: "
|
|
34207
|
+
message: "Dimensions.get() reads the size once and never updates, so layouts built from it go stale on rotation or resize."
|
|
33826
34208
|
});
|
|
33827
34209
|
if (isMemberProperty(node.callee, "addEventListener")) context.report({
|
|
33828
34210
|
node,
|
|
@@ -34644,7 +35026,10 @@ const isExpoUiComponentElement = (openingElement, contextNode, componentName) =>
|
|
|
34644
35026
|
};
|
|
34645
35027
|
//#endregion
|
|
34646
35028
|
//#region src/plugin/rules/react-native/rn-no-raw-text.ts
|
|
34647
|
-
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
|
+
};
|
|
34648
35033
|
const isRawTextContent = (child) => {
|
|
34649
35034
|
if (isNodeOfType(child, "JSXText")) return Boolean(child.value?.trim());
|
|
34650
35035
|
if (!isNodeOfType(child, "JSXExpressionContainer") || !child.expression) return false;
|
|
@@ -34665,9 +35050,10 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34665
35050
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34666
35051
|
return resolveJsxElementName(openingElement);
|
|
34667
35052
|
};
|
|
35053
|
+
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34668
35054
|
const isTextHandlingComponent = (elementName) => {
|
|
34669
35055
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34670
|
-
return
|
|
35056
|
+
return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
|
|
34671
35057
|
};
|
|
34672
35058
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34673
35059
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34711,6 +35097,7 @@ const rnNoRawText = defineRule({
|
|
|
34711
35097
|
return {
|
|
34712
35098
|
Program(programNode) {
|
|
34713
35099
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
35100
|
+
if (!containsJsxElement(programNode)) return;
|
|
34714
35101
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34715
35102
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34716
35103
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38665,14 +39052,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38665
39052
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38666
39053
|
category: "Accessibility",
|
|
38667
39054
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38668
|
-
|
|
38669
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38670
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38671
|
-
if (!role) return;
|
|
38672
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38673
|
-
const isImplicit = !roleAttribute;
|
|
38674
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38675
|
-
if (!supported) return;
|
|
39055
|
+
let ariaAttributes = null;
|
|
38676
39056
|
for (const attribute of node.attributes) {
|
|
38677
39057
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38678
39058
|
const attributeNode = attribute;
|
|
@@ -38682,6 +39062,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38682
39062
|
const propName = propRawName.toLowerCase();
|
|
38683
39063
|
if (!propName.startsWith("aria-")) continue;
|
|
38684
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) {
|
|
38685
39080
|
if (supported.has(propName)) continue;
|
|
38686
39081
|
context.report({
|
|
38687
39082
|
node: attribute,
|
|
@@ -38903,7 +39298,7 @@ const isInsideClassComponent = (node) => {
|
|
|
38903
39298
|
const hasShortCircuitAncestor = (descendant, ancestor) => {
|
|
38904
39299
|
let current = descendant.parent;
|
|
38905
39300
|
while (current && current !== ancestor) {
|
|
38906
|
-
if (isNodeOfType(current, "ConditionalExpression")) return true;
|
|
39301
|
+
if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
|
|
38907
39302
|
if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
|
|
38908
39303
|
current = current.parent ?? null;
|
|
38909
39304
|
}
|
|
@@ -39029,6 +39424,7 @@ const rulesOfHooks = defineRule({
|
|
|
39029
39424
|
if (parentInfo.isComponentOrHook) isInsideComponentOrHook = true;
|
|
39030
39425
|
}
|
|
39031
39426
|
if (!isInsideComponentOrHook) {
|
|
39427
|
+
if (!enclosing.hasResolvedName) return;
|
|
39032
39428
|
if (isLocalNonHookFunctionCallee(node, context.scopes, settings)) return;
|
|
39033
39429
|
context.report({
|
|
39034
39430
|
node: node.callee,
|
|
@@ -39519,6 +39915,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39519
39915
|
cachedFunctionNames.add(node.id.name);
|
|
39520
39916
|
},
|
|
39521
39917
|
CallExpression(node) {
|
|
39918
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39522
39919
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39523
39920
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39524
39921
|
const firstArg = node.arguments?.[0];
|
|
@@ -39602,6 +39999,14 @@ const objectExpressionHasCachingConfig = (objectExpression) => (objectExpression
|
|
|
39602
39999
|
const objectExpressionHasSpread = (objectExpression) => (objectExpression.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement"));
|
|
39603
40000
|
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
39604
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
|
+
};
|
|
39605
40010
|
const serverFetchWithoutRevalidate = defineRule({
|
|
39606
40011
|
id: "server-fetch-without-revalidate",
|
|
39607
40012
|
title: "Fetch without revalidate",
|
|
@@ -39621,7 +40026,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39621
40026
|
isServerSideFile = false;
|
|
39622
40027
|
return;
|
|
39623
40028
|
}
|
|
39624
|
-
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);
|
|
39625
40030
|
},
|
|
39626
40031
|
CallExpression(node) {
|
|
39627
40032
|
if (!isServerSideFile) return;
|
|
@@ -39634,6 +40039,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39634
40039
|
if (objectExpressionHasSpread(optionsArg)) return;
|
|
39635
40040
|
}
|
|
39636
40041
|
const urlArg = node.arguments?.[0];
|
|
40042
|
+
if (isImportMetaUrlAssetArgument(urlArg)) return;
|
|
39637
40043
|
const urlText = isNodeOfType(urlArg, "Literal") && typeof urlArg.value === "string" ? `"${urlArg.value}"` : "url";
|
|
39638
40044
|
context.report({
|
|
39639
40045
|
node,
|
|
@@ -39862,7 +40268,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
39862
40268
|
if (node.kind === "let" || node.kind === "var") {
|
|
39863
40269
|
context.report({
|
|
39864
40270
|
node: declarator,
|
|
39865
|
-
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.`
|
|
39866
40272
|
});
|
|
39867
40273
|
continue;
|
|
39868
40274
|
}
|
|
@@ -39923,25 +40329,29 @@ const GATE_LEADING_VERBS = new Set([
|
|
|
39923
40329
|
"authorize",
|
|
39924
40330
|
"authenticate"
|
|
39925
40331
|
]);
|
|
39926
|
-
const
|
|
39927
|
-
|
|
39928
|
-
|
|
39929
|
-
|
|
39930
|
-
|
|
39931
|
-
|
|
39932
|
-
|
|
39933
|
-
if (declarator.init && !isNodeOfType(declarator.init, "AwaitExpression")) return true;
|
|
39934
|
-
}
|
|
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;
|
|
39935
40339
|
}
|
|
39936
40340
|
return false;
|
|
39937
40341
|
};
|
|
39938
|
-
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) => {
|
|
39939
40349
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
39940
40350
|
for (const declarator of declaration.declarations ?? []) {
|
|
39941
40351
|
const init = declarator.init;
|
|
39942
40352
|
if (!isNodeOfType(init, "AwaitExpression")) continue;
|
|
39943
40353
|
const argument = init.argument;
|
|
39944
|
-
if (isNodeOfType(argument, "
|
|
40354
|
+
if (isNodeOfType(argument, "CallExpression") && isRequestScopedCallee(argument.callee)) return true;
|
|
39945
40355
|
}
|
|
39946
40356
|
return false;
|
|
39947
40357
|
};
|
|
@@ -39976,7 +40386,8 @@ const serverSequentialIndependentAwait = defineRule({
|
|
|
39976
40386
|
if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
|
|
39977
40387
|
if (!declarationStartsWithAwait(nextStatement)) continue;
|
|
39978
40388
|
if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
|
|
39979
|
-
if (
|
|
40389
|
+
if (declarationAwaitsExistingPromise(nextStatement)) continue;
|
|
40390
|
+
if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
|
|
39980
40391
|
if (declarationAwaitsGate(currentStatement)) continue;
|
|
39981
40392
|
context.report({
|
|
39982
40393
|
node: nextStatement,
|
|
@@ -40445,7 +40856,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40445
40856
|
//#endregion
|
|
40446
40857
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40447
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;
|
|
40448
|
-
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
|
+
};
|
|
40449
40869
|
const supabaseTableMissingRls = defineRule({
|
|
40450
40870
|
id: "supabase-table-missing-rls",
|
|
40451
40871
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40456,11 +40876,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40456
40876
|
const content = sanitizeSqlForScan(file.content);
|
|
40457
40877
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40458
40878
|
const findings = [];
|
|
40879
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40459
40880
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40460
40881
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40461
40882
|
const tableName = match[1];
|
|
40462
40883
|
if (tableName === void 0) continue;
|
|
40463
|
-
|
|
40884
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40885
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40464
40886
|
const location = getLocationAtIndex(content, match.index);
|
|
40465
40887
|
findings.push({
|
|
40466
40888
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40736,6 +41158,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40736
41158
|
severity: "warn",
|
|
40737
41159
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40738
41160
|
create: (context) => {
|
|
41161
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40739
41162
|
let hasHeadContentElement = false;
|
|
40740
41163
|
let hasDocumentHeadElement = false;
|
|
40741
41164
|
let hasCustomHeadChildElement = false;
|
|
@@ -40772,8 +41195,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40772
41195
|
};
|
|
40773
41196
|
return {
|
|
40774
41197
|
Program(node) {
|
|
40775
|
-
const filename = context.filename ?? "";
|
|
40776
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40777
41198
|
const statements = node.body ?? [];
|
|
40778
41199
|
for (const statement of statements) collectImportBindings(statement);
|
|
40779
41200
|
for (const statement of statements) {
|
|
@@ -40782,18 +41203,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40782
41203
|
}
|
|
40783
41204
|
},
|
|
40784
41205
|
ImportDeclaration(node) {
|
|
40785
|
-
const filename = context.filename ?? "";
|
|
40786
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40787
41206
|
collectImportBindings(node);
|
|
40788
41207
|
},
|
|
40789
41208
|
VariableDeclarator(node) {
|
|
40790
|
-
const filename = context.filename ?? "";
|
|
40791
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40792
41209
|
collectVariableAlias(node);
|
|
40793
41210
|
},
|
|
40794
41211
|
JSXOpeningElement(node) {
|
|
40795
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40796
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40797
41212
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40798
41213
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40799
41214
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40807,8 +41222,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40807
41222
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40808
41223
|
},
|
|
40809
41224
|
"Program:exit"(programNode) {
|
|
40810
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40811
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40812
41225
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40813
41226
|
node: programNode,
|
|
40814
41227
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40832,27 +41245,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40832
41245
|
requires: ["tanstack-start"],
|
|
40833
41246
|
severity: "warn",
|
|
40834
41247
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40835
|
-
create: (context) =>
|
|
40836
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40837
|
-
|
|
40838
|
-
|
|
40839
|
-
|
|
40840
|
-
|
|
40841
|
-
|
|
40842
|
-
|
|
40843
|
-
|
|
40844
|
-
|
|
40845
|
-
|
|
40846
|
-
|
|
40847
|
-
|
|
40848
|
-
|
|
40849
|
-
|
|
40850
|
-
|
|
40851
|
-
|
|
40852
|
-
|
|
40853
|
-
|
|
40854
|
-
|
|
40855
|
-
|
|
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
|
+
}
|
|
40856
41271
|
});
|
|
40857
41272
|
//#endregion
|
|
40858
41273
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40916,7 +41331,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40916
41331
|
severity: "warn",
|
|
40917
41332
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40918
41333
|
create: (context) => {
|
|
40919
|
-
|
|
41334
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40920
41335
|
let deferredCallbackDepth = 0;
|
|
40921
41336
|
let eventHandlerDepth = 0;
|
|
40922
41337
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40925,7 +41340,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40925
41340
|
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
40926
41341
|
if (callParent.callee === functionNode) return false;
|
|
40927
41342
|
if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
|
|
40928
|
-
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;
|
|
40929
41344
|
};
|
|
40930
41345
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
|
|
40931
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));
|
|
@@ -40953,11 +41368,11 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40953
41368
|
if (isNodeOfType(parent, "ReturnStatement")) {
|
|
40954
41369
|
const outerFunction = findEnclosingFunction(parent);
|
|
40955
41370
|
const hookName = outerFunction ? getFunctionBindingName(outerFunction) : null;
|
|
40956
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41371
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
40957
41372
|
}
|
|
40958
41373
|
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
|
|
40959
41374
|
const hookName = getFunctionBindingName(parent);
|
|
40960
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41375
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
40961
41376
|
}
|
|
40962
41377
|
return false;
|
|
40963
41378
|
};
|
|
@@ -40980,7 +41395,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40980
41395
|
};
|
|
40981
41396
|
return {
|
|
40982
41397
|
CallExpression(node) {
|
|
40983
|
-
if (!isRouteFile) return;
|
|
40984
41398
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40985
41399
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40986
41400
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40992,39 +41406,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40992
41406
|
}
|
|
40993
41407
|
},
|
|
40994
41408
|
"CallExpression:exit"(node) {
|
|
40995
|
-
if (!isRouteFile) return;
|
|
40996
41409
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40997
41410
|
},
|
|
40998
41411
|
JSXAttribute(node) {
|
|
40999
|
-
if (!isRouteFile) return;
|
|
41000
41412
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
41001
41413
|
},
|
|
41002
41414
|
"JSXAttribute:exit"(node) {
|
|
41003
|
-
if (!isRouteFile) return;
|
|
41004
41415
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41005
41416
|
},
|
|
41006
41417
|
Property(node) {
|
|
41007
|
-
if (!isRouteFile) return;
|
|
41008
41418
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
41009
41419
|
},
|
|
41010
41420
|
"Property:exit"(node) {
|
|
41011
|
-
if (!isRouteFile) return;
|
|
41012
41421
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41013
41422
|
},
|
|
41014
41423
|
VariableDeclarator(node) {
|
|
41015
|
-
if (!isRouteFile) return;
|
|
41016
41424
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
41017
41425
|
},
|
|
41018
41426
|
"VariableDeclarator:exit"(node) {
|
|
41019
|
-
if (!isRouteFile) return;
|
|
41020
41427
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41021
41428
|
},
|
|
41022
41429
|
FunctionDeclaration(node) {
|
|
41023
|
-
if (!isRouteFile) return;
|
|
41024
41430
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41025
41431
|
},
|
|
41026
41432
|
"FunctionDeclaration:exit"(node) {
|
|
41027
|
-
if (!isRouteFile) return;
|
|
41028
41433
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41029
41434
|
}
|
|
41030
41435
|
};
|
|
@@ -41108,23 +41513,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41108
41513
|
requires: ["tanstack-start"],
|
|
41109
41514
|
severity: "warn",
|
|
41110
41515
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41111
|
-
create: (context) =>
|
|
41112
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41113
|
-
|
|
41114
|
-
|
|
41115
|
-
|
|
41116
|
-
|
|
41117
|
-
|
|
41118
|
-
|
|
41119
|
-
|
|
41120
|
-
|
|
41121
|
-
|
|
41122
|
-
|
|
41123
|
-
|
|
41124
|
-
|
|
41125
|
-
|
|
41126
|
-
|
|
41127
|
-
|
|
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
|
+
}
|
|
41128
41535
|
});
|
|
41129
41536
|
//#endregion
|
|
41130
41537
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41165,10 +41572,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41165
41572
|
const propertyName = getPropertyKeyName(property);
|
|
41166
41573
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41167
41574
|
}
|
|
41168
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41575
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
|
|
41169
41576
|
let lastIndex = -1;
|
|
41170
41577
|
for (const propertyName of sensitiveProperties) {
|
|
41171
|
-
const currentIndex =
|
|
41578
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
|
|
41172
41579
|
if (currentIndex < lastIndex) {
|
|
41173
41580
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41174
41581
|
context.report({
|
|
@@ -41205,10 +41612,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41205
41612
|
} else return;
|
|
41206
41613
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41207
41614
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41208
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41615
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
|
|
41209
41616
|
let lastIndex = -1;
|
|
41210
41617
|
for (const methodName of orderSensitiveMethods) {
|
|
41211
|
-
const currentIndex =
|
|
41618
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
|
|
41212
41619
|
if (currentIndex < lastIndex) {
|
|
41213
41620
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41214
41621
|
context.report({
|
|
@@ -41492,13 +41899,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41492
41899
|
//#endregion
|
|
41493
41900
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41494
41901
|
const ZOD_MODULE = "zod";
|
|
41902
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41495
41903
|
const getStaticPropertyName = (member) => {
|
|
41496
41904
|
const property = member.property;
|
|
41497
41905
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41498
41906
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41499
41907
|
return null;
|
|
41500
41908
|
};
|
|
41909
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41501
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) => {
|
|
41502
41917
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41503
41918
|
if (!specifier) return null;
|
|
41504
41919
|
const declaration = specifier.parent;
|
|
@@ -41635,25 +42050,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41635
42050
|
tags: ["migration-hint"],
|
|
41636
42051
|
severity: "warn",
|
|
41637
42052
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41638
|
-
create: (context) =>
|
|
41639
|
-
|
|
41640
|
-
|
|
41641
|
-
|
|
41642
|
-
|
|
41643
|
-
|
|
41644
|
-
|
|
41645
|
-
|
|
41646
|
-
|
|
41647
|
-
|
|
41648
|
-
|
|
41649
|
-
|
|
41650
|
-
|
|
41651
|
-
|
|
41652
|
-
|
|
41653
|
-
|
|
41654
|
-
|
|
41655
|
-
|
|
41656
|
-
|
|
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
|
+
}
|
|
41657
42080
|
});
|
|
41658
42081
|
//#endregion
|
|
41659
42082
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41845,16 +42268,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41845
42268
|
tags: ["migration-hint"],
|
|
41846
42269
|
severity: "warn",
|
|
41847
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()`.",
|
|
41848
|
-
create: (context) =>
|
|
41849
|
-
|
|
41850
|
-
|
|
41851
|
-
|
|
41852
|
-
|
|
41853
|
-
|
|
41854
|
-
|
|
41855
|
-
|
|
41856
|
-
|
|
41857
|
-
|
|
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
|
+
}
|
|
41858
42289
|
});
|
|
41859
42290
|
//#endregion
|
|
41860
42291
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41889,13 +42320,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41889
42320
|
tags: ["migration-hint"],
|
|
41890
42321
|
severity: "warn",
|
|
41891
42322
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41892
|
-
create: (context) =>
|
|
41893
|
-
|
|
41894
|
-
|
|
41895
|
-
node
|
|
41896
|
-
|
|
41897
|
-
|
|
41898
|
-
|
|
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
|
+
}
|
|
41899
42339
|
});
|
|
41900
42340
|
//#endregion
|
|
41901
42341
|
//#region src/plugin/rule-registry.ts
|