oxlint-plugin-react-doctor 0.6.2-dev.b4faf74 → 0.6.2-dev.da3b19c
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 +750 -539
- 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) {
|
|
@@ -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;
|
|
@@ -14378,14 +14474,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14378
14474
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14379
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`.";
|
|
14380
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.";
|
|
14381
|
-
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14477
|
+
const DEFAULT_CONTROL_COMPONENTS = new Set([
|
|
14382
14478
|
"input",
|
|
14383
14479
|
"meter",
|
|
14384
14480
|
"output",
|
|
14385
14481
|
"progress",
|
|
14386
14482
|
"select",
|
|
14387
14483
|
"textarea"
|
|
14388
|
-
];
|
|
14484
|
+
]);
|
|
14389
14485
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14390
14486
|
"alt",
|
|
14391
14487
|
"aria-label",
|
|
@@ -14397,8 +14493,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14397
14493
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14398
14494
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14399
14495
|
return {
|
|
14400
|
-
labelComponents: ["label", ...ruleSettings.labelComponents ?? []]
|
|
14401
|
-
labelAttributes:
|
|
14496
|
+
labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
|
|
14497
|
+
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14402
14498
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14403
14499
|
assert: ruleSettings.assert ?? "either",
|
|
14404
14500
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14406,7 +14502,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14406
14502
|
};
|
|
14407
14503
|
};
|
|
14408
14504
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14409
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14505
|
+
if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
|
|
14410
14506
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14411
14507
|
};
|
|
14412
14508
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14432,7 +14528,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14432
14528
|
const attributeName = attribute.name;
|
|
14433
14529
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14434
14530
|
const propName = getJsxAttributeName(attributeName);
|
|
14435
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14531
|
+
if (!propName || !searchContext.labelAttributes.has(propName)) continue;
|
|
14436
14532
|
const attributeValue = attribute.value;
|
|
14437
14533
|
if (!attributeValue) continue;
|
|
14438
14534
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14454,7 +14550,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14454
14550
|
const attributeName = attribute.name;
|
|
14455
14551
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14456
14552
|
const propName = getJsxAttributeName(attributeName);
|
|
14457
|
-
if (propName && searchContext.labelAttributes.
|
|
14553
|
+
if (propName && searchContext.labelAttributes.has(propName)) return true;
|
|
14458
14554
|
}
|
|
14459
14555
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14460
14556
|
return false;
|
|
@@ -14477,7 +14573,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14477
14573
|
if (isTestlikeFile) return;
|
|
14478
14574
|
const opening = node.openingElement;
|
|
14479
14575
|
const tagName = getElementType(opening, context.settings);
|
|
14480
|
-
if (!settings.labelComponents.
|
|
14576
|
+
if (!settings.labelComponents.has(tagName)) return;
|
|
14481
14577
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14482
14578
|
const searchContext = {
|
|
14483
14579
|
depth: settings.depth,
|
|
@@ -15066,9 +15162,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15066
15162
|
const reactDoctor = settings?.["react-doctor"];
|
|
15067
15163
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15068
15164
|
return {
|
|
15069
|
-
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15070
|
-
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15071
|
-
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 ?? []])
|
|
15072
15168
|
};
|
|
15073
15169
|
};
|
|
15074
15170
|
const evaluateMuted = (attribute) => {
|
|
@@ -15097,7 +15193,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15097
15193
|
let rendersCaptionTrack = false;
|
|
15098
15194
|
walkAst(expression, (inner) => {
|
|
15099
15195
|
if (rendersCaptionTrack) return false;
|
|
15100
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15196
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15101
15197
|
rendersCaptionTrack = true;
|
|
15102
15198
|
return false;
|
|
15103
15199
|
}
|
|
@@ -15115,7 +15211,7 @@ const mediaHasCaption = defineRule({
|
|
|
15115
15211
|
const settings = resolveSettings$23(context.settings);
|
|
15116
15212
|
return { JSXOpeningElement(node) {
|
|
15117
15213
|
const tag = getElementType(node, context.settings);
|
|
15118
|
-
if (!(settings.audio.
|
|
15214
|
+
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
15119
15215
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15120
15216
|
const parent = node.parent;
|
|
15121
15217
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15130,7 +15226,7 @@ const mediaHasCaption = defineRule({
|
|
|
15130
15226
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15131
15227
|
const opening = child.openingElement;
|
|
15132
15228
|
const childTag = getElementType(opening, context.settings);
|
|
15133
|
-
if (!settings.track.
|
|
15229
|
+
if (!settings.track.has(childTag)) return false;
|
|
15134
15230
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15135
15231
|
if (!kindAttribute) return false;
|
|
15136
15232
|
let kindValue = kindAttribute.value;
|
|
@@ -15441,16 +15537,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15441
15537
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15442
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;
|
|
15443
15539
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15444
|
-
const
|
|
15540
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15445
15541
|
const strippedSource = stripJsComments(sourceText);
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
for (const match of strippedSource.matchAll(
|
|
15449
|
-
|
|
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;
|
|
15450
15550
|
};
|
|
15551
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15451
15552
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15452
15553
|
try {
|
|
15453
|
-
|
|
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);
|
|
15454
15564
|
} catch {
|
|
15455
15565
|
return false;
|
|
15456
15566
|
}
|
|
@@ -15595,8 +15705,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15595
15705
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15596
15706
|
let didFindFetchCall = false;
|
|
15597
15707
|
walkAst(node, (child) => {
|
|
15708
|
+
if (didFindFetchCall) return false;
|
|
15598
15709
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15599
|
-
if (isFetchCall$1(child))
|
|
15710
|
+
if (isFetchCall$1(child)) {
|
|
15711
|
+
didFindFetchCall = true;
|
|
15712
|
+
return false;
|
|
15713
|
+
}
|
|
15600
15714
|
});
|
|
15601
15715
|
return didFindFetchCall;
|
|
15602
15716
|
};
|
|
@@ -15610,6 +15724,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15610
15724
|
severity: "warn",
|
|
15611
15725
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15612
15726
|
create: (context) => {
|
|
15727
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15728
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15613
15729
|
let fileHasUseClient = false;
|
|
15614
15730
|
return {
|
|
15615
15731
|
Program(programNode) {
|
|
@@ -15619,8 +15735,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15619
15735
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15620
15736
|
const callback = getEffectCallback(node);
|
|
15621
15737
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15622
|
-
|
|
15623
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15738
|
+
context.report({
|
|
15624
15739
|
node,
|
|
15625
15740
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15626
15741
|
});
|
|
@@ -16285,16 +16400,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16285
16400
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16286
16401
|
create: (context) => {
|
|
16287
16402
|
let resolveBinding = () => null;
|
|
16403
|
+
let isRouteHandlerFile = false;
|
|
16404
|
+
let mutatingSegment = null;
|
|
16288
16405
|
return {
|
|
16289
16406
|
Program(node) {
|
|
16290
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;
|
|
16291
16411
|
},
|
|
16292
16412
|
ExportNamedDeclaration(node) {
|
|
16293
|
-
|
|
16294
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16295
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16413
|
+
if (!isRouteHandlerFile) return;
|
|
16296
16414
|
if (!isExportedGetHandler(node)) return;
|
|
16297
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16298
16415
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16299
16416
|
for (const handlerBody of handlerBodies) {
|
|
16300
16417
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17326,8 +17443,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17326
17443
|
programToAnalysis.set(programNode, analysis);
|
|
17327
17444
|
return analysis;
|
|
17328
17445
|
};
|
|
17446
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17329
17447
|
const getScopeForNode = (node, manager) => {
|
|
17330
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;
|
|
17331
17455
|
let bestScope = null;
|
|
17332
17456
|
let bestSize = Infinity;
|
|
17333
17457
|
for (const scope of manager.scopes) {
|
|
@@ -17340,6 +17464,7 @@ const getScopeForNode = (node, manager) => {
|
|
|
17340
17464
|
bestScope = scope;
|
|
17341
17465
|
}
|
|
17342
17466
|
}
|
|
17467
|
+
scopeByNode.set(node, bestScope);
|
|
17343
17468
|
return bestScope;
|
|
17344
17469
|
};
|
|
17345
17470
|
//#endregion
|
|
@@ -17374,11 +17499,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17374
17499
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17375
17500
|
}
|
|
17376
17501
|
};
|
|
17502
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17377
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;
|
|
17378
17511
|
const refs = [];
|
|
17379
17512
|
ascend(analysis, ref, (upRef) => {
|
|
17380
17513
|
refs.push(upRef);
|
|
17381
17514
|
});
|
|
17515
|
+
upstreamByRef.set(ref, refs);
|
|
17382
17516
|
return refs;
|
|
17383
17517
|
};
|
|
17384
17518
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17388,11 +17522,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17388
17522
|
});
|
|
17389
17523
|
return nodes;
|
|
17390
17524
|
};
|
|
17525
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17391
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;
|
|
17392
17534
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17393
|
-
if (
|
|
17394
|
-
|
|
17395
|
-
|
|
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;
|
|
17396
17543
|
};
|
|
17397
17544
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17398
17545
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17467,22 +17614,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17467
17614
|
};
|
|
17468
17615
|
//#endregion
|
|
17469
17616
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17470
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17471
|
-
if (!node.range) return null;
|
|
17472
|
-
let best = null;
|
|
17473
|
-
let bestSize = Infinity;
|
|
17474
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17475
|
-
const block = scope.block;
|
|
17476
|
-
if (!block?.range) continue;
|
|
17477
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17478
|
-
const size = block.range[1] - block.range[0];
|
|
17479
|
-
if (size <= bestSize) {
|
|
17480
|
-
bestSize = size;
|
|
17481
|
-
best = scope;
|
|
17482
|
-
}
|
|
17483
|
-
}
|
|
17484
|
-
return best;
|
|
17485
|
-
};
|
|
17486
17617
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17487
17618
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17488
17619
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17513,7 +17644,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17513
17644
|
const isWrappedSeparately = () => {
|
|
17514
17645
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17515
17646
|
const bindingName = node.id.name;
|
|
17516
|
-
const containingScope =
|
|
17647
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17517
17648
|
if (!containingScope) return false;
|
|
17518
17649
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17519
17650
|
if (!variable) return false;
|
|
@@ -18863,7 +18994,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18863
18994
|
}
|
|
18864
18995
|
return false;
|
|
18865
18996
|
};
|
|
18866
|
-
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
|
+
};
|
|
18867
19008
|
//#endregion
|
|
18868
19009
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18869
19010
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19368,8 +19509,8 @@ const CONTEXT_MODULES = [
|
|
|
19368
19509
|
];
|
|
19369
19510
|
const isCreateContextCallee = (callee) => {
|
|
19370
19511
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19371
|
-
|
|
19372
|
-
return
|
|
19512
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19513
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19373
19514
|
}
|
|
19374
19515
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19375
19516
|
const namespaceIdentifier = callee.object;
|
|
@@ -19379,8 +19520,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19379
19520
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19380
19521
|
const namespaceName = namespaceIdentifier.name;
|
|
19381
19522
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19382
|
-
|
|
19383
|
-
return
|
|
19523
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19524
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19384
19525
|
}
|
|
19385
19526
|
return false;
|
|
19386
19527
|
};
|
|
@@ -19782,38 +19923,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19782
19923
|
};
|
|
19783
19924
|
const parseColorToRgb = (value) => {
|
|
19784
19925
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
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
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
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
|
+
}
|
|
19817
19964
|
return null;
|
|
19818
19965
|
};
|
|
19819
19966
|
//#endregion
|
|
@@ -19841,9 +19988,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19841
19988
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19842
19989
|
return null;
|
|
19843
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;
|
|
19844
19995
|
const parseShadowLayerBlur = (layer) => {
|
|
19845
|
-
const
|
|
19846
|
-
|
|
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;
|
|
19847
20003
|
};
|
|
19848
20004
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19849
20005
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19963,7 +20119,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19963
20119
|
};
|
|
19964
20120
|
//#endregion
|
|
19965
20121
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20122
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19966
20123
|
const collectComponentPropNames = (componentFunction) => {
|
|
20124
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20125
|
+
if (cached) return cached;
|
|
19967
20126
|
const propNames = /* @__PURE__ */ new Set();
|
|
19968
20127
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19969
20128
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19977,27 +20136,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19977
20136
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19978
20137
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19979
20138
|
});
|
|
20139
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19980
20140
|
return propNames;
|
|
19981
20141
|
};
|
|
19982
|
-
const
|
|
20142
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20143
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20144
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20145
|
+
if (cached) return cached;
|
|
19983
20146
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19984
20147
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19985
|
-
if (boundNames.has(bindingName)) return true;
|
|
19986
|
-
let declaresName = false;
|
|
19987
20148
|
walkAst(functionNode, (child) => {
|
|
19988
|
-
if (declaresName) return false;
|
|
19989
20149
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
19990
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
19991
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
19992
|
-
collectPatternNames(child.id, declaratorNames);
|
|
19993
|
-
if (declaratorNames.has(bindingName)) {
|
|
19994
|
-
declaresName = true;
|
|
19995
|
-
return false;
|
|
19996
|
-
}
|
|
19997
|
-
}
|
|
20150
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
19998
20151
|
});
|
|
19999
|
-
|
|
20152
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20153
|
+
return boundNames;
|
|
20000
20154
|
};
|
|
20155
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20001
20156
|
const isPropertyNamePosition = (identifier) => {
|
|
20002
20157
|
const parent = identifier.parent;
|
|
20003
20158
|
if (!parent) return false;
|
|
@@ -20420,36 +20575,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20420
20575
|
}
|
|
20421
20576
|
return hasNestedFunction;
|
|
20422
20577
|
};
|
|
20423
|
-
const
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
if (isReseeded) return false;
|
|
20431
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20432
|
-
isReseeded = true;
|
|
20433
|
-
return false;
|
|
20434
|
-
}
|
|
20435
|
-
});
|
|
20436
|
-
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;
|
|
20437
20585
|
};
|
|
20438
|
-
const
|
|
20586
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20439
20587
|
const setterName = getStateSetterName(useStateCall);
|
|
20440
20588
|
if (!setterName) return false;
|
|
20441
20589
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20442
20590
|
if (!componentFunction) return false;
|
|
20443
|
-
let
|
|
20591
|
+
let isExempt = false;
|
|
20444
20592
|
walkAst(componentFunction, (child) => {
|
|
20445
|
-
if (
|
|
20446
|
-
if (child
|
|
20447
|
-
|
|
20448
|
-
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;
|
|
20449
20596
|
return false;
|
|
20450
20597
|
}
|
|
20451
20598
|
});
|
|
20452
|
-
return
|
|
20599
|
+
return isExempt;
|
|
20453
20600
|
};
|
|
20454
20601
|
const noDerivedUseState = defineRule({
|
|
20455
20602
|
id: "no-derived-useState",
|
|
@@ -20466,8 +20613,7 @@ const noDerivedUseState = defineRule({
|
|
|
20466
20613
|
const initializer = node.arguments[0];
|
|
20467
20614
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20468
20615
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20469
|
-
if (
|
|
20470
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20616
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20471
20617
|
context.report({
|
|
20472
20618
|
node,
|
|
20473
20619
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20478,8 +20624,7 @@ const noDerivedUseState = defineRule({
|
|
|
20478
20624
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20479
20625
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20480
20626
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20481
|
-
if (
|
|
20482
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20627
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20483
20628
|
context.report({
|
|
20484
20629
|
node,
|
|
20485
20630
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22178,20 +22323,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22178
22323
|
return null;
|
|
22179
22324
|
};
|
|
22180
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
|
+
});
|
|
22181
22338
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22182
|
-
for (const binding of useStateBindings)
|
|
22183
|
-
let didFindAnySetterCall = false;
|
|
22184
|
-
let areAllSetterCallsInHandlers = true;
|
|
22185
|
-
walkAst(componentBody, (child) => {
|
|
22186
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22187
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22188
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22189
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22190
|
-
didFindAnySetterCall = true;
|
|
22191
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22192
|
-
});
|
|
22193
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22194
|
-
}
|
|
22339
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22195
22340
|
return handlerOnlyWriteStateNames;
|
|
22196
22341
|
};
|
|
22197
22342
|
const noEventTriggerState = defineRule({
|
|
@@ -22531,6 +22676,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22531
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)-)/;
|
|
22532
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)-)/;
|
|
22533
22678
|
const splitVariantScope = (token) => {
|
|
22679
|
+
if (!token.includes(":")) return {
|
|
22680
|
+
scope: "",
|
|
22681
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22682
|
+
};
|
|
22534
22683
|
const segments = token.split(":");
|
|
22535
22684
|
const rawUtility = segments[segments.length - 1];
|
|
22536
22685
|
return {
|
|
@@ -22554,6 +22703,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22554
22703
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22555
22704
|
for (const token of classStr.split(/\s+/)) {
|
|
22556
22705
|
if (!token) continue;
|
|
22706
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22557
22707
|
const { scope, utility } = splitVariantScope(token);
|
|
22558
22708
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22559
22709
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23283,6 +23433,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23283
23433
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23284
23434
|
});
|
|
23285
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-])/;
|
|
23286
23438
|
const noLongTransitionDuration = defineRule({
|
|
23287
23439
|
id: "no-long-transition-duration",
|
|
23288
23440
|
title: "Transition duration too long",
|
|
@@ -23306,11 +23458,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23306
23458
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23307
23459
|
let longestDurationPropertyMs = 0;
|
|
23308
23460
|
for (const segment of value.split(",")) {
|
|
23309
|
-
const
|
|
23310
|
-
|
|
23311
|
-
const
|
|
23312
|
-
|
|
23313
|
-
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);
|
|
23314
23465
|
}
|
|
23315
23466
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23316
23467
|
}
|
|
@@ -23318,7 +23469,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23318
23469
|
let longestDurationMs = 0;
|
|
23319
23470
|
for (const segment of value.split(",")) {
|
|
23320
23471
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23321
|
-
const firstTimeMatch = segment.match(
|
|
23472
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23322
23473
|
if (!firstTimeMatch) continue;
|
|
23323
23474
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23324
23475
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24490,14 +24641,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24490
24641
|
out.hasOpaqueBranch = true;
|
|
24491
24642
|
};
|
|
24492
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.`;
|
|
24493
|
-
const
|
|
24644
|
+
const INTERACTIVE_HANDLERS_LOWER = new Set([
|
|
24494
24645
|
"onClick",
|
|
24495
24646
|
"onMouseDown",
|
|
24496
24647
|
"onMouseUp",
|
|
24497
24648
|
"onKeyDown",
|
|
24498
24649
|
"onKeyPress",
|
|
24499
24650
|
"onKeyUp"
|
|
24500
|
-
];
|
|
24651
|
+
].map((handlerName) => handlerName.toLowerCase()));
|
|
24501
24652
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24502
24653
|
id: "no-noninteractive-element-interactions",
|
|
24503
24654
|
title: "Handler on non-interactive element",
|
|
@@ -24512,7 +24663,16 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24512
24663
|
const tag = getElementType(node, context.settings);
|
|
24513
24664
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24514
24665
|
if (tag === "label") return;
|
|
24515
|
-
|
|
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;
|
|
24516
24676
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24517
24677
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24518
24678
|
if (roleAttr) {
|
|
@@ -25825,16 +25985,22 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25825
25985
|
["tr", "row"],
|
|
25826
25986
|
["ul", "list"]
|
|
25827
25987
|
];
|
|
25828
|
-
const
|
|
25829
|
-
|
|
25830
|
-
|
|
25831
|
-
|
|
25832
|
-
|
|
25833
|
-
const
|
|
25834
|
-
|
|
25835
|
-
|
|
25836
|
-
|
|
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;
|
|
25837
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;
|
|
25838
26004
|
//#endregion
|
|
25839
26005
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
25840
26006
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26076,14 +26242,14 @@ const noRenderInRender = defineRule({
|
|
|
26076
26242
|
const expression = node.expression;
|
|
26077
26243
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26078
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;
|
|
26079
26248
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26080
26249
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26081
|
-
|
|
26082
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26250
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26083
26251
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26084
|
-
calleeName = expression.callee.property.name;
|
|
26085
26252
|
}
|
|
26086
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26087
26253
|
context.report({
|
|
26088
26254
|
node: expression,
|
|
26089
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.`
|
|
@@ -26334,6 +26500,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26334
26500
|
"headers",
|
|
26335
26501
|
"remountDeps"
|
|
26336
26502
|
];
|
|
26503
|
+
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26337
26504
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26338
26505
|
"createFileRoute",
|
|
26339
26506
|
"createRoute",
|
|
@@ -26349,6 +26516,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26349
26516
|
"server",
|
|
26350
26517
|
"handler"
|
|
26351
26518
|
];
|
|
26519
|
+
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26352
26520
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26353
26521
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26354
26522
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27053,14 +27221,21 @@ const noStaticElementInteractions = defineRule({
|
|
|
27053
27221
|
category: "Accessibility",
|
|
27054
27222
|
create: (context) => {
|
|
27055
27223
|
const settings = resolveSettings$12(context.settings);
|
|
27224
|
+
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27056
27225
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27057
27226
|
return { JSXOpeningElement(node) {
|
|
27058
27227
|
if (isTestlikeFile) return;
|
|
27059
27228
|
let hasNonBlockerHandler = false;
|
|
27060
27229
|
let hasAnyHandler = false;
|
|
27061
|
-
|
|
27062
|
-
|
|
27063
|
-
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);
|
|
27064
27239
|
if (isNullValue(attribute)) continue;
|
|
27065
27240
|
hasAnyHandler = true;
|
|
27066
27241
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -28241,13 +28416,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28241
28416
|
["xml:lang", "xmlLang"],
|
|
28242
28417
|
["xml:space", "xmlSpace"]
|
|
28243
28418
|
]);
|
|
28244
|
-
const
|
|
28419
|
+
const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
|
|
28245
28420
|
"charset",
|
|
28246
28421
|
"allowFullScreen",
|
|
28247
28422
|
"webkitAllowFullScreen",
|
|
28248
28423
|
"mozAllowFullScreen",
|
|
28249
28424
|
"webkitDirectory"
|
|
28250
|
-
];
|
|
28425
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
28251
28426
|
//#endregion
|
|
28252
28427
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28253
28428
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28451,10 +28626,7 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28451
28626
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28452
28627
|
return !tagName.includes("-");
|
|
28453
28628
|
};
|
|
28454
|
-
const normalizeAttributeCase = (name) =>
|
|
28455
|
-
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28456
|
-
return name;
|
|
28457
|
-
};
|
|
28629
|
+
const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
|
|
28458
28630
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28459
28631
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28460
28632
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28798,32 +28970,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28798
28970
|
if (!isHocCallee$1(parent)) return false;
|
|
28799
28971
|
return parent.arguments[0] === node;
|
|
28800
28972
|
};
|
|
28973
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
28974
|
+
"map",
|
|
28975
|
+
"forEach",
|
|
28976
|
+
"filter",
|
|
28977
|
+
"flatMap",
|
|
28978
|
+
"reduce",
|
|
28979
|
+
"reduceRight"
|
|
28980
|
+
]);
|
|
28801
28981
|
const isReturnOfMapCallback = (node) => {
|
|
28802
28982
|
const parent = node.parent;
|
|
28803
28983
|
if (!parent) return false;
|
|
28804
28984
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28805
28985
|
const callee = parent.callee;
|
|
28806
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28807
|
-
"map",
|
|
28808
|
-
"forEach",
|
|
28809
|
-
"filter",
|
|
28810
|
-
"flatMap",
|
|
28811
|
-
"reduce",
|
|
28812
|
-
"reduceRight"
|
|
28813
|
-
].includes(callee.property.name);
|
|
28986
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28814
28987
|
}
|
|
28815
28988
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28816
28989
|
const callbackParent = parent.parent;
|
|
28817
28990
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28818
28991
|
const callee = callbackParent.callee;
|
|
28819
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28820
|
-
"map",
|
|
28821
|
-
"forEach",
|
|
28822
|
-
"filter",
|
|
28823
|
-
"flatMap",
|
|
28824
|
-
"reduce",
|
|
28825
|
-
"reduceRight"
|
|
28826
|
-
].includes(callee.property.name);
|
|
28992
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28827
28993
|
}
|
|
28828
28994
|
}
|
|
28829
28995
|
return false;
|
|
@@ -28859,10 +29025,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28859
29025
|
valueNode = parent;
|
|
28860
29026
|
parent = parent.parent;
|
|
28861
29027
|
continue;
|
|
28862
|
-
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);
|
|
28863
29029
|
case "AssignmentExpression": {
|
|
28864
29030
|
const assignmentTarget = parent.left;
|
|
28865
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
29031
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28866
29032
|
}
|
|
28867
29033
|
case "Property":
|
|
28868
29034
|
if (parent.value !== valueNode) return false;
|
|
@@ -28955,14 +29121,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28955
29121
|
};
|
|
28956
29122
|
return {
|
|
28957
29123
|
JSXOpeningElement(node) {
|
|
28958
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29124
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28959
29125
|
recordInstantiation(node.name, node.name.name);
|
|
28960
29126
|
return;
|
|
28961
29127
|
}
|
|
28962
29128
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28963
29129
|
},
|
|
28964
29130
|
Identifier(node) {
|
|
28965
|
-
if (!
|
|
29131
|
+
if (!isReactComponentName(node.name)) return;
|
|
28966
29132
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28967
29133
|
recordInstantiation(node, node.name);
|
|
28968
29134
|
},
|
|
@@ -30772,13 +30938,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30772
30938
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30773
30939
|
},
|
|
30774
30940
|
JSXAttribute(node) {
|
|
30775
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30776
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30777
30941
|
if (!node.value) return;
|
|
30778
30942
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30779
30943
|
const innerExpression = node.value.expression;
|
|
30780
30944
|
if (!innerExpression) return;
|
|
30781
30945
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
30946
|
+
if (!isInsideFunctionScope(node)) return;
|
|
30947
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30782
30948
|
const parentJsxOpening = node.parent;
|
|
30783
30949
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30784
30950
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -32150,41 +32316,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32150
32316
|
});
|
|
32151
32317
|
return found;
|
|
32152
32318
|
};
|
|
32153
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32154
|
-
if (!componentBody) return false;
|
|
32155
|
-
let found = false;
|
|
32156
|
-
walkAst(componentBody, (child) => {
|
|
32157
|
-
if (found) return;
|
|
32158
|
-
if (!isFunctionLike$1(child)) return;
|
|
32159
|
-
const functionBody = child.body;
|
|
32160
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32161
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32162
|
-
});
|
|
32163
|
-
return found;
|
|
32164
|
-
};
|
|
32165
32319
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32166
32320
|
"then",
|
|
32167
32321
|
"catch",
|
|
32168
32322
|
"finally"
|
|
32169
32323
|
]);
|
|
32170
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32171
|
-
if (!componentBody) return false;
|
|
32172
|
-
let found = false;
|
|
32173
|
-
walkAst(componentBody, (child) => {
|
|
32174
|
-
if (found) return;
|
|
32175
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32176
|
-
const callee = child.callee;
|
|
32177
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32178
|
-
for (const argument of child.arguments ?? []) {
|
|
32179
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32180
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32181
|
-
found = true;
|
|
32182
|
-
return;
|
|
32183
|
-
}
|
|
32184
|
-
}
|
|
32185
|
-
});
|
|
32186
|
-
return found;
|
|
32187
|
-
};
|
|
32188
32324
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32189
32325
|
"useApolloClient",
|
|
32190
32326
|
"useMutation",
|
|
@@ -32197,20 +32333,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32197
32333
|
"fetch",
|
|
32198
32334
|
"axios"
|
|
32199
32335
|
]);
|
|
32200
|
-
const
|
|
32201
|
-
if (!body) return false;
|
|
32336
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32202
32337
|
let found = false;
|
|
32203
|
-
walkAst(
|
|
32204
|
-
if (found) return;
|
|
32338
|
+
walkAst(fnBody, (child) => {
|
|
32339
|
+
if (found) return false;
|
|
32205
32340
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32206
32341
|
const callee = child.callee;
|
|
32207
32342
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32208
32343
|
found = true;
|
|
32209
|
-
return;
|
|
32344
|
+
return false;
|
|
32210
32345
|
}
|
|
32211
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")
|
|
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
|
+
}
|
|
32358
|
+
}
|
|
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)) {
|
|
32212
32364
|
found = true;
|
|
32213
|
-
return;
|
|
32365
|
+
return false;
|
|
32214
32366
|
}
|
|
32215
32367
|
}
|
|
32216
32368
|
});
|
|
@@ -32235,7 +32387,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32235
32387
|
const secondBinding = node.id.elements[1];
|
|
32236
32388
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32237
32389
|
const fnBody = enclosingFunctionBody(node);
|
|
32238
|
-
if (fnBody && (
|
|
32390
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32239
32391
|
context.report({
|
|
32240
32392
|
node: node.init,
|
|
32241
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`
|
|
@@ -33029,17 +33181,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33029
33181
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33030
33182
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33031
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
|
+
});
|
|
33032
33189
|
for (const binding of bindings) {
|
|
33033
33190
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33034
33191
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33035
33192
|
const setterSuffix = binding.setterName.slice(3);
|
|
33036
33193
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33037
|
-
|
|
33038
|
-
walkAst(componentBody, (child) => {
|
|
33039
|
-
if (setterCalled) return;
|
|
33040
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33041
|
-
});
|
|
33042
|
-
if (!setterCalled) continue;
|
|
33194
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33043
33195
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33044
33196
|
context.report({
|
|
33045
33197
|
node: binding.declarator,
|
|
@@ -33371,6 +33523,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33371
33523
|
FlashList: ["@shopify/flash-list"],
|
|
33372
33524
|
LegendList: ["@legendapp/list"]
|
|
33373
33525
|
};
|
|
33526
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33374
33527
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33375
33528
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33376
33529
|
"renderItem",
|
|
@@ -33613,33 +33766,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33613
33766
|
requires: ["react-native"],
|
|
33614
33767
|
severity: "warn",
|
|
33615
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.",
|
|
33616
|
-
create: (context) =>
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
|
|
33623
|
-
|
|
33624
|
-
|
|
33625
|
-
|
|
33626
|
-
|
|
33627
|
-
|
|
33628
|
-
|
|
33629
|
-
|
|
33630
|
-
|
|
33631
|
-
hasDataProp =
|
|
33632
|
-
|
|
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
|
+
});
|
|
33633
33802
|
}
|
|
33634
|
-
}
|
|
33635
|
-
|
|
33636
|
-
if (dataIsEmptyLiteral) return;
|
|
33637
|
-
if (!hasDataProp) return;
|
|
33638
|
-
context.report({
|
|
33639
|
-
node,
|
|
33640
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33641
|
-
});
|
|
33642
|
-
} })
|
|
33803
|
+
};
|
|
33804
|
+
}
|
|
33643
33805
|
});
|
|
33644
33806
|
//#endregion
|
|
33645
33807
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33650,25 +33812,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33650
33812
|
requires: ["react-native"],
|
|
33651
33813
|
severity: "warn",
|
|
33652
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.",
|
|
33653
|
-
create: (context) =>
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
33663
|
-
|
|
33664
|
-
|
|
33665
|
-
|
|
33666
|
-
|
|
33667
|
-
|
|
33668
|
-
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
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
|
+
}
|
|
33672
33843
|
});
|
|
33673
33844
|
//#endregion
|
|
33674
33845
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -34652,9 +34823,10 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34652
34823
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34653
34824
|
return resolveJsxElementName(openingElement);
|
|
34654
34825
|
};
|
|
34826
|
+
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34655
34827
|
const isTextHandlingComponent = (elementName) => {
|
|
34656
34828
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34657
|
-
return
|
|
34829
|
+
return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
|
|
34658
34830
|
};
|
|
34659
34831
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34660
34832
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34698,6 +34870,7 @@ const rnNoRawText = defineRule({
|
|
|
34698
34870
|
return {
|
|
34699
34871
|
Program(programNode) {
|
|
34700
34872
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
34873
|
+
if (!containsJsxElement(programNode)) return;
|
|
34701
34874
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34702
34875
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34703
34876
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38652,14 +38825,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38652
38825
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38653
38826
|
category: "Accessibility",
|
|
38654
38827
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38655
|
-
|
|
38656
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38657
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38658
|
-
if (!role) return;
|
|
38659
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38660
|
-
const isImplicit = !roleAttribute;
|
|
38661
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38662
|
-
if (!supported) return;
|
|
38828
|
+
let ariaAttributes = null;
|
|
38663
38829
|
for (const attribute of node.attributes) {
|
|
38664
38830
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38665
38831
|
const attributeNode = attribute;
|
|
@@ -38669,6 +38835,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38669
38835
|
const propName = propRawName.toLowerCase();
|
|
38670
38836
|
if (!propName.startsWith("aria-")) continue;
|
|
38671
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) {
|
|
38672
38853
|
if (supported.has(propName)) continue;
|
|
38673
38854
|
context.report({
|
|
38674
38855
|
node: attribute,
|
|
@@ -39506,6 +39687,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39506
39687
|
cachedFunctionNames.add(node.id.name);
|
|
39507
39688
|
},
|
|
39508
39689
|
CallExpression(node) {
|
|
39690
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39509
39691
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39510
39692
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39511
39693
|
const firstArg = node.arguments?.[0];
|
|
@@ -40432,7 +40614,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40432
40614
|
//#endregion
|
|
40433
40615
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40434
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;
|
|
40435
|
-
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
|
+
};
|
|
40436
40627
|
const supabaseTableMissingRls = defineRule({
|
|
40437
40628
|
id: "supabase-table-missing-rls",
|
|
40438
40629
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40443,11 +40634,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40443
40634
|
const content = sanitizeSqlForScan(file.content);
|
|
40444
40635
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40445
40636
|
const findings = [];
|
|
40637
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40446
40638
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40447
40639
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40448
40640
|
const tableName = match[1];
|
|
40449
40641
|
if (tableName === void 0) continue;
|
|
40450
|
-
|
|
40642
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40643
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40451
40644
|
const location = getLocationAtIndex(content, match.index);
|
|
40452
40645
|
findings.push({
|
|
40453
40646
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40723,6 +40916,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40723
40916
|
severity: "warn",
|
|
40724
40917
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40725
40918
|
create: (context) => {
|
|
40919
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40726
40920
|
let hasHeadContentElement = false;
|
|
40727
40921
|
let hasDocumentHeadElement = false;
|
|
40728
40922
|
let hasCustomHeadChildElement = false;
|
|
@@ -40759,8 +40953,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40759
40953
|
};
|
|
40760
40954
|
return {
|
|
40761
40955
|
Program(node) {
|
|
40762
|
-
const filename = context.filename ?? "";
|
|
40763
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40764
40956
|
const statements = node.body ?? [];
|
|
40765
40957
|
for (const statement of statements) collectImportBindings(statement);
|
|
40766
40958
|
for (const statement of statements) {
|
|
@@ -40769,18 +40961,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40769
40961
|
}
|
|
40770
40962
|
},
|
|
40771
40963
|
ImportDeclaration(node) {
|
|
40772
|
-
const filename = context.filename ?? "";
|
|
40773
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40774
40964
|
collectImportBindings(node);
|
|
40775
40965
|
},
|
|
40776
40966
|
VariableDeclarator(node) {
|
|
40777
|
-
const filename = context.filename ?? "";
|
|
40778
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40779
40967
|
collectVariableAlias(node);
|
|
40780
40968
|
},
|
|
40781
40969
|
JSXOpeningElement(node) {
|
|
40782
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40783
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40784
40970
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40785
40971
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40786
40972
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40794,8 +40980,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40794
40980
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40795
40981
|
},
|
|
40796
40982
|
"Program:exit"(programNode) {
|
|
40797
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40798
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40799
40983
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40800
40984
|
node: programNode,
|
|
40801
40985
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40819,27 +41003,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40819
41003
|
requires: ["tanstack-start"],
|
|
40820
41004
|
severity: "warn",
|
|
40821
41005
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40822
|
-
create: (context) =>
|
|
40823
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
40830
|
-
|
|
40831
|
-
|
|
40832
|
-
|
|
40833
|
-
|
|
40834
|
-
|
|
40835
|
-
|
|
40836
|
-
|
|
40837
|
-
|
|
40838
|
-
|
|
40839
|
-
|
|
40840
|
-
|
|
40841
|
-
|
|
40842
|
-
|
|
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
|
+
}
|
|
40843
41029
|
});
|
|
40844
41030
|
//#endregion
|
|
40845
41031
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40903,7 +41089,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40903
41089
|
severity: "warn",
|
|
40904
41090
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40905
41091
|
create: (context) => {
|
|
40906
|
-
|
|
41092
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40907
41093
|
let deferredCallbackDepth = 0;
|
|
40908
41094
|
let eventHandlerDepth = 0;
|
|
40909
41095
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40967,7 +41153,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40967
41153
|
};
|
|
40968
41154
|
return {
|
|
40969
41155
|
CallExpression(node) {
|
|
40970
|
-
if (!isRouteFile) return;
|
|
40971
41156
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40972
41157
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40973
41158
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40979,39 +41164,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40979
41164
|
}
|
|
40980
41165
|
},
|
|
40981
41166
|
"CallExpression:exit"(node) {
|
|
40982
|
-
if (!isRouteFile) return;
|
|
40983
41167
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40984
41168
|
},
|
|
40985
41169
|
JSXAttribute(node) {
|
|
40986
|
-
if (!isRouteFile) return;
|
|
40987
41170
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40988
41171
|
},
|
|
40989
41172
|
"JSXAttribute:exit"(node) {
|
|
40990
|
-
if (!isRouteFile) return;
|
|
40991
41173
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40992
41174
|
},
|
|
40993
41175
|
Property(node) {
|
|
40994
|
-
if (!isRouteFile) return;
|
|
40995
41176
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40996
41177
|
},
|
|
40997
41178
|
"Property:exit"(node) {
|
|
40998
|
-
if (!isRouteFile) return;
|
|
40999
41179
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41000
41180
|
},
|
|
41001
41181
|
VariableDeclarator(node) {
|
|
41002
|
-
if (!isRouteFile) return;
|
|
41003
41182
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
41004
41183
|
},
|
|
41005
41184
|
"VariableDeclarator:exit"(node) {
|
|
41006
|
-
if (!isRouteFile) return;
|
|
41007
41185
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41008
41186
|
},
|
|
41009
41187
|
FunctionDeclaration(node) {
|
|
41010
|
-
if (!isRouteFile) return;
|
|
41011
41188
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41012
41189
|
},
|
|
41013
41190
|
"FunctionDeclaration:exit"(node) {
|
|
41014
|
-
if (!isRouteFile) return;
|
|
41015
41191
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41016
41192
|
}
|
|
41017
41193
|
};
|
|
@@ -41095,23 +41271,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41095
41271
|
requires: ["tanstack-start"],
|
|
41096
41272
|
severity: "warn",
|
|
41097
41273
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41098
|
-
create: (context) =>
|
|
41099
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41108
|
-
|
|
41109
|
-
|
|
41110
|
-
|
|
41111
|
-
|
|
41112
|
-
|
|
41113
|
-
|
|
41114
|
-
|
|
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
|
+
}
|
|
41115
41293
|
});
|
|
41116
41294
|
//#endregion
|
|
41117
41295
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41152,10 +41330,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41152
41330
|
const propertyName = getPropertyKeyName(property);
|
|
41153
41331
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41154
41332
|
}
|
|
41155
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41333
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
|
|
41156
41334
|
let lastIndex = -1;
|
|
41157
41335
|
for (const propertyName of sensitiveProperties) {
|
|
41158
|
-
const currentIndex =
|
|
41336
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
|
|
41159
41337
|
if (currentIndex < lastIndex) {
|
|
41160
41338
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41161
41339
|
context.report({
|
|
@@ -41192,10 +41370,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41192
41370
|
} else return;
|
|
41193
41371
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41194
41372
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41195
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41373
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
|
|
41196
41374
|
let lastIndex = -1;
|
|
41197
41375
|
for (const methodName of orderSensitiveMethods) {
|
|
41198
|
-
const currentIndex =
|
|
41376
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
|
|
41199
41377
|
if (currentIndex < lastIndex) {
|
|
41200
41378
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41201
41379
|
context.report({
|
|
@@ -41479,13 +41657,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41479
41657
|
//#endregion
|
|
41480
41658
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41481
41659
|
const ZOD_MODULE = "zod";
|
|
41660
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41482
41661
|
const getStaticPropertyName = (member) => {
|
|
41483
41662
|
const property = member.property;
|
|
41484
41663
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41485
41664
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41486
41665
|
return null;
|
|
41487
41666
|
};
|
|
41667
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41488
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) => {
|
|
41489
41675
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41490
41676
|
if (!specifier) return null;
|
|
41491
41677
|
const declaration = specifier.parent;
|
|
@@ -41622,25 +41808,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41622
41808
|
tags: ["migration-hint"],
|
|
41623
41809
|
severity: "warn",
|
|
41624
41810
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41625
|
-
create: (context) =>
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41637
|
-
|
|
41638
|
-
|
|
41639
|
-
|
|
41640
|
-
|
|
41641
|
-
|
|
41642
|
-
|
|
41643
|
-
|
|
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
|
+
}
|
|
41644
41838
|
});
|
|
41645
41839
|
//#endregion
|
|
41646
41840
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41832,16 +42026,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41832
42026
|
tags: ["migration-hint"],
|
|
41833
42027
|
severity: "warn",
|
|
41834
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()`.",
|
|
41835
|
-
create: (context) =>
|
|
41836
|
-
|
|
41837
|
-
|
|
41838
|
-
|
|
41839
|
-
|
|
41840
|
-
|
|
41841
|
-
|
|
41842
|
-
|
|
41843
|
-
|
|
41844
|
-
|
|
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
|
+
}
|
|
41845
42047
|
});
|
|
41846
42048
|
//#endregion
|
|
41847
42049
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41876,13 +42078,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41876
42078
|
tags: ["migration-hint"],
|
|
41877
42079
|
severity: "warn",
|
|
41878
42080
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41879
|
-
create: (context) =>
|
|
41880
|
-
|
|
41881
|
-
|
|
41882
|
-
node
|
|
41883
|
-
|
|
41884
|
-
|
|
41885
|
-
|
|
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
|
+
}
|
|
41886
42097
|
});
|
|
41887
42098
|
//#endregion
|
|
41888
42099
|
//#region src/plugin/rule-registry.ts
|