oxlint-plugin-react-doctor 0.6.2-dev.397816a → 0.6.2-dev.59e8178
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 +760 -542
- 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
|
|
@@ -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) {
|
|
@@ -3458,7 +3524,7 @@ const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
|
3458
3524
|
];
|
|
3459
3525
|
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
3460
3526
|
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
3461
|
-
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
3527
|
+
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth(?!or(?!i[sz])))/i;
|
|
3462
3528
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
3463
3529
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
3464
3530
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
@@ -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
|
};
|
|
@@ -5658,12 +5719,20 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5658
5719
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5659
5720
|
};
|
|
5660
5721
|
const isInertParseTarget = (target, fileContent) => {
|
|
5722
|
+
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5723
|
+
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5724
|
+
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5661
5725
|
const escapedTarget = escapeRegExp(target);
|
|
5662
5726
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5663
5727
|
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
|
-
|
|
5728
|
+
if (fileHasCreateElement) {
|
|
5729
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5730
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5731
|
+
}
|
|
5732
|
+
if (fileHasIsolatedDocument) {
|
|
5733
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5734
|
+
}
|
|
5735
|
+
if (!fileHasCreateElement) return false;
|
|
5667
5736
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5668
5737
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5669
5738
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5756,10 +5825,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
5756
5825
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5757
5826
|
if (valueIdentifier !== void 0) {
|
|
5758
5827
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5828
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5829
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5830
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
5763
5831
|
}
|
|
5764
5832
|
}
|
|
5765
5833
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5908,6 +5976,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5908
5976
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5909
5977
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5910
5978
|
if (!classNameLiteral) return;
|
|
5979
|
+
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5911
5980
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5912
5981
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5913
5982
|
if (matchedPairs.length === 0) return;
|
|
@@ -5932,6 +6001,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
5932
6001
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5933
6002
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5934
6003
|
if (!classNameLiteral) return;
|
|
6004
|
+
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
5935
6005
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
5936
6006
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
5937
6007
|
if (matchedPairs.length === 0) return;
|
|
@@ -5956,6 +6026,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
5956
6026
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5957
6027
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5958
6028
|
if (!classNameLiteral) return;
|
|
6029
|
+
if (!classNameLiteral.includes("space-")) return;
|
|
5959
6030
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
5960
6031
|
let hasFlexOrGridLayout = false;
|
|
5961
6032
|
for (const token of tokens) {
|
|
@@ -6147,7 +6218,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6147
6218
|
const actualMinor = Number(match[2]);
|
|
6148
6219
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6149
6220
|
};
|
|
6221
|
+
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6150
6222
|
const containsJsx$1 = (root) => {
|
|
6223
|
+
const cached = containsJsxCache.get(root);
|
|
6224
|
+
if (cached !== void 0) return cached;
|
|
6151
6225
|
let found = false;
|
|
6152
6226
|
const visit = (node) => {
|
|
6153
6227
|
if (found) return;
|
|
@@ -6170,6 +6244,7 @@ const containsJsx$1 = (root) => {
|
|
|
6170
6244
|
}
|
|
6171
6245
|
};
|
|
6172
6246
|
visit(root);
|
|
6247
|
+
containsJsxCache.set(root, found);
|
|
6173
6248
|
return found;
|
|
6174
6249
|
};
|
|
6175
6250
|
const getStaticMemberName = (node) => {
|
|
@@ -6261,27 +6336,6 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6261
6336
|
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
6337
|
return false;
|
|
6263
6338
|
};
|
|
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
6339
|
const memberExpressionPath = (node) => {
|
|
6286
6340
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6287
6341
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6289,15 +6343,16 @@ const memberExpressionPath = (node) => {
|
|
|
6289
6343
|
const propertyName = getStaticMemberName(node);
|
|
6290
6344
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6291
6345
|
};
|
|
6292
|
-
const
|
|
6293
|
-
|
|
6346
|
+
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6347
|
+
const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
6348
|
+
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6349
|
+
if (cached) return cached;
|
|
6350
|
+
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6351
|
+
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6294
6352
|
const visit = (node) => {
|
|
6295
|
-
if (found) return;
|
|
6296
6353
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6297
|
-
if (
|
|
6298
|
-
|
|
6299
|
-
return;
|
|
6300
|
-
}
|
|
6354
|
+
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
6355
|
+
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
6301
6356
|
}
|
|
6302
6357
|
const record = node;
|
|
6303
6358
|
for (const key of Object.keys(record)) {
|
|
@@ -6306,12 +6361,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
|
6306
6361
|
if (Array.isArray(child)) {
|
|
6307
6362
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6308
6363
|
} else if (isAstNode(child)) visit(child);
|
|
6309
|
-
if (found) return;
|
|
6310
6364
|
}
|
|
6311
6365
|
};
|
|
6312
6366
|
visit(programRoot);
|
|
6313
|
-
|
|
6367
|
+
const index = {
|
|
6368
|
+
identifierTargets,
|
|
6369
|
+
memberPathSegments
|
|
6370
|
+
};
|
|
6371
|
+
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6372
|
+
return index;
|
|
6314
6373
|
};
|
|
6374
|
+
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6375
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6315
6376
|
const displayName = defineRule({
|
|
6316
6377
|
id: "display-name",
|
|
6317
6378
|
title: "Component missing display name",
|
|
@@ -7183,27 +7244,8 @@ const isDescendantScope = (inner, outer) => {
|
|
|
7183
7244
|
return false;
|
|
7184
7245
|
};
|
|
7185
7246
|
//#endregion
|
|
7186
|
-
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7187
|
-
/**
|
|
7188
|
-
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7189
|
-
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7190
|
-
* match (`true`) or the chain's root (`false`).
|
|
7191
|
-
*
|
|
7192
|
-
* Was duplicated byte-identical in:
|
|
7193
|
-
* - exhaustive-deps-symbol-stability.ts
|
|
7194
|
-
* - semantic/closure-captures.ts
|
|
7195
|
-
*/
|
|
7196
|
-
const isAstDescendant = (inner, outer) => {
|
|
7197
|
-
let current = inner;
|
|
7198
|
-
while (current) {
|
|
7199
|
-
if (current === outer) return true;
|
|
7200
|
-
current = current.parent ?? null;
|
|
7201
|
-
}
|
|
7202
|
-
return false;
|
|
7203
|
-
};
|
|
7204
|
-
//#endregion
|
|
7205
7247
|
//#region src/plugin/semantic/closure-captures.ts
|
|
7206
|
-
const
|
|
7248
|
+
const computeClosureCaptures = (functionNode, scopes) => {
|
|
7207
7249
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
7208
7250
|
const out = [];
|
|
7209
7251
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7238,7 +7280,20 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
7238
7280
|
}
|
|
7239
7281
|
};
|
|
7240
7282
|
visit(functionNode);
|
|
7241
|
-
return out
|
|
7283
|
+
return out;
|
|
7284
|
+
};
|
|
7285
|
+
const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
7286
|
+
const closureCaptures = (functionNode, scopes) => {
|
|
7287
|
+
let capturesByFunction = capturesByAnalysis.get(scopes);
|
|
7288
|
+
if (!capturesByFunction) {
|
|
7289
|
+
capturesByFunction = /* @__PURE__ */ new WeakMap();
|
|
7290
|
+
capturesByAnalysis.set(scopes, capturesByFunction);
|
|
7291
|
+
}
|
|
7292
|
+
const memoizedCaptures = capturesByFunction.get(functionNode);
|
|
7293
|
+
if (memoizedCaptures) return memoizedCaptures;
|
|
7294
|
+
const computedCaptures = computeClosureCaptures(functionNode, scopes);
|
|
7295
|
+
capturesByFunction.set(functionNode, computedCaptures);
|
|
7296
|
+
return computedCaptures;
|
|
7242
7297
|
};
|
|
7243
7298
|
//#endregion
|
|
7244
7299
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
@@ -7367,6 +7422,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
|
|
|
7367
7422
|
};
|
|
7368
7423
|
};
|
|
7369
7424
|
//#endregion
|
|
7425
|
+
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7426
|
+
/**
|
|
7427
|
+
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7428
|
+
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7429
|
+
* match (`true`) or the chain's root (`false`).
|
|
7430
|
+
*
|
|
7431
|
+
* Was duplicated byte-identical in:
|
|
7432
|
+
* - exhaustive-deps-symbol-stability.ts
|
|
7433
|
+
* - semantic/closure-captures.ts
|
|
7434
|
+
*/
|
|
7435
|
+
const isAstDescendant = (inner, outer) => {
|
|
7436
|
+
let current = inner;
|
|
7437
|
+
while (current) {
|
|
7438
|
+
if (current === outer) return true;
|
|
7439
|
+
current = current.parent ?? null;
|
|
7440
|
+
}
|
|
7441
|
+
return false;
|
|
7442
|
+
};
|
|
7443
|
+
//#endregion
|
|
7370
7444
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
7371
7445
|
/**
|
|
7372
7446
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -8266,9 +8340,14 @@ const firebaseQueryFilterAsAuth = defineRule({
|
|
|
8266
8340
|
* `*Handler`), so the cheap escape-then-replace shape is enough
|
|
8267
8341
|
* and avoids pulling picomatch into the per-file rule path.
|
|
8268
8342
|
*/
|
|
8343
|
+
const compiledGlobs = /* @__PURE__ */ new Map();
|
|
8269
8344
|
const compileGlob = (pattern) => {
|
|
8345
|
+
const cached = compiledGlobs.get(pattern);
|
|
8346
|
+
if (cached) return cached;
|
|
8270
8347
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
8271
|
-
|
|
8348
|
+
const compiled = new RegExp(`^${escaped}$`);
|
|
8349
|
+
compiledGlobs.set(pattern, compiled);
|
|
8350
|
+
return compiled;
|
|
8272
8351
|
};
|
|
8273
8352
|
//#endregion
|
|
8274
8353
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
@@ -8558,7 +8637,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
8558
8637
|
const resolveSettings$40 = (settings) => {
|
|
8559
8638
|
const reactDoctor = settings?.["react-doctor"];
|
|
8560
8639
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
8561
|
-
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
8640
|
+
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
8562
8641
|
};
|
|
8563
8642
|
const headingHasContent = defineRule({
|
|
8564
8643
|
id: "heading-has-content",
|
|
@@ -8571,7 +8650,7 @@ const headingHasContent = defineRule({
|
|
|
8571
8650
|
const settings = resolveSettings$40(context.settings);
|
|
8572
8651
|
return { JSXOpeningElement(node) {
|
|
8573
8652
|
const elementType = getElementType(node, context.settings);
|
|
8574
|
-
if (!settings.headingTags.
|
|
8653
|
+
if (!settings.headingTags.has(elementType)) return;
|
|
8575
8654
|
const parent = node.parent;
|
|
8576
8655
|
if (parent && isNodeOfType(parent, "JSXElement")) {
|
|
8577
8656
|
if (objectHasAccessibleChild(parent, context.settings)) return;
|
|
@@ -9210,10 +9289,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9210
9289
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9211
9290
|
const settings = resolveSettings$37(context.settings);
|
|
9212
9291
|
return { JSXOpeningElement(node) {
|
|
9213
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9214
9292
|
const tag = getElementType(node, context.settings);
|
|
9215
9293
|
if (!settings.components.includes(tag)) return;
|
|
9216
9294
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9295
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9217
9296
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9218
9297
|
if (!altAttribute) return;
|
|
9219
9298
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9268,6 +9347,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9268
9347
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9269
9348
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9270
9349
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9350
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9271
9351
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9272
9352
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9273
9353
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9297,6 +9377,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9297
9377
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9298
9378
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9299
9379
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9380
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9300
9381
|
const content = getScannableContent(file);
|
|
9301
9382
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9302
9383
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9475,6 +9556,7 @@ const KEYBOARD_EVENT_HANDLERS = [
|
|
|
9475
9556
|
"onKeyUp"
|
|
9476
9557
|
];
|
|
9477
9558
|
const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
|
|
9559
|
+
const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
|
|
9478
9560
|
//#endregion
|
|
9479
9561
|
//#region src/plugin/utils/is-disabled-element.ts
|
|
9480
9562
|
const isDisabledElement = (openingElement) => {
|
|
@@ -9529,15 +9611,25 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9529
9611
|
const settings = resolveSettings$36(context.settings);
|
|
9530
9612
|
const tabbableSet = new Set(settings.tabbable);
|
|
9531
9613
|
return { JSXOpeningElement(node) {
|
|
9614
|
+
if (node.attributes.length === 0) return;
|
|
9532
9615
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9533
9616
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9534
9617
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9618
|
+
if (!role) return;
|
|
9619
|
+
let hasInteractiveHandler = false;
|
|
9620
|
+
for (const attribute of node.attributes) {
|
|
9621
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
9622
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
9623
|
+
if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
9624
|
+
hasInteractiveHandler = true;
|
|
9625
|
+
break;
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
if (!hasInteractiveHandler) return;
|
|
9535
9629
|
const elementType = getElementType(node, context.settings);
|
|
9536
9630
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9537
|
-
|
|
9631
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9538
9632
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9539
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9540
|
-
if (!role) return;
|
|
9541
9633
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9542
9634
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9543
9635
|
context.report({
|
|
@@ -10176,8 +10268,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10176
10268
|
walkAst(loopBody, (child) => {
|
|
10177
10269
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10178
10270
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10179
|
-
});
|
|
10180
|
-
walkAst(loopBody, (child) => {
|
|
10181
10271
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10182
10272
|
if (child.computed) return;
|
|
10183
10273
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10637,7 +10727,6 @@ const jsHoistIntl = defineRule({
|
|
|
10637
10727
|
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",
|
|
10638
10728
|
create: (context) => ({ NewExpression(node) {
|
|
10639
10729
|
if (!isIntlNewExpression(node)) return;
|
|
10640
|
-
if (isInsideCacheMemo(node)) return;
|
|
10641
10730
|
let cursor = node.parent ?? null;
|
|
10642
10731
|
let inFunctionBody = false;
|
|
10643
10732
|
while (cursor) {
|
|
@@ -10654,6 +10743,7 @@ const jsHoistIntl = defineRule({
|
|
|
10654
10743
|
cursor = cursor.parent ?? null;
|
|
10655
10744
|
}
|
|
10656
10745
|
if (!inFunctionBody) return;
|
|
10746
|
+
if (isInsideCacheMemo(node)) return;
|
|
10657
10747
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10658
10748
|
context.report({
|
|
10659
10749
|
node,
|
|
@@ -10728,7 +10818,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10728
10818
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10729
10819
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10730
10820
|
};
|
|
10731
|
-
const
|
|
10821
|
+
const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
10822
|
+
const getLoopBoundNames = (loop) => {
|
|
10823
|
+
const cached = loopBoundNamesCache.get(loop);
|
|
10824
|
+
if (cached) return cached;
|
|
10825
|
+
const names = /* @__PURE__ */ new Set();
|
|
10732
10826
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10733
10827
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10734
10828
|
});
|
|
@@ -10745,6 +10839,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10745
10839
|
if (targetRoot) names.add(targetRoot);
|
|
10746
10840
|
}
|
|
10747
10841
|
});
|
|
10842
|
+
loopBoundNamesCache.set(loop, names);
|
|
10843
|
+
return names;
|
|
10748
10844
|
};
|
|
10749
10845
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10750
10846
|
let cursor = receiver;
|
|
@@ -10770,7 +10866,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10770
10866
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10771
10867
|
let ancestor = node.parent;
|
|
10772
10868
|
while (ancestor) {
|
|
10773
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10869
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10774
10870
|
ancestor = ancestor.parent;
|
|
10775
10871
|
}
|
|
10776
10872
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -12529,6 +12625,7 @@ const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
|
12529
12625
|
"bottomContent",
|
|
12530
12626
|
"leftContent",
|
|
12531
12627
|
"rightContent",
|
|
12628
|
+
"config",
|
|
12532
12629
|
"value",
|
|
12533
12630
|
"currentValue",
|
|
12534
12631
|
"form",
|
|
@@ -12667,6 +12764,10 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12667
12764
|
"Panel",
|
|
12668
12765
|
"Overlay",
|
|
12669
12766
|
"Shape",
|
|
12767
|
+
"Avatar",
|
|
12768
|
+
"Text",
|
|
12769
|
+
"State",
|
|
12770
|
+
"Zone",
|
|
12670
12771
|
"Override",
|
|
12671
12772
|
"Overrides",
|
|
12672
12773
|
"Items",
|
|
@@ -12683,6 +12784,8 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12683
12784
|
];
|
|
12684
12785
|
const isSlotPropName = (propName) => {
|
|
12685
12786
|
if (KNOWN_SLOT_PROP_NAMES.has(propName)) return true;
|
|
12787
|
+
const decapitalized = propName.charAt(0).toLowerCase() + propName.slice(1);
|
|
12788
|
+
if (decapitalized !== propName && KNOWN_SLOT_PROP_NAMES.has(decapitalized)) return true;
|
|
12686
12789
|
for (const suffix of SLOT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
12687
12790
|
return false;
|
|
12688
12791
|
};
|
|
@@ -14371,14 +14474,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14371
14474
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14372
14475
|
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`.";
|
|
14373
14476
|
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.";
|
|
14374
|
-
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14477
|
+
const DEFAULT_CONTROL_COMPONENTS = new Set([
|
|
14375
14478
|
"input",
|
|
14376
14479
|
"meter",
|
|
14377
14480
|
"output",
|
|
14378
14481
|
"progress",
|
|
14379
14482
|
"select",
|
|
14380
14483
|
"textarea"
|
|
14381
|
-
];
|
|
14484
|
+
]);
|
|
14382
14485
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14383
14486
|
"alt",
|
|
14384
14487
|
"aria-label",
|
|
@@ -14390,8 +14493,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14390
14493
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14391
14494
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14392
14495
|
return {
|
|
14393
|
-
labelComponents: ["label", ...ruleSettings.labelComponents ?? []]
|
|
14394
|
-
labelAttributes:
|
|
14496
|
+
labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
|
|
14497
|
+
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14395
14498
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14396
14499
|
assert: ruleSettings.assert ?? "either",
|
|
14397
14500
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14399,7 +14502,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14399
14502
|
};
|
|
14400
14503
|
};
|
|
14401
14504
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14402
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14505
|
+
if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
|
|
14403
14506
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14404
14507
|
};
|
|
14405
14508
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14425,7 +14528,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14425
14528
|
const attributeName = attribute.name;
|
|
14426
14529
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14427
14530
|
const propName = getJsxAttributeName(attributeName);
|
|
14428
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14531
|
+
if (!propName || !searchContext.labelAttributes.has(propName)) continue;
|
|
14429
14532
|
const attributeValue = attribute.value;
|
|
14430
14533
|
if (!attributeValue) continue;
|
|
14431
14534
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14447,7 +14550,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14447
14550
|
const attributeName = attribute.name;
|
|
14448
14551
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14449
14552
|
const propName = getJsxAttributeName(attributeName);
|
|
14450
|
-
if (propName && searchContext.labelAttributes.
|
|
14553
|
+
if (propName && searchContext.labelAttributes.has(propName)) return true;
|
|
14451
14554
|
}
|
|
14452
14555
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14453
14556
|
return false;
|
|
@@ -14470,7 +14573,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14470
14573
|
if (isTestlikeFile) return;
|
|
14471
14574
|
const opening = node.openingElement;
|
|
14472
14575
|
const tagName = getElementType(opening, context.settings);
|
|
14473
|
-
if (!settings.labelComponents.
|
|
14576
|
+
if (!settings.labelComponents.has(tagName)) return;
|
|
14474
14577
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14475
14578
|
const searchContext = {
|
|
14476
14579
|
depth: settings.depth,
|
|
@@ -15059,9 +15162,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15059
15162
|
const reactDoctor = settings?.["react-doctor"];
|
|
15060
15163
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15061
15164
|
return {
|
|
15062
|
-
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15063
|
-
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15064
|
-
track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
|
|
15165
|
+
audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
|
|
15166
|
+
video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
|
|
15167
|
+
track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
|
|
15065
15168
|
};
|
|
15066
15169
|
};
|
|
15067
15170
|
const evaluateMuted = (attribute) => {
|
|
@@ -15090,7 +15193,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15090
15193
|
let rendersCaptionTrack = false;
|
|
15091
15194
|
walkAst(expression, (inner) => {
|
|
15092
15195
|
if (rendersCaptionTrack) return false;
|
|
15093
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15196
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15094
15197
|
rendersCaptionTrack = true;
|
|
15095
15198
|
return false;
|
|
15096
15199
|
}
|
|
@@ -15108,7 +15211,7 @@ const mediaHasCaption = defineRule({
|
|
|
15108
15211
|
const settings = resolveSettings$23(context.settings);
|
|
15109
15212
|
return { JSXOpeningElement(node) {
|
|
15110
15213
|
const tag = getElementType(node, context.settings);
|
|
15111
|
-
if (!(settings.audio.
|
|
15214
|
+
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
15112
15215
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15113
15216
|
const parent = node.parent;
|
|
15114
15217
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15123,7 +15226,7 @@ const mediaHasCaption = defineRule({
|
|
|
15123
15226
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15124
15227
|
const opening = child.openingElement;
|
|
15125
15228
|
const childTag = getElementType(opening, context.settings);
|
|
15126
|
-
if (!settings.track.
|
|
15229
|
+
if (!settings.track.has(childTag)) return false;
|
|
15127
15230
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15128
15231
|
if (!kindAttribute) return false;
|
|
15129
15232
|
let kindValue = kindAttribute.value;
|
|
@@ -15434,16 +15537,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15434
15537
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15435
15538
|
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;
|
|
15436
15539
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15437
|
-
const
|
|
15540
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15438
15541
|
const strippedSource = stripJsComments(sourceText);
|
|
15439
|
-
|
|
15440
|
-
|
|
15441
|
-
for (const match of strippedSource.matchAll(
|
|
15442
|
-
|
|
15542
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
15543
|
+
if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
|
|
15544
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
|
|
15545
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
|
|
15546
|
+
const specifiersText = match[1] ?? "";
|
|
15547
|
+
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15548
|
+
}
|
|
15549
|
+
return exportedNames;
|
|
15443
15550
|
};
|
|
15551
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15444
15552
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15445
15553
|
try {
|
|
15446
|
-
|
|
15554
|
+
const fileStat = fs.statSync(filePath);
|
|
15555
|
+
const cached = exportNamesCache.get(filePath);
|
|
15556
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15557
|
+
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15558
|
+
exportNamesCache.set(filePath, {
|
|
15559
|
+
mtimeMs: fileStat.mtimeMs,
|
|
15560
|
+
size: fileStat.size,
|
|
15561
|
+
exportedNames
|
|
15562
|
+
});
|
|
15563
|
+
return exportedNames.has(exportedName);
|
|
15447
15564
|
} catch {
|
|
15448
15565
|
return false;
|
|
15449
15566
|
}
|
|
@@ -15588,8 +15705,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15588
15705
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15589
15706
|
let didFindFetchCall = false;
|
|
15590
15707
|
walkAst(node, (child) => {
|
|
15708
|
+
if (didFindFetchCall) return false;
|
|
15591
15709
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15592
|
-
if (isFetchCall$1(child))
|
|
15710
|
+
if (isFetchCall$1(child)) {
|
|
15711
|
+
didFindFetchCall = true;
|
|
15712
|
+
return false;
|
|
15713
|
+
}
|
|
15593
15714
|
});
|
|
15594
15715
|
return didFindFetchCall;
|
|
15595
15716
|
};
|
|
@@ -15603,6 +15724,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15603
15724
|
severity: "warn",
|
|
15604
15725
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15605
15726
|
create: (context) => {
|
|
15727
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15728
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15606
15729
|
let fileHasUseClient = false;
|
|
15607
15730
|
return {
|
|
15608
15731
|
Program(programNode) {
|
|
@@ -15612,8 +15735,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15612
15735
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15613
15736
|
const callback = getEffectCallback(node);
|
|
15614
15737
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15615
|
-
|
|
15616
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15738
|
+
context.report({
|
|
15617
15739
|
node,
|
|
15618
15740
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15619
15741
|
});
|
|
@@ -16278,16 +16400,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16278
16400
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16279
16401
|
create: (context) => {
|
|
16280
16402
|
let resolveBinding = () => null;
|
|
16403
|
+
let isRouteHandlerFile = false;
|
|
16404
|
+
let mutatingSegment = null;
|
|
16281
16405
|
return {
|
|
16282
16406
|
Program(node) {
|
|
16283
16407
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16408
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16409
|
+
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16410
|
+
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16284
16411
|
},
|
|
16285
16412
|
ExportNamedDeclaration(node) {
|
|
16286
|
-
|
|
16287
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16288
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16413
|
+
if (!isRouteHandlerFile) return;
|
|
16289
16414
|
if (!isExportedGetHandler(node)) return;
|
|
16290
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16291
16415
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16292
16416
|
for (const handlerBody of handlerBodies) {
|
|
16293
16417
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17319,8 +17443,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17319
17443
|
programToAnalysis.set(programNode, analysis);
|
|
17320
17444
|
return analysis;
|
|
17321
17445
|
};
|
|
17446
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17322
17447
|
const getScopeForNode = (node, manager) => {
|
|
17323
17448
|
if (!node.range) return null;
|
|
17449
|
+
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17450
|
+
if (!scopeByNode) {
|
|
17451
|
+
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17452
|
+
scopeByNodeCache.set(manager, scopeByNode);
|
|
17453
|
+
}
|
|
17454
|
+
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17324
17455
|
let bestScope = null;
|
|
17325
17456
|
let bestSize = Infinity;
|
|
17326
17457
|
for (const scope of manager.scopes) {
|
|
@@ -17333,6 +17464,7 @@ const getScopeForNode = (node, manager) => {
|
|
|
17333
17464
|
bestScope = scope;
|
|
17334
17465
|
}
|
|
17335
17466
|
}
|
|
17467
|
+
scopeByNode.set(node, bestScope);
|
|
17336
17468
|
return bestScope;
|
|
17337
17469
|
};
|
|
17338
17470
|
//#endregion
|
|
@@ -17367,11 +17499,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17367
17499
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17368
17500
|
}
|
|
17369
17501
|
};
|
|
17502
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17370
17503
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17504
|
+
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17505
|
+
if (!upstreamByRef) {
|
|
17506
|
+
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17507
|
+
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17508
|
+
}
|
|
17509
|
+
const cached = upstreamByRef.get(ref);
|
|
17510
|
+
if (cached) return cached;
|
|
17371
17511
|
const refs = [];
|
|
17372
17512
|
ascend(analysis, ref, (upRef) => {
|
|
17373
17513
|
refs.push(upRef);
|
|
17374
17514
|
});
|
|
17515
|
+
upstreamByRef.set(ref, refs);
|
|
17375
17516
|
return refs;
|
|
17376
17517
|
};
|
|
17377
17518
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17381,11 +17522,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17381
17522
|
});
|
|
17382
17523
|
return nodes;
|
|
17383
17524
|
};
|
|
17525
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17384
17526
|
const getRef = (analysis, identifier) => {
|
|
17527
|
+
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17528
|
+
if (!refByIdentifier) {
|
|
17529
|
+
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17530
|
+
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17531
|
+
}
|
|
17532
|
+
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17533
|
+
let resolvedReference = null;
|
|
17385
17534
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17386
|
-
if (
|
|
17387
|
-
|
|
17388
|
-
|
|
17535
|
+
if (scope) {
|
|
17536
|
+
for (const reference of scope.references) if (reference.identifier === identifier) {
|
|
17537
|
+
resolvedReference = reference;
|
|
17538
|
+
break;
|
|
17539
|
+
}
|
|
17540
|
+
}
|
|
17541
|
+
refByIdentifier.set(identifier, resolvedReference);
|
|
17542
|
+
return resolvedReference;
|
|
17389
17543
|
};
|
|
17390
17544
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17391
17545
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17460,22 +17614,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17460
17614
|
};
|
|
17461
17615
|
//#endregion
|
|
17462
17616
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17463
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17464
|
-
if (!node.range) return null;
|
|
17465
|
-
let best = null;
|
|
17466
|
-
let bestSize = Infinity;
|
|
17467
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17468
|
-
const block = scope.block;
|
|
17469
|
-
if (!block?.range) continue;
|
|
17470
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17471
|
-
const size = block.range[1] - block.range[0];
|
|
17472
|
-
if (size <= bestSize) {
|
|
17473
|
-
bestSize = size;
|
|
17474
|
-
best = scope;
|
|
17475
|
-
}
|
|
17476
|
-
}
|
|
17477
|
-
return best;
|
|
17478
|
-
};
|
|
17479
17617
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17480
17618
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17481
17619
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17506,7 +17644,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17506
17644
|
const isWrappedSeparately = () => {
|
|
17507
17645
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17508
17646
|
const bindingName = node.id.name;
|
|
17509
|
-
const containingScope =
|
|
17647
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17510
17648
|
if (!containingScope) return false;
|
|
17511
17649
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17512
17650
|
if (!variable) return false;
|
|
@@ -18856,7 +18994,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18856
18994
|
}
|
|
18857
18995
|
return false;
|
|
18858
18996
|
};
|
|
18859
|
-
const
|
|
18997
|
+
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
18998
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
18999
|
+
const cachedEntry = renderOutputCache.get(functionNode);
|
|
19000
|
+
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
19001
|
+
const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
|
|
19002
|
+
renderOutputCache.set(functionNode, {
|
|
19003
|
+
scopes,
|
|
19004
|
+
hasRenderOutput
|
|
19005
|
+
});
|
|
19006
|
+
return hasRenderOutput;
|
|
19007
|
+
};
|
|
18860
19008
|
//#endregion
|
|
18861
19009
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18862
19010
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19361,8 +19509,8 @@ const CONTEXT_MODULES = [
|
|
|
19361
19509
|
];
|
|
19362
19510
|
const isCreateContextCallee = (callee) => {
|
|
19363
19511
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19364
|
-
|
|
19365
|
-
return
|
|
19512
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19513
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19366
19514
|
}
|
|
19367
19515
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19368
19516
|
const namespaceIdentifier = callee.object;
|
|
@@ -19372,8 +19520,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19372
19520
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19373
19521
|
const namespaceName = namespaceIdentifier.name;
|
|
19374
19522
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19375
|
-
|
|
19376
|
-
return
|
|
19523
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19524
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19377
19525
|
}
|
|
19378
19526
|
return false;
|
|
19379
19527
|
};
|
|
@@ -19775,38 +19923,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19775
19923
|
};
|
|
19776
19924
|
const parseColorToRgb = (value) => {
|
|
19777
19925
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19926
|
+
if (trimmed.startsWith("#")) {
|
|
19927
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
19928
|
+
if (hex8Match) return {
|
|
19929
|
+
red: parseInt(hex8Match[1], 16),
|
|
19930
|
+
green: parseInt(hex8Match[2], 16),
|
|
19931
|
+
blue: parseInt(hex8Match[3], 16)
|
|
19932
|
+
};
|
|
19933
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
19934
|
+
if (hex6Match) return {
|
|
19935
|
+
red: parseInt(hex6Match[1], 16),
|
|
19936
|
+
green: parseInt(hex6Match[2], 16),
|
|
19937
|
+
blue: parseInt(hex6Match[3], 16)
|
|
19938
|
+
};
|
|
19939
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
19940
|
+
if (hex4Match) return {
|
|
19941
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
19942
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
19943
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
19944
|
+
};
|
|
19945
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
19946
|
+
if (hex3Match) return {
|
|
19947
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
19948
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
19949
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
19950
|
+
};
|
|
19951
|
+
}
|
|
19952
|
+
if (trimmed.includes("rgb")) {
|
|
19953
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
19954
|
+
if (rgbMatch) return {
|
|
19955
|
+
red: parseInt(rgbMatch[1], 10),
|
|
19956
|
+
green: parseInt(rgbMatch[2], 10),
|
|
19957
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
19958
|
+
};
|
|
19959
|
+
}
|
|
19960
|
+
if (trimmed.includes("hsl")) {
|
|
19961
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19962
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19963
|
+
}
|
|
19810
19964
|
return null;
|
|
19811
19965
|
};
|
|
19812
19966
|
//#endregion
|
|
@@ -19834,9 +19988,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19834
19988
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19835
19989
|
return null;
|
|
19836
19990
|
};
|
|
19991
|
+
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
19992
|
+
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
19993
|
+
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
19994
|
+
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19837
19995
|
const parseShadowLayerBlur = (layer) => {
|
|
19838
|
-
const
|
|
19839
|
-
|
|
19996
|
+
const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
|
|
19997
|
+
let tokenIndex = 0;
|
|
19998
|
+
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
19999
|
+
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
20000
|
+
tokenIndex += 1;
|
|
20001
|
+
}
|
|
20002
|
+
return 0;
|
|
19840
20003
|
};
|
|
19841
20004
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19842
20005
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19956,7 +20119,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19956
20119
|
};
|
|
19957
20120
|
//#endregion
|
|
19958
20121
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20122
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19959
20123
|
const collectComponentPropNames = (componentFunction) => {
|
|
20124
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20125
|
+
if (cached) return cached;
|
|
19960
20126
|
const propNames = /* @__PURE__ */ new Set();
|
|
19961
20127
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19962
20128
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19970,27 +20136,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19970
20136
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19971
20137
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19972
20138
|
});
|
|
20139
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19973
20140
|
return propNames;
|
|
19974
20141
|
};
|
|
19975
|
-
const
|
|
20142
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20143
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20144
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20145
|
+
if (cached) return cached;
|
|
19976
20146
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19977
20147
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19978
|
-
if (boundNames.has(bindingName)) return true;
|
|
19979
|
-
let declaresName = false;
|
|
19980
20148
|
walkAst(functionNode, (child) => {
|
|
19981
|
-
if (declaresName) return false;
|
|
19982
20149
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
19983
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
19984
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
19985
|
-
collectPatternNames(child.id, declaratorNames);
|
|
19986
|
-
if (declaratorNames.has(bindingName)) {
|
|
19987
|
-
declaresName = true;
|
|
19988
|
-
return false;
|
|
19989
|
-
}
|
|
19990
|
-
}
|
|
20150
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
19991
20151
|
});
|
|
19992
|
-
|
|
20152
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20153
|
+
return boundNames;
|
|
19993
20154
|
};
|
|
20155
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
19994
20156
|
const isPropertyNamePosition = (identifier) => {
|
|
19995
20157
|
const parent = identifier.parent;
|
|
19996
20158
|
if (!parent) return false;
|
|
@@ -20413,36 +20575,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20413
20575
|
}
|
|
20414
20576
|
return hasNestedFunction;
|
|
20415
20577
|
};
|
|
20416
|
-
const
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
if (isReseeded) return false;
|
|
20424
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20425
|
-
isReseeded = true;
|
|
20426
|
-
return false;
|
|
20427
|
-
}
|
|
20428
|
-
});
|
|
20429
|
-
return isReseeded;
|
|
20578
|
+
const isInRenderScope = (node, componentFunction) => {
|
|
20579
|
+
let cursor = node.parent ?? null;
|
|
20580
|
+
while (cursor && cursor !== componentFunction) {
|
|
20581
|
+
if (isFunctionLike$1(cursor)) return false;
|
|
20582
|
+
cursor = cursor.parent ?? null;
|
|
20583
|
+
}
|
|
20584
|
+
return true;
|
|
20430
20585
|
};
|
|
20431
|
-
const
|
|
20586
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20432
20587
|
const setterName = getStateSetterName(useStateCall);
|
|
20433
20588
|
if (!setterName) return false;
|
|
20434
20589
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20435
20590
|
if (!componentFunction) return false;
|
|
20436
|
-
let
|
|
20591
|
+
let isExempt = false;
|
|
20437
20592
|
walkAst(componentFunction, (child) => {
|
|
20438
|
-
if (
|
|
20439
|
-
if (child
|
|
20440
|
-
|
|
20441
|
-
isAdjusted = true;
|
|
20593
|
+
if (isExempt) return false;
|
|
20594
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
|
|
20595
|
+
isExempt = true;
|
|
20442
20596
|
return false;
|
|
20443
20597
|
}
|
|
20444
20598
|
});
|
|
20445
|
-
return
|
|
20599
|
+
return isExempt;
|
|
20446
20600
|
};
|
|
20447
20601
|
const noDerivedUseState = defineRule({
|
|
20448
20602
|
id: "no-derived-useState",
|
|
@@ -20459,8 +20613,7 @@ const noDerivedUseState = defineRule({
|
|
|
20459
20613
|
const initializer = node.arguments[0];
|
|
20460
20614
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20461
20615
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20462
|
-
if (
|
|
20463
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20616
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20464
20617
|
context.report({
|
|
20465
20618
|
node,
|
|
20466
20619
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20471,8 +20624,7 @@ const noDerivedUseState = defineRule({
|
|
|
20471
20624
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20472
20625
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20473
20626
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20474
|
-
if (
|
|
20475
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20627
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20476
20628
|
context.report({
|
|
20477
20629
|
node,
|
|
20478
20630
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22171,20 +22323,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22171
22323
|
return null;
|
|
22172
22324
|
};
|
|
22173
22325
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22326
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22327
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22328
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22329
|
+
walkAst(componentBody, (child) => {
|
|
22330
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22331
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22332
|
+
const setterName = child.callee.name;
|
|
22333
|
+
if (!setterNames.has(setterName)) return;
|
|
22334
|
+
settersWithAnyCall.add(setterName);
|
|
22335
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22336
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22337
|
+
});
|
|
22174
22338
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22175
|
-
for (const binding of useStateBindings)
|
|
22176
|
-
let didFindAnySetterCall = false;
|
|
22177
|
-
let areAllSetterCallsInHandlers = true;
|
|
22178
|
-
walkAst(componentBody, (child) => {
|
|
22179
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22180
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22181
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22182
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22183
|
-
didFindAnySetterCall = true;
|
|
22184
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22185
|
-
});
|
|
22186
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22187
|
-
}
|
|
22339
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22188
22340
|
return handlerOnlyWriteStateNames;
|
|
22189
22341
|
};
|
|
22190
22342
|
const noEventTriggerState = defineRule({
|
|
@@ -22524,6 +22676,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22524
22676
|
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)-)/;
|
|
22525
22677
|
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)-)/;
|
|
22526
22678
|
const splitVariantScope = (token) => {
|
|
22679
|
+
if (!token.includes(":")) return {
|
|
22680
|
+
scope: "",
|
|
22681
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22682
|
+
};
|
|
22527
22683
|
const segments = token.split(":");
|
|
22528
22684
|
const rawUtility = segments[segments.length - 1];
|
|
22529
22685
|
return {
|
|
@@ -22547,6 +22703,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22547
22703
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22548
22704
|
for (const token of classStr.split(/\s+/)) {
|
|
22549
22705
|
if (!token) continue;
|
|
22706
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22550
22707
|
const { scope, utility } = splitVariantScope(token);
|
|
22551
22708
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22552
22709
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23276,6 +23433,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23276
23433
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23277
23434
|
});
|
|
23278
23435
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23436
|
+
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23437
|
+
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23279
23438
|
const noLongTransitionDuration = defineRule({
|
|
23280
23439
|
id: "no-long-transition-duration",
|
|
23281
23440
|
title: "Transition duration too long",
|
|
@@ -23299,11 +23458,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23299
23458
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23300
23459
|
let longestDurationPropertyMs = 0;
|
|
23301
23460
|
for (const segment of value.split(",")) {
|
|
23302
|
-
const
|
|
23303
|
-
|
|
23304
|
-
const
|
|
23305
|
-
|
|
23306
|
-
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23461
|
+
const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
|
|
23462
|
+
if (!durationMatch) continue;
|
|
23463
|
+
const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
|
|
23464
|
+
longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
|
|
23307
23465
|
}
|
|
23308
23466
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23309
23467
|
}
|
|
@@ -23311,7 +23469,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23311
23469
|
let longestDurationMs = 0;
|
|
23312
23470
|
for (const segment of value.split(",")) {
|
|
23313
23471
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23314
|
-
const firstTimeMatch = segment.match(
|
|
23472
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23315
23473
|
if (!firstTimeMatch) continue;
|
|
23316
23474
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23317
23475
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24483,14 +24641,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24483
24641
|
out.hasOpaqueBranch = true;
|
|
24484
24642
|
};
|
|
24485
24643
|
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.`;
|
|
24486
|
-
const
|
|
24644
|
+
const INTERACTIVE_HANDLERS_LOWER = new Set([
|
|
24487
24645
|
"onClick",
|
|
24488
24646
|
"onMouseDown",
|
|
24489
24647
|
"onMouseUp",
|
|
24490
24648
|
"onKeyDown",
|
|
24491
24649
|
"onKeyPress",
|
|
24492
24650
|
"onKeyUp"
|
|
24493
|
-
];
|
|
24651
|
+
].map((handlerName) => handlerName.toLowerCase()));
|
|
24494
24652
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24495
24653
|
id: "no-noninteractive-element-interactions",
|
|
24496
24654
|
title: "Handler on non-interactive element",
|
|
@@ -24505,7 +24663,16 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24505
24663
|
const tag = getElementType(node, context.settings);
|
|
24506
24664
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24507
24665
|
if (tag === "label") return;
|
|
24508
|
-
|
|
24666
|
+
let hasHandler = false;
|
|
24667
|
+
for (const attribute of node.attributes) {
|
|
24668
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24669
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
24670
|
+
if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
24671
|
+
hasHandler = true;
|
|
24672
|
+
break;
|
|
24673
|
+
}
|
|
24674
|
+
}
|
|
24675
|
+
if (!hasHandler) return;
|
|
24509
24676
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24510
24677
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24511
24678
|
if (roleAttr) {
|
|
@@ -25818,16 +25985,22 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25818
25985
|
["tr", "row"],
|
|
25819
25986
|
["ul", "list"]
|
|
25820
25987
|
];
|
|
25821
|
-
const
|
|
25822
|
-
|
|
25823
|
-
|
|
25824
|
-
|
|
25825
|
-
|
|
25826
|
-
const
|
|
25827
|
-
|
|
25828
|
-
|
|
25829
|
-
|
|
25988
|
+
const EMPTY_ROLE_LIST = [];
|
|
25989
|
+
const buildLookup = (pairs, keyIndex) => {
|
|
25990
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
25991
|
+
for (const pair of pairs) {
|
|
25992
|
+
const key = pair[keyIndex];
|
|
25993
|
+
const value = pair[keyIndex === 0 ? 1 : 0];
|
|
25994
|
+
const values = lookup.get(key);
|
|
25995
|
+
if (!values) lookup.set(key, [value]);
|
|
25996
|
+
else if (!values.includes(value)) values.push(value);
|
|
25997
|
+
}
|
|
25998
|
+
return lookup;
|
|
25830
25999
|
};
|
|
26000
|
+
const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
|
|
26001
|
+
const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
|
|
26002
|
+
const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
|
|
26003
|
+
const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
|
|
25831
26004
|
//#endregion
|
|
25832
26005
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
25833
26006
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26069,14 +26242,14 @@ const noRenderInRender = defineRule({
|
|
|
26069
26242
|
const expression = node.expression;
|
|
26070
26243
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26071
26244
|
let calleeName = null;
|
|
26245
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26246
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26247
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26072
26248
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26073
26249
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26074
|
-
|
|
26075
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26250
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26076
26251
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26077
|
-
calleeName = expression.callee.property.name;
|
|
26078
26252
|
}
|
|
26079
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26080
26253
|
context.report({
|
|
26081
26254
|
node: expression,
|
|
26082
26255
|
message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
|
|
@@ -26327,6 +26500,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26327
26500
|
"headers",
|
|
26328
26501
|
"remountDeps"
|
|
26329
26502
|
];
|
|
26503
|
+
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26330
26504
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26331
26505
|
"createFileRoute",
|
|
26332
26506
|
"createRoute",
|
|
@@ -26342,6 +26516,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26342
26516
|
"server",
|
|
26343
26517
|
"handler"
|
|
26344
26518
|
];
|
|
26519
|
+
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26345
26520
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26346
26521
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26347
26522
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27046,14 +27221,21 @@ const noStaticElementInteractions = defineRule({
|
|
|
27046
27221
|
category: "Accessibility",
|
|
27047
27222
|
create: (context) => {
|
|
27048
27223
|
const settings = resolveSettings$12(context.settings);
|
|
27224
|
+
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27049
27225
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27050
27226
|
return { JSXOpeningElement(node) {
|
|
27051
27227
|
if (isTestlikeFile) return;
|
|
27052
27228
|
let hasNonBlockerHandler = false;
|
|
27053
27229
|
let hasAnyHandler = false;
|
|
27054
|
-
|
|
27055
|
-
|
|
27056
|
-
if (!attribute) continue;
|
|
27230
|
+
let seenHandlerNames = null;
|
|
27231
|
+
for (const attribute of node.attributes) {
|
|
27232
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
27233
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
27234
|
+
if (!attributeName) continue;
|
|
27235
|
+
const handlerNameLower = attributeName.toLowerCase();
|
|
27236
|
+
if (!handlersLower.has(handlerNameLower)) continue;
|
|
27237
|
+
if (seenHandlerNames?.has(handlerNameLower)) continue;
|
|
27238
|
+
(seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
27057
27239
|
if (isNullValue(attribute)) continue;
|
|
27058
27240
|
hasAnyHandler = true;
|
|
27059
27241
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -28234,13 +28416,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28234
28416
|
["xml:lang", "xmlLang"],
|
|
28235
28417
|
["xml:space", "xmlSpace"]
|
|
28236
28418
|
]);
|
|
28237
|
-
const
|
|
28419
|
+
const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
|
|
28238
28420
|
"charset",
|
|
28239
28421
|
"allowFullScreen",
|
|
28240
28422
|
"webkitAllowFullScreen",
|
|
28241
28423
|
"mozAllowFullScreen",
|
|
28242
28424
|
"webkitDirectory"
|
|
28243
|
-
];
|
|
28425
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
28244
28426
|
//#endregion
|
|
28245
28427
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28246
28428
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28444,10 +28626,7 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28444
28626
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28445
28627
|
return !tagName.includes("-");
|
|
28446
28628
|
};
|
|
28447
|
-
const normalizeAttributeCase = (name) =>
|
|
28448
|
-
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28449
|
-
return name;
|
|
28450
|
-
};
|
|
28629
|
+
const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
|
|
28451
28630
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28452
28631
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28453
28632
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28791,32 +28970,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28791
28970
|
if (!isHocCallee$1(parent)) return false;
|
|
28792
28971
|
return parent.arguments[0] === node;
|
|
28793
28972
|
};
|
|
28973
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
28974
|
+
"map",
|
|
28975
|
+
"forEach",
|
|
28976
|
+
"filter",
|
|
28977
|
+
"flatMap",
|
|
28978
|
+
"reduce",
|
|
28979
|
+
"reduceRight"
|
|
28980
|
+
]);
|
|
28794
28981
|
const isReturnOfMapCallback = (node) => {
|
|
28795
28982
|
const parent = node.parent;
|
|
28796
28983
|
if (!parent) return false;
|
|
28797
28984
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28798
28985
|
const callee = parent.callee;
|
|
28799
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28800
|
-
"map",
|
|
28801
|
-
"forEach",
|
|
28802
|
-
"filter",
|
|
28803
|
-
"flatMap",
|
|
28804
|
-
"reduce",
|
|
28805
|
-
"reduceRight"
|
|
28806
|
-
].includes(callee.property.name);
|
|
28986
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28807
28987
|
}
|
|
28808
28988
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28809
28989
|
const callbackParent = parent.parent;
|
|
28810
28990
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28811
28991
|
const callee = callbackParent.callee;
|
|
28812
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28813
|
-
"map",
|
|
28814
|
-
"forEach",
|
|
28815
|
-
"filter",
|
|
28816
|
-
"flatMap",
|
|
28817
|
-
"reduce",
|
|
28818
|
-
"reduceRight"
|
|
28819
|
-
].includes(callee.property.name);
|
|
28992
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28820
28993
|
}
|
|
28821
28994
|
}
|
|
28822
28995
|
return false;
|
|
@@ -28852,10 +29025,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28852
29025
|
valueNode = parent;
|
|
28853
29026
|
parent = parent.parent;
|
|
28854
29027
|
continue;
|
|
28855
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
29028
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
|
|
28856
29029
|
case "AssignmentExpression": {
|
|
28857
29030
|
const assignmentTarget = parent.left;
|
|
28858
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
29031
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28859
29032
|
}
|
|
28860
29033
|
case "Property":
|
|
28861
29034
|
if (parent.value !== valueNode) return false;
|
|
@@ -28948,14 +29121,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28948
29121
|
};
|
|
28949
29122
|
return {
|
|
28950
29123
|
JSXOpeningElement(node) {
|
|
28951
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29124
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28952
29125
|
recordInstantiation(node.name, node.name.name);
|
|
28953
29126
|
return;
|
|
28954
29127
|
}
|
|
28955
29128
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28956
29129
|
},
|
|
28957
29130
|
Identifier(node) {
|
|
28958
|
-
if (!
|
|
29131
|
+
if (!isReactComponentName(node.name)) return;
|
|
28959
29132
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28960
29133
|
recordInstantiation(node, node.name);
|
|
28961
29134
|
},
|
|
@@ -29728,10 +29901,10 @@ const onlyExportComponents = defineRule({
|
|
|
29728
29901
|
};
|
|
29729
29902
|
for (const child of componentCandidates) {
|
|
29730
29903
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
29731
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
|
|
29904
|
+
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29732
29905
|
}
|
|
29733
29906
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
29734
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child)) localComponents.push(child.id);
|
|
29907
|
+
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29735
29908
|
}
|
|
29736
29909
|
}
|
|
29737
29910
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
@@ -30765,13 +30938,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30765
30938
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30766
30939
|
},
|
|
30767
30940
|
JSXAttribute(node) {
|
|
30768
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30769
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30770
30941
|
if (!node.value) return;
|
|
30771
30942
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30772
30943
|
const innerExpression = node.value.expression;
|
|
30773
30944
|
if (!innerExpression) return;
|
|
30774
30945
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
30946
|
+
if (!isInsideFunctionScope(node)) return;
|
|
30947
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30775
30948
|
const parentJsxOpening = node.parent;
|
|
30776
30949
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30777
30950
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -32143,41 +32316,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32143
32316
|
});
|
|
32144
32317
|
return found;
|
|
32145
32318
|
};
|
|
32146
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32147
|
-
if (!componentBody) return false;
|
|
32148
|
-
let found = false;
|
|
32149
|
-
walkAst(componentBody, (child) => {
|
|
32150
|
-
if (found) return;
|
|
32151
|
-
if (!isFunctionLike$1(child)) return;
|
|
32152
|
-
const functionBody = child.body;
|
|
32153
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32154
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32155
|
-
});
|
|
32156
|
-
return found;
|
|
32157
|
-
};
|
|
32158
32319
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32159
32320
|
"then",
|
|
32160
32321
|
"catch",
|
|
32161
32322
|
"finally"
|
|
32162
32323
|
]);
|
|
32163
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32164
|
-
if (!componentBody) return false;
|
|
32165
|
-
let found = false;
|
|
32166
|
-
walkAst(componentBody, (child) => {
|
|
32167
|
-
if (found) return;
|
|
32168
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32169
|
-
const callee = child.callee;
|
|
32170
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32171
|
-
for (const argument of child.arguments ?? []) {
|
|
32172
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32173
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32174
|
-
found = true;
|
|
32175
|
-
return;
|
|
32176
|
-
}
|
|
32177
|
-
}
|
|
32178
|
-
});
|
|
32179
|
-
return found;
|
|
32180
|
-
};
|
|
32181
32324
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32182
32325
|
"useApolloClient",
|
|
32183
32326
|
"useMutation",
|
|
@@ -32190,20 +32333,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32190
32333
|
"fetch",
|
|
32191
32334
|
"axios"
|
|
32192
32335
|
]);
|
|
32193
|
-
const
|
|
32194
|
-
if (!body) return false;
|
|
32336
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32195
32337
|
let found = false;
|
|
32196
|
-
walkAst(
|
|
32197
|
-
if (found) return;
|
|
32338
|
+
walkAst(fnBody, (child) => {
|
|
32339
|
+
if (found) return false;
|
|
32198
32340
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32199
32341
|
const callee = child.callee;
|
|
32200
32342
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32201
32343
|
found = true;
|
|
32202
|
-
return;
|
|
32344
|
+
return false;
|
|
32345
|
+
}
|
|
32346
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32347
|
+
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32348
|
+
found = true;
|
|
32349
|
+
return false;
|
|
32350
|
+
}
|
|
32351
|
+
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32352
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32353
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32354
|
+
found = true;
|
|
32355
|
+
return false;
|
|
32356
|
+
}
|
|
32357
|
+
}
|
|
32203
32358
|
}
|
|
32204
|
-
|
|
32359
|
+
return;
|
|
32360
|
+
}
|
|
32361
|
+
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32362
|
+
const functionBody = child.body;
|
|
32363
|
+
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32205
32364
|
found = true;
|
|
32206
|
-
return;
|
|
32365
|
+
return false;
|
|
32207
32366
|
}
|
|
32208
32367
|
}
|
|
32209
32368
|
});
|
|
@@ -32228,7 +32387,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32228
32387
|
const secondBinding = node.id.elements[1];
|
|
32229
32388
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32230
32389
|
const fnBody = enclosingFunctionBody(node);
|
|
32231
|
-
if (fnBody && (
|
|
32390
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32232
32391
|
context.report({
|
|
32233
32392
|
node: node.init,
|
|
32234
32393
|
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`
|
|
@@ -33022,17 +33181,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33022
33181
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33023
33182
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33024
33183
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33184
|
+
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33185
|
+
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33186
|
+
walkAst(componentBody, (child) => {
|
|
33187
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33188
|
+
});
|
|
33025
33189
|
for (const binding of bindings) {
|
|
33026
33190
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33027
33191
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33028
33192
|
const setterSuffix = binding.setterName.slice(3);
|
|
33029
33193
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33030
|
-
|
|
33031
|
-
walkAst(componentBody, (child) => {
|
|
33032
|
-
if (setterCalled) return;
|
|
33033
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33034
|
-
});
|
|
33035
|
-
if (!setterCalled) continue;
|
|
33194
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33036
33195
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33037
33196
|
context.report({
|
|
33038
33197
|
node: binding.declarator,
|
|
@@ -33364,6 +33523,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33364
33523
|
FlashList: ["@shopify/flash-list"],
|
|
33365
33524
|
LegendList: ["@legendapp/list"]
|
|
33366
33525
|
};
|
|
33526
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33367
33527
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33368
33528
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33369
33529
|
"renderItem",
|
|
@@ -33606,33 +33766,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33606
33766
|
requires: ["react-native"],
|
|
33607
33767
|
severity: "warn",
|
|
33608
33768
|
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.",
|
|
33609
|
-
create: (context) =>
|
|
33610
|
-
|
|
33611
|
-
|
|
33612
|
-
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
|
|
33623
|
-
|
|
33624
|
-
hasDataProp =
|
|
33625
|
-
|
|
33769
|
+
create: (context) => {
|
|
33770
|
+
let fileImportsRecycler = false;
|
|
33771
|
+
return {
|
|
33772
|
+
Program(node) {
|
|
33773
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33774
|
+
},
|
|
33775
|
+
JSXOpeningElement(node) {
|
|
33776
|
+
if (!fileImportsRecycler) return;
|
|
33777
|
+
const localElementName = resolveJsxElementName(node);
|
|
33778
|
+
if (!localElementName) return;
|
|
33779
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
33780
|
+
if (canonicalRecyclerName === null) return;
|
|
33781
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
33782
|
+
let hasSizingHint = false;
|
|
33783
|
+
let dataIsEmptyLiteral = false;
|
|
33784
|
+
let hasDataProp = false;
|
|
33785
|
+
for (const attribute of node.attributes ?? []) {
|
|
33786
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
33787
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
33788
|
+
const attributeName = attribute.name.name;
|
|
33789
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
33790
|
+
if (attributeName === "data") {
|
|
33791
|
+
hasDataProp = true;
|
|
33792
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
33793
|
+
}
|
|
33794
|
+
}
|
|
33795
|
+
if (hasSizingHint) return;
|
|
33796
|
+
if (dataIsEmptyLiteral) return;
|
|
33797
|
+
if (!hasDataProp) return;
|
|
33798
|
+
context.report({
|
|
33799
|
+
node,
|
|
33800
|
+
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33801
|
+
});
|
|
33626
33802
|
}
|
|
33627
|
-
}
|
|
33628
|
-
|
|
33629
|
-
if (dataIsEmptyLiteral) return;
|
|
33630
|
-
if (!hasDataProp) return;
|
|
33631
|
-
context.report({
|
|
33632
|
-
node,
|
|
33633
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33634
|
-
});
|
|
33635
|
-
} })
|
|
33803
|
+
};
|
|
33804
|
+
}
|
|
33636
33805
|
});
|
|
33637
33806
|
//#endregion
|
|
33638
33807
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33643,25 +33812,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33643
33812
|
requires: ["react-native"],
|
|
33644
33813
|
severity: "warn",
|
|
33645
33814
|
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.",
|
|
33646
|
-
create: (context) =>
|
|
33647
|
-
|
|
33648
|
-
|
|
33649
|
-
|
|
33650
|
-
|
|
33651
|
-
|
|
33652
|
-
|
|
33653
|
-
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
33663
|
-
|
|
33664
|
-
|
|
33815
|
+
create: (context) => {
|
|
33816
|
+
let fileImportsRecycler = false;
|
|
33817
|
+
return {
|
|
33818
|
+
Program(node) {
|
|
33819
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33820
|
+
},
|
|
33821
|
+
JSXOpeningElement(node) {
|
|
33822
|
+
if (!fileImportsRecycler) return;
|
|
33823
|
+
const elementName = resolveJsxElementName(node);
|
|
33824
|
+
if (!elementName) return;
|
|
33825
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
33826
|
+
let hasRecycleItemsEnabled = false;
|
|
33827
|
+
let hasGetItemType = false;
|
|
33828
|
+
for (const attr of node.attributes ?? []) {
|
|
33829
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
33830
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
33831
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
33832
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
33833
|
+
else hasRecycleItemsEnabled = true;
|
|
33834
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
33835
|
+
}
|
|
33836
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
33837
|
+
node,
|
|
33838
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
33839
|
+
});
|
|
33840
|
+
}
|
|
33841
|
+
};
|
|
33842
|
+
}
|
|
33665
33843
|
});
|
|
33666
33844
|
//#endregion
|
|
33667
33845
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -34645,9 +34823,10 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34645
34823
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34646
34824
|
return resolveJsxElementName(openingElement);
|
|
34647
34825
|
};
|
|
34826
|
+
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34648
34827
|
const isTextHandlingComponent = (elementName) => {
|
|
34649
34828
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34650
|
-
return
|
|
34829
|
+
return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
|
|
34651
34830
|
};
|
|
34652
34831
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34653
34832
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34691,6 +34870,7 @@ const rnNoRawText = defineRule({
|
|
|
34691
34870
|
return {
|
|
34692
34871
|
Program(programNode) {
|
|
34693
34872
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
34873
|
+
if (!containsJsxElement(programNode)) return;
|
|
34694
34874
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34695
34875
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34696
34876
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38645,14 +38825,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38645
38825
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38646
38826
|
category: "Accessibility",
|
|
38647
38827
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38648
|
-
|
|
38649
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38650
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38651
|
-
if (!role) return;
|
|
38652
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38653
|
-
const isImplicit = !roleAttribute;
|
|
38654
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38655
|
-
if (!supported) return;
|
|
38828
|
+
let ariaAttributes = null;
|
|
38656
38829
|
for (const attribute of node.attributes) {
|
|
38657
38830
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38658
38831
|
const attributeNode = attribute;
|
|
@@ -38662,6 +38835,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38662
38835
|
const propName = propRawName.toLowerCase();
|
|
38663
38836
|
if (!propName.startsWith("aria-")) continue;
|
|
38664
38837
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
38838
|
+
(ariaAttributes ??= []).push({
|
|
38839
|
+
attribute,
|
|
38840
|
+
propName
|
|
38841
|
+
});
|
|
38842
|
+
}
|
|
38843
|
+
if (!ariaAttributes) return;
|
|
38844
|
+
const elementType = getElementType(node, context.settings);
|
|
38845
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38846
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38847
|
+
if (!role) return;
|
|
38848
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38849
|
+
const isImplicit = !roleAttribute;
|
|
38850
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38851
|
+
if (!supported) return;
|
|
38852
|
+
for (const { attribute, propName } of ariaAttributes) {
|
|
38665
38853
|
if (supported.has(propName)) continue;
|
|
38666
38854
|
context.report({
|
|
38667
38855
|
node: attribute,
|
|
@@ -39499,6 +39687,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39499
39687
|
cachedFunctionNames.add(node.id.name);
|
|
39500
39688
|
},
|
|
39501
39689
|
CallExpression(node) {
|
|
39690
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39502
39691
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39503
39692
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39504
39693
|
const firstArg = node.arguments?.[0];
|
|
@@ -40425,7 +40614,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40425
40614
|
//#endregion
|
|
40426
40615
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40427
40616
|
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;
|
|
40428
|
-
const
|
|
40617
|
+
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;
|
|
40618
|
+
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40619
|
+
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40620
|
+
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40621
|
+
const tableName = match[1];
|
|
40622
|
+
if (tableName === void 0) continue;
|
|
40623
|
+
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40624
|
+
}
|
|
40625
|
+
return lastEnableIndexByTable;
|
|
40626
|
+
};
|
|
40429
40627
|
const supabaseTableMissingRls = defineRule({
|
|
40430
40628
|
id: "supabase-table-missing-rls",
|
|
40431
40629
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40436,11 +40634,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40436
40634
|
const content = sanitizeSqlForScan(file.content);
|
|
40437
40635
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40438
40636
|
const findings = [];
|
|
40637
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40439
40638
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40440
40639
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40441
40640
|
const tableName = match[1];
|
|
40442
40641
|
if (tableName === void 0) continue;
|
|
40443
|
-
|
|
40642
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40643
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40444
40644
|
const location = getLocationAtIndex(content, match.index);
|
|
40445
40645
|
findings.push({
|
|
40446
40646
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40716,6 +40916,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40716
40916
|
severity: "warn",
|
|
40717
40917
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40718
40918
|
create: (context) => {
|
|
40919
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40719
40920
|
let hasHeadContentElement = false;
|
|
40720
40921
|
let hasDocumentHeadElement = false;
|
|
40721
40922
|
let hasCustomHeadChildElement = false;
|
|
@@ -40752,8 +40953,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40752
40953
|
};
|
|
40753
40954
|
return {
|
|
40754
40955
|
Program(node) {
|
|
40755
|
-
const filename = context.filename ?? "";
|
|
40756
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40757
40956
|
const statements = node.body ?? [];
|
|
40758
40957
|
for (const statement of statements) collectImportBindings(statement);
|
|
40759
40958
|
for (const statement of statements) {
|
|
@@ -40762,18 +40961,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40762
40961
|
}
|
|
40763
40962
|
},
|
|
40764
40963
|
ImportDeclaration(node) {
|
|
40765
|
-
const filename = context.filename ?? "";
|
|
40766
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40767
40964
|
collectImportBindings(node);
|
|
40768
40965
|
},
|
|
40769
40966
|
VariableDeclarator(node) {
|
|
40770
|
-
const filename = context.filename ?? "";
|
|
40771
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40772
40967
|
collectVariableAlias(node);
|
|
40773
40968
|
},
|
|
40774
40969
|
JSXOpeningElement(node) {
|
|
40775
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40776
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40777
40970
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40778
40971
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40779
40972
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40787,8 +40980,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40787
40980
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40788
40981
|
},
|
|
40789
40982
|
"Program:exit"(programNode) {
|
|
40790
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40791
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40792
40983
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40793
40984
|
node: programNode,
|
|
40794
40985
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40812,27 +41003,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40812
41003
|
requires: ["tanstack-start"],
|
|
40813
41004
|
severity: "warn",
|
|
40814
41005
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40815
|
-
create: (context) =>
|
|
40816
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40817
|
-
|
|
40818
|
-
|
|
40819
|
-
|
|
40820
|
-
|
|
40821
|
-
|
|
40822
|
-
|
|
40823
|
-
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
40830
|
-
|
|
40831
|
-
|
|
40832
|
-
|
|
40833
|
-
|
|
40834
|
-
|
|
40835
|
-
|
|
41006
|
+
create: (context) => {
|
|
41007
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41008
|
+
return { JSXOpeningElement(node) {
|
|
41009
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
41010
|
+
const attributes = node.attributes ?? [];
|
|
41011
|
+
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
41012
|
+
if (!hrefAttribute?.value) return;
|
|
41013
|
+
let hrefValue = null;
|
|
41014
|
+
if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
|
|
41015
|
+
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
41016
|
+
if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
|
|
41017
|
+
if (hrefValue.startsWith("//")) return;
|
|
41018
|
+
const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
|
|
41019
|
+
if (pathname.startsWith("/api/")) return;
|
|
41020
|
+
if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
|
|
41021
|
+
if (findJsxAttribute(attributes, "download")) return;
|
|
41022
|
+
if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
|
|
41023
|
+
context.report({
|
|
41024
|
+
node,
|
|
41025
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
41026
|
+
});
|
|
41027
|
+
} };
|
|
41028
|
+
}
|
|
40836
41029
|
});
|
|
40837
41030
|
//#endregion
|
|
40838
41031
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40896,7 +41089,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40896
41089
|
severity: "warn",
|
|
40897
41090
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40898
41091
|
create: (context) => {
|
|
40899
|
-
|
|
41092
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40900
41093
|
let deferredCallbackDepth = 0;
|
|
40901
41094
|
let eventHandlerDepth = 0;
|
|
40902
41095
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40960,7 +41153,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40960
41153
|
};
|
|
40961
41154
|
return {
|
|
40962
41155
|
CallExpression(node) {
|
|
40963
|
-
if (!isRouteFile) return;
|
|
40964
41156
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40965
41157
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40966
41158
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40972,39 +41164,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40972
41164
|
}
|
|
40973
41165
|
},
|
|
40974
41166
|
"CallExpression:exit"(node) {
|
|
40975
|
-
if (!isRouteFile) return;
|
|
40976
41167
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40977
41168
|
},
|
|
40978
41169
|
JSXAttribute(node) {
|
|
40979
|
-
if (!isRouteFile) return;
|
|
40980
41170
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40981
41171
|
},
|
|
40982
41172
|
"JSXAttribute:exit"(node) {
|
|
40983
|
-
if (!isRouteFile) return;
|
|
40984
41173
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40985
41174
|
},
|
|
40986
41175
|
Property(node) {
|
|
40987
|
-
if (!isRouteFile) return;
|
|
40988
41176
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40989
41177
|
},
|
|
40990
41178
|
"Property:exit"(node) {
|
|
40991
|
-
if (!isRouteFile) return;
|
|
40992
41179
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40993
41180
|
},
|
|
40994
41181
|
VariableDeclarator(node) {
|
|
40995
|
-
if (!isRouteFile) return;
|
|
40996
41182
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
40997
41183
|
},
|
|
40998
41184
|
"VariableDeclarator:exit"(node) {
|
|
40999
|
-
if (!isRouteFile) return;
|
|
41000
41185
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41001
41186
|
},
|
|
41002
41187
|
FunctionDeclaration(node) {
|
|
41003
|
-
if (!isRouteFile) return;
|
|
41004
41188
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41005
41189
|
},
|
|
41006
41190
|
"FunctionDeclaration:exit"(node) {
|
|
41007
|
-
if (!isRouteFile) return;
|
|
41008
41191
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41009
41192
|
}
|
|
41010
41193
|
};
|
|
@@ -41088,23 +41271,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41088
41271
|
requires: ["tanstack-start"],
|
|
41089
41272
|
severity: "warn",
|
|
41090
41273
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41091
|
-
create: (context) =>
|
|
41092
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41093
|
-
|
|
41094
|
-
|
|
41095
|
-
|
|
41096
|
-
|
|
41097
|
-
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41274
|
+
create: (context) => {
|
|
41275
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41276
|
+
return { CallExpression(node) {
|
|
41277
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41278
|
+
const callback = node.arguments?.[0];
|
|
41279
|
+
if (!callback) return;
|
|
41280
|
+
let hasFetchCall = false;
|
|
41281
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
41282
|
+
walkAst(callback, (child) => {
|
|
41283
|
+
if (hasFetchCall) return false;
|
|
41284
|
+
if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
41285
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
|
|
41286
|
+
});
|
|
41287
|
+
if (hasFetchCall) context.report({
|
|
41288
|
+
node,
|
|
41289
|
+
message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
|
|
41290
|
+
});
|
|
41291
|
+
} };
|
|
41292
|
+
}
|
|
41108
41293
|
});
|
|
41109
41294
|
//#endregion
|
|
41110
41295
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41145,10 +41330,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41145
41330
|
const propertyName = getPropertyKeyName(property);
|
|
41146
41331
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41147
41332
|
}
|
|
41148
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41333
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
|
|
41149
41334
|
let lastIndex = -1;
|
|
41150
41335
|
for (const propertyName of sensitiveProperties) {
|
|
41151
|
-
const currentIndex =
|
|
41336
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
|
|
41152
41337
|
if (currentIndex < lastIndex) {
|
|
41153
41338
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41154
41339
|
context.report({
|
|
@@ -41185,10 +41370,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41185
41370
|
} else return;
|
|
41186
41371
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41187
41372
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41188
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41373
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
|
|
41189
41374
|
let lastIndex = -1;
|
|
41190
41375
|
for (const methodName of orderSensitiveMethods) {
|
|
41191
|
-
const currentIndex =
|
|
41376
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
|
|
41192
41377
|
if (currentIndex < lastIndex) {
|
|
41193
41378
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41194
41379
|
context.report({
|
|
@@ -41472,13 +41657,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41472
41657
|
//#endregion
|
|
41473
41658
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41474
41659
|
const ZOD_MODULE = "zod";
|
|
41660
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41475
41661
|
const getStaticPropertyName = (member) => {
|
|
41476
41662
|
const property = member.property;
|
|
41477
41663
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41478
41664
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41479
41665
|
return null;
|
|
41480
41666
|
};
|
|
41667
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41481
41668
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41669
|
+
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41670
|
+
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41671
|
+
importInfoCache.set(identifier, importInfo);
|
|
41672
|
+
return importInfo;
|
|
41673
|
+
};
|
|
41674
|
+
const computeImportInfoForIdentifier = (identifier) => {
|
|
41482
41675
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41483
41676
|
if (!specifier) return null;
|
|
41484
41677
|
const declaration = specifier.parent;
|
|
@@ -41615,25 +41808,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41615
41808
|
tags: ["migration-hint"],
|
|
41616
41809
|
severity: "warn",
|
|
41617
41810
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41618
|
-
create: (context) =>
|
|
41619
|
-
|
|
41620
|
-
|
|
41621
|
-
|
|
41622
|
-
|
|
41623
|
-
|
|
41624
|
-
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41811
|
+
create: (context) => {
|
|
41812
|
+
let fileImportsZod = false;
|
|
41813
|
+
return {
|
|
41814
|
+
Program(node) {
|
|
41815
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
41816
|
+
},
|
|
41817
|
+
CallExpression(node) {
|
|
41818
|
+
if (!fileImportsZod) return;
|
|
41819
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
41820
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
41821
|
+
context.report({
|
|
41822
|
+
node,
|
|
41823
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41824
|
+
});
|
|
41825
|
+
},
|
|
41826
|
+
MemberExpression(node) {
|
|
41827
|
+
if (!fileImportsZod) return;
|
|
41828
|
+
const parent = node.parent;
|
|
41829
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41830
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
41831
|
+
context.report({
|
|
41832
|
+
node,
|
|
41833
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41834
|
+
});
|
|
41835
|
+
}
|
|
41836
|
+
};
|
|
41837
|
+
}
|
|
41637
41838
|
});
|
|
41638
41839
|
//#endregion
|
|
41639
41840
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41825,16 +42026,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41825
42026
|
tags: ["migration-hint"],
|
|
41826
42027
|
severity: "warn",
|
|
41827
42028
|
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()`.",
|
|
41828
|
-
create: (context) =>
|
|
41829
|
-
|
|
41830
|
-
|
|
41831
|
-
|
|
41832
|
-
|
|
41833
|
-
|
|
41834
|
-
|
|
41835
|
-
|
|
41836
|
-
|
|
41837
|
-
|
|
42029
|
+
create: (context) => {
|
|
42030
|
+
let fileImportsZod = false;
|
|
42031
|
+
return {
|
|
42032
|
+
Program(node) {
|
|
42033
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42034
|
+
},
|
|
42035
|
+
CallExpression(node) {
|
|
42036
|
+
if (!fileImportsZod) return;
|
|
42037
|
+
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);
|
|
42038
|
+
},
|
|
42039
|
+
MemberExpression(node) {
|
|
42040
|
+
if (!fileImportsZod) return;
|
|
42041
|
+
const parent = node.parent;
|
|
42042
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42043
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
42044
|
+
}
|
|
42045
|
+
};
|
|
42046
|
+
}
|
|
41838
42047
|
});
|
|
41839
42048
|
//#endregion
|
|
41840
42049
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41869,13 +42078,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41869
42078
|
tags: ["migration-hint"],
|
|
41870
42079
|
severity: "warn",
|
|
41871
42080
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41872
|
-
create: (context) =>
|
|
41873
|
-
|
|
41874
|
-
|
|
41875
|
-
node
|
|
41876
|
-
|
|
41877
|
-
|
|
41878
|
-
|
|
42081
|
+
create: (context) => {
|
|
42082
|
+
let fileImportsZod = false;
|
|
42083
|
+
return {
|
|
42084
|
+
Program(node) {
|
|
42085
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42086
|
+
},
|
|
42087
|
+
CallExpression(node) {
|
|
42088
|
+
if (!fileImportsZod) return;
|
|
42089
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42090
|
+
context.report({
|
|
42091
|
+
node,
|
|
42092
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42093
|
+
});
|
|
42094
|
+
}
|
|
42095
|
+
};
|
|
42096
|
+
}
|
|
41879
42097
|
});
|
|
41880
42098
|
//#endregion
|
|
41881
42099
|
//#region src/plugin/rule-registry.ts
|