oxlint-plugin-react-doctor 0.6.2-dev.397816a → 0.6.2-dev.ac71a3b
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 +610 -465
- 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
|
|
@@ -760,24 +783,6 @@ const collectChildComponentNames = (element, into) => {
|
|
|
760
783
|
into.add(name);
|
|
761
784
|
});
|
|
762
785
|
};
|
|
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
786
|
const countEffectHookCalls = (body) => {
|
|
782
787
|
if (!body) return 0;
|
|
783
788
|
let count = 0;
|
|
@@ -787,6 +792,36 @@ const countEffectHookCalls = (body) => {
|
|
|
787
792
|
});
|
|
788
793
|
return count;
|
|
789
794
|
};
|
|
795
|
+
const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
|
|
796
|
+
const getComponentEffectIndex = (programRoot) => {
|
|
797
|
+
const cached = componentEffectIndexCache.get(programRoot);
|
|
798
|
+
if (cached) return cached;
|
|
799
|
+
const bodyByName = /* @__PURE__ */ new Map();
|
|
800
|
+
walkAst(programRoot, (node) => {
|
|
801
|
+
if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
|
|
802
|
+
bodyByName.set(node.id.name, node.body);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
|
|
806
|
+
const initializer = node.init;
|
|
807
|
+
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
const index = {
|
|
811
|
+
bodyByName,
|
|
812
|
+
effectCountByName: /* @__PURE__ */ new Map()
|
|
813
|
+
};
|
|
814
|
+
componentEffectIndexCache.set(programRoot, index);
|
|
815
|
+
return index;
|
|
816
|
+
};
|
|
817
|
+
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
818
|
+
const index = getComponentEffectIndex(programRoot);
|
|
819
|
+
const cachedCount = index.effectCountByName.get(componentName);
|
|
820
|
+
if (cachedCount !== void 0) return cachedCount;
|
|
821
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
822
|
+
index.effectCountByName.set(componentName, count);
|
|
823
|
+
return count;
|
|
824
|
+
};
|
|
790
825
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
791
826
|
id: "activity-wraps-effect-heavy-subtree",
|
|
792
827
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -843,9 +878,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
843
878
|
let totalEffects = 0;
|
|
844
879
|
const effectfulChildren = [];
|
|
845
880
|
for (const componentName of childComponentNames) {
|
|
846
|
-
const
|
|
847
|
-
if (!body) continue;
|
|
848
|
-
const effectCount = countEffectHookCalls(body);
|
|
881
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
849
882
|
if (effectCount === 0) continue;
|
|
850
883
|
totalEffects += effectCount;
|
|
851
884
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1394,11 +1427,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1394
1427
|
};
|
|
1395
1428
|
//#endregion
|
|
1396
1429
|
//#region src/plugin/utils/get-element-type.ts
|
|
1430
|
+
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1431
|
+
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
1397
1432
|
const readJsxA11ySettings = (settings) => {
|
|
1398
|
-
if (!settings) return
|
|
1433
|
+
if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
|
|
1434
|
+
const cachedSettings = jsxA11ySettingsCache.get(settings);
|
|
1435
|
+
if (cachedSettings) return cachedSettings;
|
|
1399
1436
|
const block = settings["jsx-a11y"];
|
|
1400
|
-
|
|
1401
|
-
|
|
1437
|
+
const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
|
|
1438
|
+
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1439
|
+
return a11ySettings;
|
|
1402
1440
|
};
|
|
1403
1441
|
const flattenJsxName$2 = (name) => {
|
|
1404
1442
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
@@ -1406,8 +1444,7 @@ const flattenJsxName$2 = (name) => {
|
|
|
1406
1444
|
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1407
1445
|
return "";
|
|
1408
1446
|
};
|
|
1409
|
-
const
|
|
1410
|
-
const a11ySettings = readJsxA11ySettings(settings);
|
|
1447
|
+
const computeElementType = (openingElement, a11ySettings) => {
|
|
1411
1448
|
const baseName = flattenJsxName$2(openingElement.name);
|
|
1412
1449
|
if (a11ySettings.polymorphicPropName) {
|
|
1413
1450
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
@@ -1419,6 +1456,23 @@ const getElementType = (openingElement, settings) => {
|
|
|
1419
1456
|
if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
|
|
1420
1457
|
return baseName;
|
|
1421
1458
|
};
|
|
1459
|
+
const elementTypeCache = /* @__PURE__ */ new WeakMap();
|
|
1460
|
+
const getElementType = (openingElement, settings) => {
|
|
1461
|
+
const cachedEntry = elementTypeCache.get(openingElement);
|
|
1462
|
+
if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
|
|
1463
|
+
const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
|
|
1464
|
+
elementTypeCache.set(openingElement, {
|
|
1465
|
+
settings,
|
|
1466
|
+
elementType
|
|
1467
|
+
});
|
|
1468
|
+
return elementType;
|
|
1469
|
+
};
|
|
1470
|
+
//#endregion
|
|
1471
|
+
//#region src/plugin/utils/has-jsx-a11y-settings.ts
|
|
1472
|
+
const hasJsxA11ySettings = (settings) => {
|
|
1473
|
+
const block = settings?.["jsx-a11y"];
|
|
1474
|
+
return typeof block === "object" && block !== null;
|
|
1475
|
+
};
|
|
1422
1476
|
//#endregion
|
|
1423
1477
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1424
1478
|
const collectFromProgram = (programRoot) => {
|
|
@@ -1972,7 +2026,12 @@ const altText = defineRule({
|
|
|
1972
2026
|
const objectAliases = new Set(settings.object ?? []);
|
|
1973
2027
|
const areaAliases = new Set(settings.area ?? []);
|
|
1974
2028
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2029
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
1975
2030
|
return { JSXOpeningElement(node) {
|
|
2031
|
+
if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
|
|
2032
|
+
const rawName = node.name.name;
|
|
2033
|
+
if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
|
|
2034
|
+
}
|
|
1976
2035
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1977
2036
|
const tag = getElementType(node, context.settings);
|
|
1978
2037
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2155,7 +2214,9 @@ const anchorIsValid = defineRule({
|
|
|
2155
2214
|
category: "Accessibility",
|
|
2156
2215
|
create: (context) => {
|
|
2157
2216
|
const settings = resolveSettings$50(context.settings);
|
|
2217
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2158
2218
|
return { JSXOpeningElement(node) {
|
|
2219
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2159
2220
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2160
2221
|
let hrefAttribute;
|
|
2161
2222
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3458,7 +3519,7 @@ const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
|
3458
3519
|
];
|
|
3459
3520
|
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
3460
3521
|
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
3461
|
-
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
3522
|
+
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth(?!or(?!i[sz])))/i;
|
|
3462
3523
|
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
3463
3524
|
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
3464
3525
|
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
@@ -3581,8 +3642,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3581
3642
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3582
3643
|
//#endregion
|
|
3583
3644
|
//#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
|
|
3645
|
+
const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
|
|
3584
3646
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3585
|
-
for (const match of content.matchAll(
|
|
3647
|
+
for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
|
|
3586
3648
|
const value = match[0] ?? "";
|
|
3587
3649
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3588
3650
|
}
|
|
@@ -4049,62 +4111,56 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4049
4111
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4050
4112
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4051
4113
|
};
|
|
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
4114
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4070
4115
|
"push",
|
|
4071
4116
|
"unshift",
|
|
4072
4117
|
"splice"
|
|
4073
4118
|
]);
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4119
|
+
const addDerivedBindings = (block, names) => {
|
|
4120
|
+
const declaratorBindings = [];
|
|
4076
4121
|
walkAst(block, (child) => {
|
|
4077
4122
|
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4078
|
-
if (!isNodeOfType(child, "
|
|
4079
|
-
|
|
4080
|
-
|
|
4123
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
4124
|
+
if (!isNodeOfType(child.id, "Identifier")) return;
|
|
4125
|
+
const referencedNames = /* @__PURE__ */ new Set();
|
|
4126
|
+
collectReferenceIdentifierNames(child.init, referencedNames);
|
|
4127
|
+
declaratorBindings.push({
|
|
4128
|
+
declaredName: child.id.name,
|
|
4129
|
+
referencedNames
|
|
4130
|
+
});
|
|
4081
4131
|
});
|
|
4082
|
-
return mutated;
|
|
4083
|
-
};
|
|
4084
|
-
const addDerivedBindings = (block, names) => {
|
|
4085
4132
|
let didGrow = true;
|
|
4086
4133
|
while (didGrow) {
|
|
4087
4134
|
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);
|
|
4135
|
+
for (const { declaredName, referencedNames } of declaratorBindings) {
|
|
4136
|
+
if (names.has(declaredName)) continue;
|
|
4137
|
+
for (const referenced of referencedNames) if (names.has(referenced)) {
|
|
4138
|
+
names.add(declaredName);
|
|
4096
4139
|
didGrow = true;
|
|
4097
4140
|
break;
|
|
4098
4141
|
}
|
|
4099
|
-
}
|
|
4142
|
+
}
|
|
4100
4143
|
}
|
|
4101
4144
|
};
|
|
4102
4145
|
const hasLoopCarriedDependency = (block) => {
|
|
4103
|
-
const carried =
|
|
4104
|
-
|
|
4146
|
+
const carried = /* @__PURE__ */ new Set();
|
|
4147
|
+
const awaitedReferences = /* @__PURE__ */ new Set();
|
|
4148
|
+
walkAst(block, (child) => {
|
|
4149
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4150
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.left) {
|
|
4151
|
+
collectPatternIdentifiers(child.left, carried);
|
|
4152
|
+
return;
|
|
4153
|
+
}
|
|
4154
|
+
if (isNodeOfType(child, "AwaitExpression") && child.argument) {
|
|
4155
|
+
collectReferenceIdentifierNames(child.argument, awaitedReferences);
|
|
4156
|
+
return;
|
|
4157
|
+
}
|
|
4158
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
4159
|
+
const callee = child.callee;
|
|
4160
|
+
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);
|
|
4161
|
+
});
|
|
4105
4162
|
if (carried.size === 0) return false;
|
|
4106
4163
|
addDerivedBindings(block, carried);
|
|
4107
|
-
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4108
4164
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4109
4165
|
return false;
|
|
4110
4166
|
};
|
|
@@ -4839,9 +4895,8 @@ const isCreateElementCall = (node) => {
|
|
|
4839
4895
|
const callee = node.callee;
|
|
4840
4896
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
|
|
4841
4897
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
4842
|
-
if (
|
|
4843
|
-
|
|
4844
|
-
return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
|
|
4898
|
+
if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
|
|
4899
|
+
return !memberChainContainsDocument(callee.object);
|
|
4845
4900
|
}
|
|
4846
4901
|
return false;
|
|
4847
4902
|
};
|
|
@@ -5658,12 +5713,20 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5658
5713
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5659
5714
|
};
|
|
5660
5715
|
const isInertParseTarget = (target, fileContent) => {
|
|
5716
|
+
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5717
|
+
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5718
|
+
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5661
5719
|
const escapedTarget = escapeRegExp(target);
|
|
5662
5720
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5663
5721
|
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
|
-
|
|
5722
|
+
if (fileHasCreateElement) {
|
|
5723
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5724
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5725
|
+
}
|
|
5726
|
+
if (fileHasIsolatedDocument) {
|
|
5727
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5728
|
+
}
|
|
5729
|
+
if (!fileHasCreateElement) return false;
|
|
5667
5730
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5668
5731
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5669
5732
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5756,10 +5819,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
5756
5819
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5757
5820
|
if (valueIdentifier !== void 0) {
|
|
5758
5821
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5822
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5823
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5824
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
5763
5825
|
}
|
|
5764
5826
|
}
|
|
5765
5827
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5908,6 +5970,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5908
5970
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5909
5971
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5910
5972
|
if (!classNameLiteral) return;
|
|
5973
|
+
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5911
5974
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5912
5975
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5913
5976
|
if (matchedPairs.length === 0) return;
|
|
@@ -5932,6 +5995,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
5932
5995
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5933
5996
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5934
5997
|
if (!classNameLiteral) return;
|
|
5998
|
+
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
5935
5999
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
5936
6000
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
5937
6001
|
if (matchedPairs.length === 0) return;
|
|
@@ -5956,6 +6020,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
5956
6020
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5957
6021
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5958
6022
|
if (!classNameLiteral) return;
|
|
6023
|
+
if (!classNameLiteral.includes("space-")) return;
|
|
5959
6024
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
5960
6025
|
let hasFlexOrGridLayout = false;
|
|
5961
6026
|
for (const token of tokens) {
|
|
@@ -6147,7 +6212,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6147
6212
|
const actualMinor = Number(match[2]);
|
|
6148
6213
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6149
6214
|
};
|
|
6215
|
+
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6150
6216
|
const containsJsx$1 = (root) => {
|
|
6217
|
+
const cached = containsJsxCache.get(root);
|
|
6218
|
+
if (cached !== void 0) return cached;
|
|
6151
6219
|
let found = false;
|
|
6152
6220
|
const visit = (node) => {
|
|
6153
6221
|
if (found) return;
|
|
@@ -6170,6 +6238,7 @@ const containsJsx$1 = (root) => {
|
|
|
6170
6238
|
}
|
|
6171
6239
|
};
|
|
6172
6240
|
visit(root);
|
|
6241
|
+
containsJsxCache.set(root, found);
|
|
6173
6242
|
return found;
|
|
6174
6243
|
};
|
|
6175
6244
|
const getStaticMemberName = (node) => {
|
|
@@ -6261,27 +6330,6 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6261
6330
|
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
6331
|
return false;
|
|
6263
6332
|
};
|
|
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
6333
|
const memberExpressionPath = (node) => {
|
|
6286
6334
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6287
6335
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6289,15 +6337,16 @@ const memberExpressionPath = (node) => {
|
|
|
6289
6337
|
const propertyName = getStaticMemberName(node);
|
|
6290
6338
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6291
6339
|
};
|
|
6292
|
-
const
|
|
6293
|
-
|
|
6340
|
+
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6341
|
+
const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
6342
|
+
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6343
|
+
if (cached) return cached;
|
|
6344
|
+
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6345
|
+
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6294
6346
|
const visit = (node) => {
|
|
6295
|
-
if (found) return;
|
|
6296
6347
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6297
|
-
if (
|
|
6298
|
-
|
|
6299
|
-
return;
|
|
6300
|
-
}
|
|
6348
|
+
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
6349
|
+
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
6301
6350
|
}
|
|
6302
6351
|
const record = node;
|
|
6303
6352
|
for (const key of Object.keys(record)) {
|
|
@@ -6306,12 +6355,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
|
6306
6355
|
if (Array.isArray(child)) {
|
|
6307
6356
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6308
6357
|
} else if (isAstNode(child)) visit(child);
|
|
6309
|
-
if (found) return;
|
|
6310
6358
|
}
|
|
6311
6359
|
};
|
|
6312
6360
|
visit(programRoot);
|
|
6313
|
-
|
|
6361
|
+
const index = {
|
|
6362
|
+
identifierTargets,
|
|
6363
|
+
memberPathSegments
|
|
6364
|
+
};
|
|
6365
|
+
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6366
|
+
return index;
|
|
6314
6367
|
};
|
|
6368
|
+
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6369
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6315
6370
|
const displayName = defineRule({
|
|
6316
6371
|
id: "display-name",
|
|
6317
6372
|
title: "Component missing display name",
|
|
@@ -7183,27 +7238,8 @@ const isDescendantScope = (inner, outer) => {
|
|
|
7183
7238
|
return false;
|
|
7184
7239
|
};
|
|
7185
7240
|
//#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
7241
|
//#region src/plugin/semantic/closure-captures.ts
|
|
7206
|
-
const
|
|
7242
|
+
const computeClosureCaptures = (functionNode, scopes) => {
|
|
7207
7243
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
7208
7244
|
const out = [];
|
|
7209
7245
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7238,7 +7274,20 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
7238
7274
|
}
|
|
7239
7275
|
};
|
|
7240
7276
|
visit(functionNode);
|
|
7241
|
-
return out
|
|
7277
|
+
return out;
|
|
7278
|
+
};
|
|
7279
|
+
const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
7280
|
+
const closureCaptures = (functionNode, scopes) => {
|
|
7281
|
+
let capturesByFunction = capturesByAnalysis.get(scopes);
|
|
7282
|
+
if (!capturesByFunction) {
|
|
7283
|
+
capturesByFunction = /* @__PURE__ */ new WeakMap();
|
|
7284
|
+
capturesByAnalysis.set(scopes, capturesByFunction);
|
|
7285
|
+
}
|
|
7286
|
+
const memoizedCaptures = capturesByFunction.get(functionNode);
|
|
7287
|
+
if (memoizedCaptures) return memoizedCaptures;
|
|
7288
|
+
const computedCaptures = computeClosureCaptures(functionNode, scopes);
|
|
7289
|
+
capturesByFunction.set(functionNode, computedCaptures);
|
|
7290
|
+
return computedCaptures;
|
|
7242
7291
|
};
|
|
7243
7292
|
//#endregion
|
|
7244
7293
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
@@ -7367,6 +7416,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
|
|
|
7367
7416
|
};
|
|
7368
7417
|
};
|
|
7369
7418
|
//#endregion
|
|
7419
|
+
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7420
|
+
/**
|
|
7421
|
+
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7422
|
+
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7423
|
+
* match (`true`) or the chain's root (`false`).
|
|
7424
|
+
*
|
|
7425
|
+
* Was duplicated byte-identical in:
|
|
7426
|
+
* - exhaustive-deps-symbol-stability.ts
|
|
7427
|
+
* - semantic/closure-captures.ts
|
|
7428
|
+
*/
|
|
7429
|
+
const isAstDescendant = (inner, outer) => {
|
|
7430
|
+
let current = inner;
|
|
7431
|
+
while (current) {
|
|
7432
|
+
if (current === outer) return true;
|
|
7433
|
+
current = current.parent ?? null;
|
|
7434
|
+
}
|
|
7435
|
+
return false;
|
|
7436
|
+
};
|
|
7437
|
+
//#endregion
|
|
7370
7438
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
7371
7439
|
/**
|
|
7372
7440
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -8266,9 +8334,14 @@ const firebaseQueryFilterAsAuth = defineRule({
|
|
|
8266
8334
|
* `*Handler`), so the cheap escape-then-replace shape is enough
|
|
8267
8335
|
* and avoids pulling picomatch into the per-file rule path.
|
|
8268
8336
|
*/
|
|
8337
|
+
const compiledGlobs = /* @__PURE__ */ new Map();
|
|
8269
8338
|
const compileGlob = (pattern) => {
|
|
8339
|
+
const cached = compiledGlobs.get(pattern);
|
|
8340
|
+
if (cached) return cached;
|
|
8270
8341
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
8271
|
-
|
|
8342
|
+
const compiled = new RegExp(`^${escaped}$`);
|
|
8343
|
+
compiledGlobs.set(pattern, compiled);
|
|
8344
|
+
return compiled;
|
|
8272
8345
|
};
|
|
8273
8346
|
//#endregion
|
|
8274
8347
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
@@ -9210,10 +9283,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9210
9283
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9211
9284
|
const settings = resolveSettings$37(context.settings);
|
|
9212
9285
|
return { JSXOpeningElement(node) {
|
|
9213
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9214
9286
|
const tag = getElementType(node, context.settings);
|
|
9215
9287
|
if (!settings.components.includes(tag)) return;
|
|
9216
9288
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9289
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9217
9290
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9218
9291
|
if (!altAttribute) return;
|
|
9219
9292
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9268,6 +9341,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9268
9341
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9269
9342
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9270
9343
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9344
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9271
9345
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9272
9346
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9273
9347
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9297,6 +9371,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9297
9371
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9298
9372
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9299
9373
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9374
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9300
9375
|
const content = getScannableContent(file);
|
|
9301
9376
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9302
9377
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9529,15 +9604,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9529
9604
|
const settings = resolveSettings$36(context.settings);
|
|
9530
9605
|
const tabbableSet = new Set(settings.tabbable);
|
|
9531
9606
|
return { JSXOpeningElement(node) {
|
|
9607
|
+
if (node.attributes.length === 0) return;
|
|
9532
9608
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9533
9609
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9534
9610
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9611
|
+
if (!role) return;
|
|
9612
|
+
if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
|
|
9535
9613
|
const elementType = getElementType(node, context.settings);
|
|
9536
9614
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9537
|
-
|
|
9615
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9538
9616
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9539
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9540
|
-
if (!role) return;
|
|
9541
9617
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9542
9618
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9543
9619
|
context.report({
|
|
@@ -10176,8 +10252,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10176
10252
|
walkAst(loopBody, (child) => {
|
|
10177
10253
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10178
10254
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10179
|
-
});
|
|
10180
|
-
walkAst(loopBody, (child) => {
|
|
10181
10255
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10182
10256
|
if (child.computed) return;
|
|
10183
10257
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10637,7 +10711,6 @@ const jsHoistIntl = defineRule({
|
|
|
10637
10711
|
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
10712
|
create: (context) => ({ NewExpression(node) {
|
|
10639
10713
|
if (!isIntlNewExpression(node)) return;
|
|
10640
|
-
if (isInsideCacheMemo(node)) return;
|
|
10641
10714
|
let cursor = node.parent ?? null;
|
|
10642
10715
|
let inFunctionBody = false;
|
|
10643
10716
|
while (cursor) {
|
|
@@ -10654,6 +10727,7 @@ const jsHoistIntl = defineRule({
|
|
|
10654
10727
|
cursor = cursor.parent ?? null;
|
|
10655
10728
|
}
|
|
10656
10729
|
if (!inFunctionBody) return;
|
|
10730
|
+
if (isInsideCacheMemo(node)) return;
|
|
10657
10731
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10658
10732
|
context.report({
|
|
10659
10733
|
node,
|
|
@@ -10728,7 +10802,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10728
10802
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10729
10803
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10730
10804
|
};
|
|
10731
|
-
const
|
|
10805
|
+
const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
10806
|
+
const getLoopBoundNames = (loop) => {
|
|
10807
|
+
const cached = loopBoundNamesCache.get(loop);
|
|
10808
|
+
if (cached) return cached;
|
|
10809
|
+
const names = /* @__PURE__ */ new Set();
|
|
10732
10810
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10733
10811
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10734
10812
|
});
|
|
@@ -10745,6 +10823,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10745
10823
|
if (targetRoot) names.add(targetRoot);
|
|
10746
10824
|
}
|
|
10747
10825
|
});
|
|
10826
|
+
loopBoundNamesCache.set(loop, names);
|
|
10827
|
+
return names;
|
|
10748
10828
|
};
|
|
10749
10829
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10750
10830
|
let cursor = receiver;
|
|
@@ -10770,7 +10850,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10770
10850
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10771
10851
|
let ancestor = node.parent;
|
|
10772
10852
|
while (ancestor) {
|
|
10773
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10853
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10774
10854
|
ancestor = ancestor.parent;
|
|
10775
10855
|
}
|
|
10776
10856
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -12529,6 +12609,7 @@ const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
|
12529
12609
|
"bottomContent",
|
|
12530
12610
|
"leftContent",
|
|
12531
12611
|
"rightContent",
|
|
12612
|
+
"config",
|
|
12532
12613
|
"value",
|
|
12533
12614
|
"currentValue",
|
|
12534
12615
|
"form",
|
|
@@ -12667,6 +12748,10 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12667
12748
|
"Panel",
|
|
12668
12749
|
"Overlay",
|
|
12669
12750
|
"Shape",
|
|
12751
|
+
"Avatar",
|
|
12752
|
+
"Text",
|
|
12753
|
+
"State",
|
|
12754
|
+
"Zone",
|
|
12670
12755
|
"Override",
|
|
12671
12756
|
"Overrides",
|
|
12672
12757
|
"Items",
|
|
@@ -12683,6 +12768,8 @@ const SLOT_PROP_SUFFIXES = [
|
|
|
12683
12768
|
];
|
|
12684
12769
|
const isSlotPropName = (propName) => {
|
|
12685
12770
|
if (KNOWN_SLOT_PROP_NAMES.has(propName)) return true;
|
|
12771
|
+
const decapitalized = propName.charAt(0).toLowerCase() + propName.slice(1);
|
|
12772
|
+
if (decapitalized !== propName && KNOWN_SLOT_PROP_NAMES.has(decapitalized)) return true;
|
|
12686
12773
|
for (const suffix of SLOT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
12687
12774
|
return false;
|
|
12688
12775
|
};
|
|
@@ -15588,8 +15675,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15588
15675
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15589
15676
|
let didFindFetchCall = false;
|
|
15590
15677
|
walkAst(node, (child) => {
|
|
15678
|
+
if (didFindFetchCall) return false;
|
|
15591
15679
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15592
|
-
if (isFetchCall$1(child))
|
|
15680
|
+
if (isFetchCall$1(child)) {
|
|
15681
|
+
didFindFetchCall = true;
|
|
15682
|
+
return false;
|
|
15683
|
+
}
|
|
15593
15684
|
});
|
|
15594
15685
|
return didFindFetchCall;
|
|
15595
15686
|
};
|
|
@@ -15603,6 +15694,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15603
15694
|
severity: "warn",
|
|
15604
15695
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15605
15696
|
create: (context) => {
|
|
15697
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15698
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15606
15699
|
let fileHasUseClient = false;
|
|
15607
15700
|
return {
|
|
15608
15701
|
Program(programNode) {
|
|
@@ -15612,8 +15705,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15612
15705
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15613
15706
|
const callback = getEffectCallback(node);
|
|
15614
15707
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15615
|
-
|
|
15616
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15708
|
+
context.report({
|
|
15617
15709
|
node,
|
|
15618
15710
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15619
15711
|
});
|
|
@@ -16278,16 +16370,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16278
16370
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16279
16371
|
create: (context) => {
|
|
16280
16372
|
let resolveBinding = () => null;
|
|
16373
|
+
let isRouteHandlerFile = false;
|
|
16374
|
+
let mutatingSegment = null;
|
|
16281
16375
|
return {
|
|
16282
16376
|
Program(node) {
|
|
16283
16377
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16378
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16379
|
+
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16380
|
+
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16284
16381
|
},
|
|
16285
16382
|
ExportNamedDeclaration(node) {
|
|
16286
|
-
|
|
16287
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16288
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16383
|
+
if (!isRouteHandlerFile) return;
|
|
16289
16384
|
if (!isExportedGetHandler(node)) return;
|
|
16290
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16291
16385
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16292
16386
|
for (const handlerBody of handlerBodies) {
|
|
16293
16387
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -18856,7 +18950,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18856
18950
|
}
|
|
18857
18951
|
return false;
|
|
18858
18952
|
};
|
|
18859
|
-
const
|
|
18953
|
+
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
18954
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
18955
|
+
const cachedEntry = renderOutputCache.get(functionNode);
|
|
18956
|
+
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
18957
|
+
const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
|
|
18958
|
+
renderOutputCache.set(functionNode, {
|
|
18959
|
+
scopes,
|
|
18960
|
+
hasRenderOutput
|
|
18961
|
+
});
|
|
18962
|
+
return hasRenderOutput;
|
|
18963
|
+
};
|
|
18860
18964
|
//#endregion
|
|
18861
18965
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18862
18966
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19361,8 +19465,8 @@ const CONTEXT_MODULES = [
|
|
|
19361
19465
|
];
|
|
19362
19466
|
const isCreateContextCallee = (callee) => {
|
|
19363
19467
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19364
|
-
|
|
19365
|
-
return
|
|
19468
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19469
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19366
19470
|
}
|
|
19367
19471
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19368
19472
|
const namespaceIdentifier = callee.object;
|
|
@@ -19372,8 +19476,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19372
19476
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19373
19477
|
const namespaceName = namespaceIdentifier.name;
|
|
19374
19478
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19375
|
-
|
|
19376
|
-
return
|
|
19479
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19480
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19377
19481
|
}
|
|
19378
19482
|
return false;
|
|
19379
19483
|
};
|
|
@@ -19775,38 +19879,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19775
19879
|
};
|
|
19776
19880
|
const parseColorToRgb = (value) => {
|
|
19777
19881
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19778
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
19782
|
-
|
|
19783
|
-
|
|
19784
|
-
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19882
|
+
if (trimmed.startsWith("#")) {
|
|
19883
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
19884
|
+
if (hex8Match) return {
|
|
19885
|
+
red: parseInt(hex8Match[1], 16),
|
|
19886
|
+
green: parseInt(hex8Match[2], 16),
|
|
19887
|
+
blue: parseInt(hex8Match[3], 16)
|
|
19888
|
+
};
|
|
19889
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
19890
|
+
if (hex6Match) return {
|
|
19891
|
+
red: parseInt(hex6Match[1], 16),
|
|
19892
|
+
green: parseInt(hex6Match[2], 16),
|
|
19893
|
+
blue: parseInt(hex6Match[3], 16)
|
|
19894
|
+
};
|
|
19895
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
19896
|
+
if (hex4Match) return {
|
|
19897
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
19898
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
19899
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
19900
|
+
};
|
|
19901
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
19902
|
+
if (hex3Match) return {
|
|
19903
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
19904
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
19905
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
19906
|
+
};
|
|
19907
|
+
}
|
|
19908
|
+
if (trimmed.includes("rgb")) {
|
|
19909
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
19910
|
+
if (rgbMatch) return {
|
|
19911
|
+
red: parseInt(rgbMatch[1], 10),
|
|
19912
|
+
green: parseInt(rgbMatch[2], 10),
|
|
19913
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
19914
|
+
};
|
|
19915
|
+
}
|
|
19916
|
+
if (trimmed.includes("hsl")) {
|
|
19917
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19918
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19919
|
+
}
|
|
19810
19920
|
return null;
|
|
19811
19921
|
};
|
|
19812
19922
|
//#endregion
|
|
@@ -19834,9 +19944,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19834
19944
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19835
19945
|
return null;
|
|
19836
19946
|
};
|
|
19947
|
+
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
19948
|
+
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
19949
|
+
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
19950
|
+
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19837
19951
|
const parseShadowLayerBlur = (layer) => {
|
|
19838
|
-
const
|
|
19839
|
-
|
|
19952
|
+
const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
|
|
19953
|
+
let tokenIndex = 0;
|
|
19954
|
+
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
19955
|
+
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
19956
|
+
tokenIndex += 1;
|
|
19957
|
+
}
|
|
19958
|
+
return 0;
|
|
19840
19959
|
};
|
|
19841
19960
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19842
19961
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19956,7 +20075,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19956
20075
|
};
|
|
19957
20076
|
//#endregion
|
|
19958
20077
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20078
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19959
20079
|
const collectComponentPropNames = (componentFunction) => {
|
|
20080
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20081
|
+
if (cached) return cached;
|
|
19960
20082
|
const propNames = /* @__PURE__ */ new Set();
|
|
19961
20083
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19962
20084
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19970,27 +20092,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19970
20092
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19971
20093
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19972
20094
|
});
|
|
20095
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19973
20096
|
return propNames;
|
|
19974
20097
|
};
|
|
19975
|
-
const
|
|
20098
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20099
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20100
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20101
|
+
if (cached) return cached;
|
|
19976
20102
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19977
20103
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19978
|
-
if (boundNames.has(bindingName)) return true;
|
|
19979
|
-
let declaresName = false;
|
|
19980
20104
|
walkAst(functionNode, (child) => {
|
|
19981
|
-
if (declaresName) return false;
|
|
19982
20105
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
19983
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
19984
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
19985
|
-
collectPatternNames(child.id, declaratorNames);
|
|
19986
|
-
if (declaratorNames.has(bindingName)) {
|
|
19987
|
-
declaresName = true;
|
|
19988
|
-
return false;
|
|
19989
|
-
}
|
|
19990
|
-
}
|
|
20106
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
19991
20107
|
});
|
|
19992
|
-
|
|
20108
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20109
|
+
return boundNames;
|
|
19993
20110
|
};
|
|
20111
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
19994
20112
|
const isPropertyNamePosition = (identifier) => {
|
|
19995
20113
|
const parent = identifier.parent;
|
|
19996
20114
|
if (!parent) return false;
|
|
@@ -20413,36 +20531,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20413
20531
|
}
|
|
20414
20532
|
return hasNestedFunction;
|
|
20415
20533
|
};
|
|
20416
|
-
const
|
|
20417
|
-
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
|
|
20421
|
-
|
|
20422
|
-
|
|
20423
|
-
if (isReseeded) return false;
|
|
20424
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20425
|
-
isReseeded = true;
|
|
20426
|
-
return false;
|
|
20427
|
-
}
|
|
20428
|
-
});
|
|
20429
|
-
return isReseeded;
|
|
20534
|
+
const isInRenderScope = (node, componentFunction) => {
|
|
20535
|
+
let cursor = node.parent ?? null;
|
|
20536
|
+
while (cursor && cursor !== componentFunction) {
|
|
20537
|
+
if (isFunctionLike$1(cursor)) return false;
|
|
20538
|
+
cursor = cursor.parent ?? null;
|
|
20539
|
+
}
|
|
20540
|
+
return true;
|
|
20430
20541
|
};
|
|
20431
|
-
const
|
|
20542
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20432
20543
|
const setterName = getStateSetterName(useStateCall);
|
|
20433
20544
|
if (!setterName) return false;
|
|
20434
20545
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20435
20546
|
if (!componentFunction) return false;
|
|
20436
|
-
let
|
|
20547
|
+
let isExempt = false;
|
|
20437
20548
|
walkAst(componentFunction, (child) => {
|
|
20438
|
-
if (
|
|
20439
|
-
if (child
|
|
20440
|
-
|
|
20441
|
-
isAdjusted = true;
|
|
20549
|
+
if (isExempt) return false;
|
|
20550
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
|
|
20551
|
+
isExempt = true;
|
|
20442
20552
|
return false;
|
|
20443
20553
|
}
|
|
20444
20554
|
});
|
|
20445
|
-
return
|
|
20555
|
+
return isExempt;
|
|
20446
20556
|
};
|
|
20447
20557
|
const noDerivedUseState = defineRule({
|
|
20448
20558
|
id: "no-derived-useState",
|
|
@@ -20459,8 +20569,7 @@ const noDerivedUseState = defineRule({
|
|
|
20459
20569
|
const initializer = node.arguments[0];
|
|
20460
20570
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20461
20571
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20462
|
-
if (
|
|
20463
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20572
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20464
20573
|
context.report({
|
|
20465
20574
|
node,
|
|
20466
20575
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20471,8 +20580,7 @@ const noDerivedUseState = defineRule({
|
|
|
20471
20580
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20472
20581
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20473
20582
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20474
|
-
if (
|
|
20475
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20583
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20476
20584
|
context.report({
|
|
20477
20585
|
node,
|
|
20478
20586
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22171,20 +22279,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22171
22279
|
return null;
|
|
22172
22280
|
};
|
|
22173
22281
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22282
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22283
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22284
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22285
|
+
walkAst(componentBody, (child) => {
|
|
22286
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22287
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22288
|
+
const setterName = child.callee.name;
|
|
22289
|
+
if (!setterNames.has(setterName)) return;
|
|
22290
|
+
settersWithAnyCall.add(setterName);
|
|
22291
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22292
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22293
|
+
});
|
|
22174
22294
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22175
|
-
for (const binding of useStateBindings)
|
|
22176
|
-
let didFindAnySetterCall = false;
|
|
22177
|
-
let areAllSetterCallsInHandlers = true;
|
|
22178
|
-
walkAst(componentBody, (child) => {
|
|
22179
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22180
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22181
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22182
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22183
|
-
didFindAnySetterCall = true;
|
|
22184
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22185
|
-
});
|
|
22186
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22187
|
-
}
|
|
22295
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22188
22296
|
return handlerOnlyWriteStateNames;
|
|
22189
22297
|
};
|
|
22190
22298
|
const noEventTriggerState = defineRule({
|
|
@@ -22524,6 +22632,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22524
22632
|
const TEXT_COLOR_PATTERN = /^text-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22525
22633
|
const BG_COLOR_PATTERN = /^bg-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22526
22634
|
const splitVariantScope = (token) => {
|
|
22635
|
+
if (!token.includes(":")) return {
|
|
22636
|
+
scope: "",
|
|
22637
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22638
|
+
};
|
|
22527
22639
|
const segments = token.split(":");
|
|
22528
22640
|
const rawUtility = segments[segments.length - 1];
|
|
22529
22641
|
return {
|
|
@@ -22547,6 +22659,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22547
22659
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22548
22660
|
for (const token of classStr.split(/\s+/)) {
|
|
22549
22661
|
if (!token) continue;
|
|
22662
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22550
22663
|
const { scope, utility } = splitVariantScope(token);
|
|
22551
22664
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22552
22665
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23276,6 +23389,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23276
23389
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23277
23390
|
});
|
|
23278
23391
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23392
|
+
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23393
|
+
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23279
23394
|
const noLongTransitionDuration = defineRule({
|
|
23280
23395
|
id: "no-long-transition-duration",
|
|
23281
23396
|
title: "Transition duration too long",
|
|
@@ -23299,11 +23414,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23299
23414
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23300
23415
|
let longestDurationPropertyMs = 0;
|
|
23301
23416
|
for (const segment of value.split(",")) {
|
|
23302
|
-
const
|
|
23303
|
-
|
|
23304
|
-
const
|
|
23305
|
-
|
|
23306
|
-
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23417
|
+
const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
|
|
23418
|
+
if (!durationMatch) continue;
|
|
23419
|
+
const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
|
|
23420
|
+
longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
|
|
23307
23421
|
}
|
|
23308
23422
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23309
23423
|
}
|
|
@@ -23311,7 +23425,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23311
23425
|
let longestDurationMs = 0;
|
|
23312
23426
|
for (const segment of value.split(",")) {
|
|
23313
23427
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23314
|
-
const firstTimeMatch = segment.match(
|
|
23428
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23315
23429
|
if (!firstTimeMatch) continue;
|
|
23316
23430
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23317
23431
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -26069,14 +26183,14 @@ const noRenderInRender = defineRule({
|
|
|
26069
26183
|
const expression = node.expression;
|
|
26070
26184
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26071
26185
|
let calleeName = null;
|
|
26186
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26187
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26188
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26072
26189
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26073
26190
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26074
|
-
|
|
26075
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26191
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26076
26192
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26077
|
-
calleeName = expression.callee.property.name;
|
|
26078
26193
|
}
|
|
26079
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26080
26194
|
context.report({
|
|
26081
26195
|
node: expression,
|
|
26082
26196
|
message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
|
|
@@ -28791,32 +28905,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28791
28905
|
if (!isHocCallee$1(parent)) return false;
|
|
28792
28906
|
return parent.arguments[0] === node;
|
|
28793
28907
|
};
|
|
28908
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
28909
|
+
"map",
|
|
28910
|
+
"forEach",
|
|
28911
|
+
"filter",
|
|
28912
|
+
"flatMap",
|
|
28913
|
+
"reduce",
|
|
28914
|
+
"reduceRight"
|
|
28915
|
+
]);
|
|
28794
28916
|
const isReturnOfMapCallback = (node) => {
|
|
28795
28917
|
const parent = node.parent;
|
|
28796
28918
|
if (!parent) return false;
|
|
28797
28919
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28798
28920
|
const callee = parent.callee;
|
|
28799
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28800
|
-
"map",
|
|
28801
|
-
"forEach",
|
|
28802
|
-
"filter",
|
|
28803
|
-
"flatMap",
|
|
28804
|
-
"reduce",
|
|
28805
|
-
"reduceRight"
|
|
28806
|
-
].includes(callee.property.name);
|
|
28921
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28807
28922
|
}
|
|
28808
28923
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28809
28924
|
const callbackParent = parent.parent;
|
|
28810
28925
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28811
28926
|
const callee = callbackParent.callee;
|
|
28812
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28813
|
-
"map",
|
|
28814
|
-
"forEach",
|
|
28815
|
-
"filter",
|
|
28816
|
-
"flatMap",
|
|
28817
|
-
"reduce",
|
|
28818
|
-
"reduceRight"
|
|
28819
|
-
].includes(callee.property.name);
|
|
28927
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28820
28928
|
}
|
|
28821
28929
|
}
|
|
28822
28930
|
return false;
|
|
@@ -28852,10 +28960,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28852
28960
|
valueNode = parent;
|
|
28853
28961
|
parent = parent.parent;
|
|
28854
28962
|
continue;
|
|
28855
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
28963
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
|
|
28856
28964
|
case "AssignmentExpression": {
|
|
28857
28965
|
const assignmentTarget = parent.left;
|
|
28858
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
28966
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28859
28967
|
}
|
|
28860
28968
|
case "Property":
|
|
28861
28969
|
if (parent.value !== valueNode) return false;
|
|
@@ -28948,14 +29056,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28948
29056
|
};
|
|
28949
29057
|
return {
|
|
28950
29058
|
JSXOpeningElement(node) {
|
|
28951
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29059
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28952
29060
|
recordInstantiation(node.name, node.name.name);
|
|
28953
29061
|
return;
|
|
28954
29062
|
}
|
|
28955
29063
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28956
29064
|
},
|
|
28957
29065
|
Identifier(node) {
|
|
28958
|
-
if (!
|
|
29066
|
+
if (!isReactComponentName(node.name)) return;
|
|
28959
29067
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28960
29068
|
recordInstantiation(node, node.name);
|
|
28961
29069
|
},
|
|
@@ -29728,10 +29836,10 @@ const onlyExportComponents = defineRule({
|
|
|
29728
29836
|
};
|
|
29729
29837
|
for (const child of componentCandidates) {
|
|
29730
29838
|
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
29731
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child)) localComponents.push(child.id);
|
|
29839
|
+
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29732
29840
|
}
|
|
29733
29841
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
29734
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child)) localComponents.push(child.id);
|
|
29842
|
+
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
29735
29843
|
}
|
|
29736
29844
|
}
|
|
29737
29845
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
@@ -30765,13 +30873,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30765
30873
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30766
30874
|
},
|
|
30767
30875
|
JSXAttribute(node) {
|
|
30768
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30769
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30770
30876
|
if (!node.value) return;
|
|
30771
30877
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30772
30878
|
const innerExpression = node.value.expression;
|
|
30773
30879
|
if (!innerExpression) return;
|
|
30774
30880
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
30881
|
+
if (!isInsideFunctionScope(node)) return;
|
|
30882
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30775
30883
|
const parentJsxOpening = node.parent;
|
|
30776
30884
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30777
30885
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -32143,41 +32251,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32143
32251
|
});
|
|
32144
32252
|
return found;
|
|
32145
32253
|
};
|
|
32146
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32147
|
-
if (!componentBody) return false;
|
|
32148
|
-
let found = false;
|
|
32149
|
-
walkAst(componentBody, (child) => {
|
|
32150
|
-
if (found) return;
|
|
32151
|
-
if (!isFunctionLike$1(child)) return;
|
|
32152
|
-
const functionBody = child.body;
|
|
32153
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32154
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32155
|
-
});
|
|
32156
|
-
return found;
|
|
32157
|
-
};
|
|
32158
32254
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32159
32255
|
"then",
|
|
32160
32256
|
"catch",
|
|
32161
32257
|
"finally"
|
|
32162
32258
|
]);
|
|
32163
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32164
|
-
if (!componentBody) return false;
|
|
32165
|
-
let found = false;
|
|
32166
|
-
walkAst(componentBody, (child) => {
|
|
32167
|
-
if (found) return;
|
|
32168
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32169
|
-
const callee = child.callee;
|
|
32170
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32171
|
-
for (const argument of child.arguments ?? []) {
|
|
32172
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32173
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32174
|
-
found = true;
|
|
32175
|
-
return;
|
|
32176
|
-
}
|
|
32177
|
-
}
|
|
32178
|
-
});
|
|
32179
|
-
return found;
|
|
32180
|
-
};
|
|
32181
32259
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32182
32260
|
"useApolloClient",
|
|
32183
32261
|
"useMutation",
|
|
@@ -32190,20 +32268,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32190
32268
|
"fetch",
|
|
32191
32269
|
"axios"
|
|
32192
32270
|
]);
|
|
32193
|
-
const
|
|
32194
|
-
if (!body) return false;
|
|
32271
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32195
32272
|
let found = false;
|
|
32196
|
-
walkAst(
|
|
32197
|
-
if (found) return;
|
|
32273
|
+
walkAst(fnBody, (child) => {
|
|
32274
|
+
if (found) return false;
|
|
32198
32275
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32199
32276
|
const callee = child.callee;
|
|
32200
32277
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32201
32278
|
found = true;
|
|
32202
|
-
return;
|
|
32279
|
+
return false;
|
|
32280
|
+
}
|
|
32281
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32282
|
+
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32283
|
+
found = true;
|
|
32284
|
+
return false;
|
|
32285
|
+
}
|
|
32286
|
+
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32287
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32288
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32289
|
+
found = true;
|
|
32290
|
+
return false;
|
|
32291
|
+
}
|
|
32292
|
+
}
|
|
32203
32293
|
}
|
|
32204
|
-
|
|
32294
|
+
return;
|
|
32295
|
+
}
|
|
32296
|
+
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32297
|
+
const functionBody = child.body;
|
|
32298
|
+
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32205
32299
|
found = true;
|
|
32206
|
-
return;
|
|
32300
|
+
return false;
|
|
32207
32301
|
}
|
|
32208
32302
|
}
|
|
32209
32303
|
});
|
|
@@ -32228,7 +32322,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32228
32322
|
const secondBinding = node.id.elements[1];
|
|
32229
32323
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32230
32324
|
const fnBody = enclosingFunctionBody(node);
|
|
32231
|
-
if (fnBody && (
|
|
32325
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32232
32326
|
context.report({
|
|
32233
32327
|
node: node.init,
|
|
32234
32328
|
message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
|
|
@@ -33022,17 +33116,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33022
33116
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33023
33117
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33024
33118
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33119
|
+
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33120
|
+
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33121
|
+
walkAst(componentBody, (child) => {
|
|
33122
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33123
|
+
});
|
|
33025
33124
|
for (const binding of bindings) {
|
|
33026
33125
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33027
33126
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33028
33127
|
const setterSuffix = binding.setterName.slice(3);
|
|
33029
33128
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33030
|
-
|
|
33031
|
-
walkAst(componentBody, (child) => {
|
|
33032
|
-
if (setterCalled) return;
|
|
33033
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33034
|
-
});
|
|
33035
|
-
if (!setterCalled) continue;
|
|
33129
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33036
33130
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33037
33131
|
context.report({
|
|
33038
33132
|
node: binding.declarator,
|
|
@@ -33364,6 +33458,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33364
33458
|
FlashList: ["@shopify/flash-list"],
|
|
33365
33459
|
LegendList: ["@legendapp/list"]
|
|
33366
33460
|
};
|
|
33461
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33367
33462
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33368
33463
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33369
33464
|
"renderItem",
|
|
@@ -33606,33 +33701,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33606
33701
|
requires: ["react-native"],
|
|
33607
33702
|
severity: "warn",
|
|
33608
33703
|
recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
|
|
33609
|
-
create: (context) =>
|
|
33610
|
-
|
|
33611
|
-
|
|
33612
|
-
|
|
33613
|
-
|
|
33614
|
-
|
|
33615
|
-
|
|
33616
|
-
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
|
|
33623
|
-
|
|
33624
|
-
hasDataProp =
|
|
33625
|
-
|
|
33704
|
+
create: (context) => {
|
|
33705
|
+
let fileImportsRecycler = false;
|
|
33706
|
+
return {
|
|
33707
|
+
Program(node) {
|
|
33708
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33709
|
+
},
|
|
33710
|
+
JSXOpeningElement(node) {
|
|
33711
|
+
if (!fileImportsRecycler) return;
|
|
33712
|
+
const localElementName = resolveJsxElementName(node);
|
|
33713
|
+
if (!localElementName) return;
|
|
33714
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
33715
|
+
if (canonicalRecyclerName === null) return;
|
|
33716
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
33717
|
+
let hasSizingHint = false;
|
|
33718
|
+
let dataIsEmptyLiteral = false;
|
|
33719
|
+
let hasDataProp = false;
|
|
33720
|
+
for (const attribute of node.attributes ?? []) {
|
|
33721
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
33722
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
33723
|
+
const attributeName = attribute.name.name;
|
|
33724
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
33725
|
+
if (attributeName === "data") {
|
|
33726
|
+
hasDataProp = true;
|
|
33727
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
33728
|
+
}
|
|
33729
|
+
}
|
|
33730
|
+
if (hasSizingHint) return;
|
|
33731
|
+
if (dataIsEmptyLiteral) return;
|
|
33732
|
+
if (!hasDataProp) return;
|
|
33733
|
+
context.report({
|
|
33734
|
+
node,
|
|
33735
|
+
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33736
|
+
});
|
|
33626
33737
|
}
|
|
33627
|
-
}
|
|
33628
|
-
|
|
33629
|
-
if (dataIsEmptyLiteral) return;
|
|
33630
|
-
if (!hasDataProp) return;
|
|
33631
|
-
context.report({
|
|
33632
|
-
node,
|
|
33633
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33634
|
-
});
|
|
33635
|
-
} })
|
|
33738
|
+
};
|
|
33739
|
+
}
|
|
33636
33740
|
});
|
|
33637
33741
|
//#endregion
|
|
33638
33742
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33643,25 +33747,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33643
33747
|
requires: ["react-native"],
|
|
33644
33748
|
severity: "warn",
|
|
33645
33749
|
recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
|
|
33646
|
-
create: (context) =>
|
|
33647
|
-
|
|
33648
|
-
|
|
33649
|
-
|
|
33650
|
-
|
|
33651
|
-
|
|
33652
|
-
|
|
33653
|
-
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
33663
|
-
|
|
33664
|
-
|
|
33750
|
+
create: (context) => {
|
|
33751
|
+
let fileImportsRecycler = false;
|
|
33752
|
+
return {
|
|
33753
|
+
Program(node) {
|
|
33754
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33755
|
+
},
|
|
33756
|
+
JSXOpeningElement(node) {
|
|
33757
|
+
if (!fileImportsRecycler) return;
|
|
33758
|
+
const elementName = resolveJsxElementName(node);
|
|
33759
|
+
if (!elementName) return;
|
|
33760
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
33761
|
+
let hasRecycleItemsEnabled = false;
|
|
33762
|
+
let hasGetItemType = false;
|
|
33763
|
+
for (const attr of node.attributes ?? []) {
|
|
33764
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
33765
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
33766
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
33767
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
33768
|
+
else hasRecycleItemsEnabled = true;
|
|
33769
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
33770
|
+
}
|
|
33771
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
33772
|
+
node,
|
|
33773
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
33774
|
+
});
|
|
33775
|
+
}
|
|
33776
|
+
};
|
|
33777
|
+
}
|
|
33665
33778
|
});
|
|
33666
33779
|
//#endregion
|
|
33667
33780
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -34691,6 +34804,7 @@ const rnNoRawText = defineRule({
|
|
|
34691
34804
|
return {
|
|
34692
34805
|
Program(programNode) {
|
|
34693
34806
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
34807
|
+
if (!containsJsxElement(programNode)) return;
|
|
34694
34808
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34695
34809
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34696
34810
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38645,14 +38759,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38645
38759
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38646
38760
|
category: "Accessibility",
|
|
38647
38761
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38648
|
-
|
|
38649
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38650
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38651
|
-
if (!role) return;
|
|
38652
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38653
|
-
const isImplicit = !roleAttribute;
|
|
38654
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38655
|
-
if (!supported) return;
|
|
38762
|
+
let ariaAttributes = null;
|
|
38656
38763
|
for (const attribute of node.attributes) {
|
|
38657
38764
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38658
38765
|
const attributeNode = attribute;
|
|
@@ -38662,6 +38769,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38662
38769
|
const propName = propRawName.toLowerCase();
|
|
38663
38770
|
if (!propName.startsWith("aria-")) continue;
|
|
38664
38771
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
38772
|
+
(ariaAttributes ??= []).push({
|
|
38773
|
+
attribute,
|
|
38774
|
+
propName
|
|
38775
|
+
});
|
|
38776
|
+
}
|
|
38777
|
+
if (!ariaAttributes) return;
|
|
38778
|
+
const elementType = getElementType(node, context.settings);
|
|
38779
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38780
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38781
|
+
if (!role) return;
|
|
38782
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38783
|
+
const isImplicit = !roleAttribute;
|
|
38784
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38785
|
+
if (!supported) return;
|
|
38786
|
+
for (const { attribute, propName } of ariaAttributes) {
|
|
38665
38787
|
if (supported.has(propName)) continue;
|
|
38666
38788
|
context.report({
|
|
38667
38789
|
node: attribute,
|
|
@@ -39499,6 +39621,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39499
39621
|
cachedFunctionNames.add(node.id.name);
|
|
39500
39622
|
},
|
|
39501
39623
|
CallExpression(node) {
|
|
39624
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39502
39625
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39503
39626
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39504
39627
|
const firstArg = node.arguments?.[0];
|
|
@@ -40425,7 +40548,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40425
40548
|
//#endregion
|
|
40426
40549
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40427
40550
|
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
40428
|
-
const
|
|
40551
|
+
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;
|
|
40552
|
+
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40553
|
+
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40554
|
+
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40555
|
+
const tableName = match[1];
|
|
40556
|
+
if (tableName === void 0) continue;
|
|
40557
|
+
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40558
|
+
}
|
|
40559
|
+
return lastEnableIndexByTable;
|
|
40560
|
+
};
|
|
40429
40561
|
const supabaseTableMissingRls = defineRule({
|
|
40430
40562
|
id: "supabase-table-missing-rls",
|
|
40431
40563
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40436,11 +40568,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40436
40568
|
const content = sanitizeSqlForScan(file.content);
|
|
40437
40569
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40438
40570
|
const findings = [];
|
|
40571
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40439
40572
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40440
40573
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40441
40574
|
const tableName = match[1];
|
|
40442
40575
|
if (tableName === void 0) continue;
|
|
40443
|
-
|
|
40576
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40577
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40444
40578
|
const location = getLocationAtIndex(content, match.index);
|
|
40445
40579
|
findings.push({
|
|
40446
40580
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40716,6 +40850,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40716
40850
|
severity: "warn",
|
|
40717
40851
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40718
40852
|
create: (context) => {
|
|
40853
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40719
40854
|
let hasHeadContentElement = false;
|
|
40720
40855
|
let hasDocumentHeadElement = false;
|
|
40721
40856
|
let hasCustomHeadChildElement = false;
|
|
@@ -40752,8 +40887,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40752
40887
|
};
|
|
40753
40888
|
return {
|
|
40754
40889
|
Program(node) {
|
|
40755
|
-
const filename = context.filename ?? "";
|
|
40756
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40757
40890
|
const statements = node.body ?? [];
|
|
40758
40891
|
for (const statement of statements) collectImportBindings(statement);
|
|
40759
40892
|
for (const statement of statements) {
|
|
@@ -40762,18 +40895,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40762
40895
|
}
|
|
40763
40896
|
},
|
|
40764
40897
|
ImportDeclaration(node) {
|
|
40765
|
-
const filename = context.filename ?? "";
|
|
40766
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40767
40898
|
collectImportBindings(node);
|
|
40768
40899
|
},
|
|
40769
40900
|
VariableDeclarator(node) {
|
|
40770
|
-
const filename = context.filename ?? "";
|
|
40771
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40772
40901
|
collectVariableAlias(node);
|
|
40773
40902
|
},
|
|
40774
40903
|
JSXOpeningElement(node) {
|
|
40775
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40776
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40777
40904
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40778
40905
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40779
40906
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40787,8 +40914,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40787
40914
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40788
40915
|
},
|
|
40789
40916
|
"Program:exit"(programNode) {
|
|
40790
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40791
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40792
40917
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40793
40918
|
node: programNode,
|
|
40794
40919
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40812,27 +40937,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40812
40937
|
requires: ["tanstack-start"],
|
|
40813
40938
|
severity: "warn",
|
|
40814
40939
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40815
|
-
create: (context) =>
|
|
40816
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40817
|
-
|
|
40818
|
-
|
|
40819
|
-
|
|
40820
|
-
|
|
40821
|
-
|
|
40822
|
-
|
|
40823
|
-
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
40830
|
-
|
|
40831
|
-
|
|
40832
|
-
|
|
40833
|
-
|
|
40834
|
-
|
|
40835
|
-
|
|
40940
|
+
create: (context) => {
|
|
40941
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40942
|
+
return { JSXOpeningElement(node) {
|
|
40943
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
40944
|
+
const attributes = node.attributes ?? [];
|
|
40945
|
+
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
40946
|
+
if (!hrefAttribute?.value) return;
|
|
40947
|
+
let hrefValue = null;
|
|
40948
|
+
if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
|
|
40949
|
+
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
40950
|
+
if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
|
|
40951
|
+
if (hrefValue.startsWith("//")) return;
|
|
40952
|
+
const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
|
|
40953
|
+
if (pathname.startsWith("/api/")) return;
|
|
40954
|
+
if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
|
|
40955
|
+
if (findJsxAttribute(attributes, "download")) return;
|
|
40956
|
+
if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
|
|
40957
|
+
context.report({
|
|
40958
|
+
node,
|
|
40959
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
40960
|
+
});
|
|
40961
|
+
} };
|
|
40962
|
+
}
|
|
40836
40963
|
});
|
|
40837
40964
|
//#endregion
|
|
40838
40965
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40896,7 +41023,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40896
41023
|
severity: "warn",
|
|
40897
41024
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40898
41025
|
create: (context) => {
|
|
40899
|
-
|
|
41026
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40900
41027
|
let deferredCallbackDepth = 0;
|
|
40901
41028
|
let eventHandlerDepth = 0;
|
|
40902
41029
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40960,7 +41087,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40960
41087
|
};
|
|
40961
41088
|
return {
|
|
40962
41089
|
CallExpression(node) {
|
|
40963
|
-
if (!isRouteFile) return;
|
|
40964
41090
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40965
41091
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40966
41092
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40972,39 +41098,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40972
41098
|
}
|
|
40973
41099
|
},
|
|
40974
41100
|
"CallExpression:exit"(node) {
|
|
40975
|
-
if (!isRouteFile) return;
|
|
40976
41101
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40977
41102
|
},
|
|
40978
41103
|
JSXAttribute(node) {
|
|
40979
|
-
if (!isRouteFile) return;
|
|
40980
41104
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40981
41105
|
},
|
|
40982
41106
|
"JSXAttribute:exit"(node) {
|
|
40983
|
-
if (!isRouteFile) return;
|
|
40984
41107
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40985
41108
|
},
|
|
40986
41109
|
Property(node) {
|
|
40987
|
-
if (!isRouteFile) return;
|
|
40988
41110
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40989
41111
|
},
|
|
40990
41112
|
"Property:exit"(node) {
|
|
40991
|
-
if (!isRouteFile) return;
|
|
40992
41113
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40993
41114
|
},
|
|
40994
41115
|
VariableDeclarator(node) {
|
|
40995
|
-
if (!isRouteFile) return;
|
|
40996
41116
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
40997
41117
|
},
|
|
40998
41118
|
"VariableDeclarator:exit"(node) {
|
|
40999
|
-
if (!isRouteFile) return;
|
|
41000
41119
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41001
41120
|
},
|
|
41002
41121
|
FunctionDeclaration(node) {
|
|
41003
|
-
if (!isRouteFile) return;
|
|
41004
41122
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41005
41123
|
},
|
|
41006
41124
|
"FunctionDeclaration:exit"(node) {
|
|
41007
|
-
if (!isRouteFile) return;
|
|
41008
41125
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41009
41126
|
}
|
|
41010
41127
|
};
|
|
@@ -41088,23 +41205,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41088
41205
|
requires: ["tanstack-start"],
|
|
41089
41206
|
severity: "warn",
|
|
41090
41207
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41091
|
-
create: (context) =>
|
|
41092
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41093
|
-
|
|
41094
|
-
|
|
41095
|
-
|
|
41096
|
-
|
|
41097
|
-
|
|
41098
|
-
|
|
41099
|
-
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41208
|
+
create: (context) => {
|
|
41209
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41210
|
+
return { CallExpression(node) {
|
|
41211
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41212
|
+
const callback = node.arguments?.[0];
|
|
41213
|
+
if (!callback) return;
|
|
41214
|
+
let hasFetchCall = false;
|
|
41215
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
41216
|
+
walkAst(callback, (child) => {
|
|
41217
|
+
if (hasFetchCall) return false;
|
|
41218
|
+
if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
41219
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
|
|
41220
|
+
});
|
|
41221
|
+
if (hasFetchCall) context.report({
|
|
41222
|
+
node,
|
|
41223
|
+
message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
|
|
41224
|
+
});
|
|
41225
|
+
} };
|
|
41226
|
+
}
|
|
41108
41227
|
});
|
|
41109
41228
|
//#endregion
|
|
41110
41229
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41472,6 +41591,7 @@ const webhookSignatureRisk = defineRule({
|
|
|
41472
41591
|
//#endregion
|
|
41473
41592
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41474
41593
|
const ZOD_MODULE = "zod";
|
|
41594
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41475
41595
|
const getStaticPropertyName = (member) => {
|
|
41476
41596
|
const property = member.property;
|
|
41477
41597
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
@@ -41615,25 +41735,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41615
41735
|
tags: ["migration-hint"],
|
|
41616
41736
|
severity: "warn",
|
|
41617
41737
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41618
|
-
create: (context) =>
|
|
41619
|
-
|
|
41620
|
-
|
|
41621
|
-
|
|
41622
|
-
|
|
41623
|
-
|
|
41624
|
-
|
|
41625
|
-
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41738
|
+
create: (context) => {
|
|
41739
|
+
let fileImportsZod = false;
|
|
41740
|
+
return {
|
|
41741
|
+
Program(node) {
|
|
41742
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
41743
|
+
},
|
|
41744
|
+
CallExpression(node) {
|
|
41745
|
+
if (!fileImportsZod) return;
|
|
41746
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
41747
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
41748
|
+
context.report({
|
|
41749
|
+
node,
|
|
41750
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41751
|
+
});
|
|
41752
|
+
},
|
|
41753
|
+
MemberExpression(node) {
|
|
41754
|
+
if (!fileImportsZod) return;
|
|
41755
|
+
const parent = node.parent;
|
|
41756
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41757
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
41758
|
+
context.report({
|
|
41759
|
+
node,
|
|
41760
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41761
|
+
});
|
|
41762
|
+
}
|
|
41763
|
+
};
|
|
41764
|
+
}
|
|
41637
41765
|
});
|
|
41638
41766
|
//#endregion
|
|
41639
41767
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41825,16 +41953,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41825
41953
|
tags: ["migration-hint"],
|
|
41826
41954
|
severity: "warn",
|
|
41827
41955
|
recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
|
|
41828
|
-
create: (context) =>
|
|
41829
|
-
|
|
41830
|
-
|
|
41831
|
-
|
|
41832
|
-
|
|
41833
|
-
|
|
41834
|
-
|
|
41835
|
-
|
|
41836
|
-
|
|
41837
|
-
|
|
41956
|
+
create: (context) => {
|
|
41957
|
+
let fileImportsZod = false;
|
|
41958
|
+
return {
|
|
41959
|
+
Program(node) {
|
|
41960
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
41961
|
+
},
|
|
41962
|
+
CallExpression(node) {
|
|
41963
|
+
if (!fileImportsZod) return;
|
|
41964
|
+
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);
|
|
41965
|
+
},
|
|
41966
|
+
MemberExpression(node) {
|
|
41967
|
+
if (!fileImportsZod) return;
|
|
41968
|
+
const parent = node.parent;
|
|
41969
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41970
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
41971
|
+
}
|
|
41972
|
+
};
|
|
41973
|
+
}
|
|
41838
41974
|
});
|
|
41839
41975
|
//#endregion
|
|
41840
41976
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41869,13 +42005,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41869
42005
|
tags: ["migration-hint"],
|
|
41870
42006
|
severity: "warn",
|
|
41871
42007
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41872
|
-
create: (context) =>
|
|
41873
|
-
|
|
41874
|
-
|
|
41875
|
-
node
|
|
41876
|
-
|
|
41877
|
-
|
|
41878
|
-
|
|
42008
|
+
create: (context) => {
|
|
42009
|
+
let fileImportsZod = false;
|
|
42010
|
+
return {
|
|
42011
|
+
Program(node) {
|
|
42012
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42013
|
+
},
|
|
42014
|
+
CallExpression(node) {
|
|
42015
|
+
if (!fileImportsZod) return;
|
|
42016
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42017
|
+
context.report({
|
|
42018
|
+
node,
|
|
42019
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42020
|
+
});
|
|
42021
|
+
}
|
|
42022
|
+
};
|
|
42023
|
+
}
|
|
41879
42024
|
});
|
|
41880
42025
|
//#endregion
|
|
41881
42026
|
//#region src/plugin/rule-registry.ts
|