oxlint-plugin-react-doctor 0.6.2-dev.b4faf74 → 0.6.2-dev.d8628d7
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 +673 -495
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -294,15 +294,38 @@ const defineRule = (rule) => {
|
|
|
294
294
|
};
|
|
295
295
|
//#endregion
|
|
296
296
|
//#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
|
|
297
|
+
let lastContentLineIndex;
|
|
298
|
+
const buildLineStartOffsets = (content) => {
|
|
299
|
+
const lineStartOffsets = [0];
|
|
300
|
+
for (let newlineIndex = content.indexOf("\n"); newlineIndex !== -1; newlineIndex = content.indexOf("\n", newlineIndex + 1)) lineStartOffsets.push(newlineIndex + 1);
|
|
301
|
+
return lineStartOffsets;
|
|
302
|
+
};
|
|
303
|
+
const getLineStartOffsets = (content) => {
|
|
304
|
+
if (lastContentLineIndex?.content === content) return lastContentLineIndex.lineStartOffsets;
|
|
305
|
+
const lineStartOffsets = buildLineStartOffsets(content);
|
|
306
|
+
lastContentLineIndex = {
|
|
307
|
+
content,
|
|
308
|
+
lineStartOffsets
|
|
309
|
+
};
|
|
310
|
+
return lineStartOffsets;
|
|
311
|
+
};
|
|
297
312
|
const getLocationAtIndex = (content, matchIndex) => {
|
|
298
|
-
if (matchIndex < 0) return {
|
|
313
|
+
if (Number.isNaN(matchIndex) || matchIndex < 0) return {
|
|
299
314
|
line: 1,
|
|
300
315
|
column: 1
|
|
301
316
|
};
|
|
302
|
-
const
|
|
317
|
+
const lineStartOffsets = getLineStartOffsets(content);
|
|
318
|
+
const boundedIndex = Math.min(Math.trunc(matchIndex), content.length);
|
|
319
|
+
let lowLineIndex = 0;
|
|
320
|
+
let highLineIndex = lineStartOffsets.length - 1;
|
|
321
|
+
while (lowLineIndex < highLineIndex) {
|
|
322
|
+
const midLineIndex = lowLineIndex + highLineIndex + 1 >> 1;
|
|
323
|
+
if (lineStartOffsets[midLineIndex] <= boundedIndex) lowLineIndex = midLineIndex;
|
|
324
|
+
else highLineIndex = midLineIndex - 1;
|
|
325
|
+
}
|
|
303
326
|
return {
|
|
304
|
-
line:
|
|
305
|
-
column:
|
|
327
|
+
line: lowLineIndex + 1,
|
|
328
|
+
column: boundedIndex - lineStartOffsets[lowLineIndex] + 1
|
|
306
329
|
};
|
|
307
330
|
};
|
|
308
331
|
//#endregion
|
|
@@ -355,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
|
355
378
|
const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
|
|
356
379
|
//#endregion
|
|
357
380
|
//#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
|
|
381
|
+
const classificationCacheByPattern = /* @__PURE__ */ new Map();
|
|
358
382
|
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
359
|
-
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
383
|
+
let classificationByPath = classificationCacheByPattern.get(sourceFilePattern);
|
|
384
|
+
if (!classificationByPath) {
|
|
385
|
+
classificationByPath = /* @__PURE__ */ new Map();
|
|
386
|
+
classificationCacheByPattern.set(sourceFilePattern, classificationByPath);
|
|
387
|
+
}
|
|
388
|
+
const cached = classificationByPath.get(relativePath);
|
|
389
|
+
if (cached !== void 0) return cached;
|
|
390
|
+
const isProduction = sourceFilePattern.test(relativePath) && !TEST_CONTEXT_PATTERN.test(relativePath) && !BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(relativePath) && !GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath);
|
|
391
|
+
classificationByPath.set(relativePath, isProduction);
|
|
392
|
+
return isProduction;
|
|
365
393
|
};
|
|
366
394
|
//#endregion
|
|
367
395
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
@@ -760,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
|
|
|
760
788
|
into.add(name);
|
|
761
789
|
});
|
|
762
790
|
};
|
|
763
|
-
const findSameFileComponentBody = (programRoot, componentName) => {
|
|
764
|
-
let foundBody = null;
|
|
765
|
-
walkAst(programRoot, (node) => {
|
|
766
|
-
if (foundBody) return false;
|
|
767
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
|
|
768
|
-
foundBody = node.body;
|
|
769
|
-
return false;
|
|
770
|
-
}
|
|
771
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
|
|
772
|
-
const initializer = node.init;
|
|
773
|
-
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
|
|
774
|
-
foundBody = initializer.body;
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
});
|
|
779
|
-
return foundBody;
|
|
780
|
-
};
|
|
781
791
|
const countEffectHookCalls = (body) => {
|
|
782
792
|
if (!body) return 0;
|
|
783
793
|
let count = 0;
|
|
@@ -787,6 +797,36 @@ const countEffectHookCalls = (body) => {
|
|
|
787
797
|
});
|
|
788
798
|
return count;
|
|
789
799
|
};
|
|
800
|
+
const componentEffectIndexCache = /* @__PURE__ */ new WeakMap();
|
|
801
|
+
const getComponentEffectIndex = (programRoot) => {
|
|
802
|
+
const cached = componentEffectIndexCache.get(programRoot);
|
|
803
|
+
if (cached) return cached;
|
|
804
|
+
const bodyByName = /* @__PURE__ */ new Map();
|
|
805
|
+
walkAst(programRoot, (node) => {
|
|
806
|
+
if (isNodeOfType(node, "FunctionDeclaration") && node.id && !bodyByName.has(node.id.name)) {
|
|
807
|
+
bodyByName.set(node.id.name, node.body);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && !bodyByName.has(node.id.name)) {
|
|
811
|
+
const initializer = node.init;
|
|
812
|
+
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) bodyByName.set(node.id.name, initializer.body);
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
const index = {
|
|
816
|
+
bodyByName,
|
|
817
|
+
effectCountByName: /* @__PURE__ */ new Map()
|
|
818
|
+
};
|
|
819
|
+
componentEffectIndexCache.set(programRoot, index);
|
|
820
|
+
return index;
|
|
821
|
+
};
|
|
822
|
+
const getSameFileComponentEffectCount = (programRoot, componentName) => {
|
|
823
|
+
const index = getComponentEffectIndex(programRoot);
|
|
824
|
+
const cachedCount = index.effectCountByName.get(componentName);
|
|
825
|
+
if (cachedCount !== void 0) return cachedCount;
|
|
826
|
+
const count = countEffectHookCalls(index.bodyByName.get(componentName) ?? null);
|
|
827
|
+
index.effectCountByName.set(componentName, count);
|
|
828
|
+
return count;
|
|
829
|
+
};
|
|
790
830
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
791
831
|
id: "activity-wraps-effect-heavy-subtree",
|
|
792
832
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -843,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
843
883
|
let totalEffects = 0;
|
|
844
884
|
const effectfulChildren = [];
|
|
845
885
|
for (const componentName of childComponentNames) {
|
|
846
|
-
const
|
|
847
|
-
if (!body) continue;
|
|
848
|
-
const effectCount = countEffectHookCalls(body);
|
|
886
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
849
887
|
if (effectCount === 0) continue;
|
|
850
888
|
totalEffects += effectCount;
|
|
851
889
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1394,11 +1432,16 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1394
1432
|
};
|
|
1395
1433
|
//#endregion
|
|
1396
1434
|
//#region src/plugin/utils/get-element-type.ts
|
|
1435
|
+
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1436
|
+
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
1397
1437
|
const readJsxA11ySettings = (settings) => {
|
|
1398
|
-
if (!settings) return
|
|
1438
|
+
if (!settings) return EMPTY_JSX_A11Y_SETTINGS;
|
|
1439
|
+
const cachedSettings = jsxA11ySettingsCache.get(settings);
|
|
1440
|
+
if (cachedSettings) return cachedSettings;
|
|
1399
1441
|
const block = settings["jsx-a11y"];
|
|
1400
|
-
|
|
1401
|
-
|
|
1442
|
+
const a11ySettings = block && typeof block === "object" ? block : EMPTY_JSX_A11Y_SETTINGS;
|
|
1443
|
+
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1444
|
+
return a11ySettings;
|
|
1402
1445
|
};
|
|
1403
1446
|
const flattenJsxName$2 = (name) => {
|
|
1404
1447
|
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
@@ -1406,8 +1449,7 @@ const flattenJsxName$2 = (name) => {
|
|
|
1406
1449
|
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1407
1450
|
return "";
|
|
1408
1451
|
};
|
|
1409
|
-
const
|
|
1410
|
-
const a11ySettings = readJsxA11ySettings(settings);
|
|
1452
|
+
const computeElementType = (openingElement, a11ySettings) => {
|
|
1411
1453
|
const baseName = flattenJsxName$2(openingElement.name);
|
|
1412
1454
|
if (a11ySettings.polymorphicPropName) {
|
|
1413
1455
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
@@ -1419,6 +1461,23 @@ const getElementType = (openingElement, settings) => {
|
|
|
1419
1461
|
if (a11ySettings.components && baseName in a11ySettings.components) return a11ySettings.components[baseName];
|
|
1420
1462
|
return baseName;
|
|
1421
1463
|
};
|
|
1464
|
+
const elementTypeCache = /* @__PURE__ */ new WeakMap();
|
|
1465
|
+
const getElementType = (openingElement, settings) => {
|
|
1466
|
+
const cachedEntry = elementTypeCache.get(openingElement);
|
|
1467
|
+
if (cachedEntry && cachedEntry.settings === settings) return cachedEntry.elementType;
|
|
1468
|
+
const elementType = computeElementType(openingElement, readJsxA11ySettings(settings));
|
|
1469
|
+
elementTypeCache.set(openingElement, {
|
|
1470
|
+
settings,
|
|
1471
|
+
elementType
|
|
1472
|
+
});
|
|
1473
|
+
return elementType;
|
|
1474
|
+
};
|
|
1475
|
+
//#endregion
|
|
1476
|
+
//#region src/plugin/utils/has-jsx-a11y-settings.ts
|
|
1477
|
+
const hasJsxA11ySettings = (settings) => {
|
|
1478
|
+
const block = settings?.["jsx-a11y"];
|
|
1479
|
+
return typeof block === "object" && block !== null;
|
|
1480
|
+
};
|
|
1422
1481
|
//#endregion
|
|
1423
1482
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1424
1483
|
const collectFromProgram = (programRoot) => {
|
|
@@ -1623,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1623
1682
|
};
|
|
1624
1683
|
//#endregion
|
|
1625
1684
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1626
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1685
|
+
const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
|
|
1627
1686
|
//#endregion
|
|
1628
1687
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1629
1688
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -1972,7 +2031,12 @@ const altText = defineRule({
|
|
|
1972
2031
|
const objectAliases = new Set(settings.object ?? []);
|
|
1973
2032
|
const areaAliases = new Set(settings.area ?? []);
|
|
1974
2033
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2034
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
1975
2035
|
return { JSXOpeningElement(node) {
|
|
2036
|
+
if (!fileHasJsxA11ySettings && isNodeOfType(node.name, "JSXIdentifier")) {
|
|
2037
|
+
const rawName = node.name.name;
|
|
2038
|
+
if (rawName !== "img" && rawName !== "object" && rawName !== "area" && rawName.toLowerCase() !== "input" && !imgAliases.has(rawName) && !objectAliases.has(rawName) && !areaAliases.has(rawName) && !inputImageAliases.has(rawName)) return;
|
|
2039
|
+
}
|
|
1976
2040
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
1977
2041
|
const tag = getElementType(node, context.settings);
|
|
1978
2042
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2155,7 +2219,9 @@ const anchorIsValid = defineRule({
|
|
|
2155
2219
|
category: "Accessibility",
|
|
2156
2220
|
create: (context) => {
|
|
2157
2221
|
const settings = resolveSettings$50(context.settings);
|
|
2222
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2158
2223
|
return { JSXOpeningElement(node) {
|
|
2224
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2159
2225
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2160
2226
|
let hrefAttribute;
|
|
2161
2227
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3581,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3581
3647
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3582
3648
|
//#endregion
|
|
3583
3649
|
//#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
|
|
3650
|
+
const PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN = new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi");
|
|
3584
3651
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3585
|
-
for (const match of content.matchAll(
|
|
3652
|
+
for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
|
|
3586
3653
|
const value = match[0] ?? "";
|
|
3587
3654
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3588
3655
|
}
|
|
@@ -4049,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4049
4116
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4050
4117
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4051
4118
|
};
|
|
4052
|
-
const collectAssignedIdentifiers = (block) => {
|
|
4053
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
4054
|
-
walkAst(block, (child) => {
|
|
4055
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4056
|
-
if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
|
|
4057
|
-
});
|
|
4058
|
-
return assigned;
|
|
4059
|
-
};
|
|
4060
|
-
const collectAwaitedArgIdentifiers = (block) => {
|
|
4061
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
4062
|
-
walkAst(block, (child) => {
|
|
4063
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4064
|
-
if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
|
|
4065
|
-
collectReferenceIdentifierNames(child.argument, referenced);
|
|
4066
|
-
});
|
|
4067
|
-
return referenced;
|
|
4068
|
-
};
|
|
4069
4119
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4070
4120
|
"push",
|
|
4071
4121
|
"unshift",
|
|
4072
4122
|
"splice"
|
|
4073
4123
|
]);
|
|
4074
|
-
const
|
|
4075
|
-
const
|
|
4124
|
+
const addDerivedBindings = (block, names) => {
|
|
4125
|
+
const declaratorBindings = [];
|
|
4076
4126
|
walkAst(block, (child) => {
|
|
4077
4127
|
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4078
|
-
if (!isNodeOfType(child, "
|
|
4079
|
-
|
|
4080
|
-
|
|
4128
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
4129
|
+
if (!isNodeOfType(child.id, "Identifier")) return;
|
|
4130
|
+
const referencedNames = /* @__PURE__ */ new Set();
|
|
4131
|
+
collectReferenceIdentifierNames(child.init, referencedNames);
|
|
4132
|
+
declaratorBindings.push({
|
|
4133
|
+
declaredName: child.id.name,
|
|
4134
|
+
referencedNames
|
|
4135
|
+
});
|
|
4081
4136
|
});
|
|
4082
|
-
return mutated;
|
|
4083
|
-
};
|
|
4084
|
-
const addDerivedBindings = (block, names) => {
|
|
4085
4137
|
let didGrow = true;
|
|
4086
4138
|
while (didGrow) {
|
|
4087
4139
|
didGrow = false;
|
|
4088
|
-
|
|
4089
|
-
if (
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
const initReferences = /* @__PURE__ */ new Set();
|
|
4093
|
-
collectReferenceIdentifierNames(child.init, initReferences);
|
|
4094
|
-
for (const referenced of initReferences) if (names.has(referenced)) {
|
|
4095
|
-
names.add(child.id.name);
|
|
4140
|
+
for (const { declaredName, referencedNames } of declaratorBindings) {
|
|
4141
|
+
if (names.has(declaredName)) continue;
|
|
4142
|
+
for (const referenced of referencedNames) if (names.has(referenced)) {
|
|
4143
|
+
names.add(declaredName);
|
|
4096
4144
|
didGrow = true;
|
|
4097
4145
|
break;
|
|
4098
4146
|
}
|
|
4099
|
-
}
|
|
4147
|
+
}
|
|
4100
4148
|
}
|
|
4101
4149
|
};
|
|
4102
4150
|
const hasLoopCarriedDependency = (block) => {
|
|
4103
|
-
const carried =
|
|
4104
|
-
|
|
4151
|
+
const carried = /* @__PURE__ */ new Set();
|
|
4152
|
+
const awaitedReferences = /* @__PURE__ */ new Set();
|
|
4153
|
+
walkAst(block, (child) => {
|
|
4154
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4155
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.left) {
|
|
4156
|
+
collectPatternIdentifiers(child.left, carried);
|
|
4157
|
+
return;
|
|
4158
|
+
}
|
|
4159
|
+
if (isNodeOfType(child, "AwaitExpression") && child.argument) {
|
|
4160
|
+
collectReferenceIdentifierNames(child.argument, awaitedReferences);
|
|
4161
|
+
return;
|
|
4162
|
+
}
|
|
4163
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
4164
|
+
const callee = child.callee;
|
|
4165
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) carried.add(callee.object.name);
|
|
4166
|
+
});
|
|
4105
4167
|
if (carried.size === 0) return false;
|
|
4106
4168
|
addDerivedBindings(block, carried);
|
|
4107
|
-
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4108
4169
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4109
4170
|
return false;
|
|
4110
4171
|
};
|
|
@@ -4839,9 +4900,8 @@ const isCreateElementCall = (node) => {
|
|
|
4839
4900
|
const callee = node.callee;
|
|
4840
4901
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
|
|
4841
4902
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
4842
|
-
if (
|
|
4843
|
-
|
|
4844
|
-
return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
|
|
4903
|
+
if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
|
|
4904
|
+
return !memberChainContainsDocument(callee.object);
|
|
4845
4905
|
}
|
|
4846
4906
|
return false;
|
|
4847
4907
|
};
|
|
@@ -5658,12 +5718,20 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5658
5718
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5659
5719
|
};
|
|
5660
5720
|
const isInertParseTarget = (target, fileContent) => {
|
|
5721
|
+
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5722
|
+
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5723
|
+
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5661
5724
|
const escapedTarget = escapeRegExp(target);
|
|
5662
5725
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5663
5726
|
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
|
-
|
|
5727
|
+
if (fileHasCreateElement) {
|
|
5728
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5729
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5730
|
+
}
|
|
5731
|
+
if (fileHasIsolatedDocument) {
|
|
5732
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5733
|
+
}
|
|
5734
|
+
if (!fileHasCreateElement) return false;
|
|
5667
5735
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5668
5736
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5669
5737
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5756,10 +5824,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
5756
5824
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5757
5825
|
if (valueIdentifier !== void 0) {
|
|
5758
5826
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5827
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5828
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5829
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
5763
5830
|
}
|
|
5764
5831
|
}
|
|
5765
5832
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5908,6 +5975,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5908
5975
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5909
5976
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5910
5977
|
if (!classNameLiteral) return;
|
|
5978
|
+
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5911
5979
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5912
5980
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5913
5981
|
if (matchedPairs.length === 0) return;
|
|
@@ -5932,6 +6000,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
5932
6000
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5933
6001
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5934
6002
|
if (!classNameLiteral) return;
|
|
6003
|
+
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
5935
6004
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
5936
6005
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
5937
6006
|
if (matchedPairs.length === 0) return;
|
|
@@ -5956,6 +6025,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
5956
6025
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5957
6026
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5958
6027
|
if (!classNameLiteral) return;
|
|
6028
|
+
if (!classNameLiteral.includes("space-")) return;
|
|
5959
6029
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
5960
6030
|
let hasFlexOrGridLayout = false;
|
|
5961
6031
|
for (const token of tokens) {
|
|
@@ -6147,7 +6217,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6147
6217
|
const actualMinor = Number(match[2]);
|
|
6148
6218
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6149
6219
|
};
|
|
6220
|
+
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6150
6221
|
const containsJsx$1 = (root) => {
|
|
6222
|
+
const cached = containsJsxCache.get(root);
|
|
6223
|
+
if (cached !== void 0) return cached;
|
|
6151
6224
|
let found = false;
|
|
6152
6225
|
const visit = (node) => {
|
|
6153
6226
|
if (found) return;
|
|
@@ -6170,6 +6243,7 @@ const containsJsx$1 = (root) => {
|
|
|
6170
6243
|
}
|
|
6171
6244
|
};
|
|
6172
6245
|
visit(root);
|
|
6246
|
+
containsJsxCache.set(root, found);
|
|
6173
6247
|
return found;
|
|
6174
6248
|
};
|
|
6175
6249
|
const getStaticMemberName = (node) => {
|
|
@@ -6261,27 +6335,6 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6261
6335
|
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
6336
|
return false;
|
|
6263
6337
|
};
|
|
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
6338
|
const memberExpressionPath = (node) => {
|
|
6286
6339
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6287
6340
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6289,15 +6342,16 @@ const memberExpressionPath = (node) => {
|
|
|
6289
6342
|
const propertyName = getStaticMemberName(node);
|
|
6290
6343
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6291
6344
|
};
|
|
6292
|
-
const
|
|
6293
|
-
|
|
6345
|
+
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6346
|
+
const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
6347
|
+
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6348
|
+
if (cached) return cached;
|
|
6349
|
+
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6350
|
+
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6294
6351
|
const visit = (node) => {
|
|
6295
|
-
if (found) return;
|
|
6296
6352
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6297
|
-
if (
|
|
6298
|
-
|
|
6299
|
-
return;
|
|
6300
|
-
}
|
|
6353
|
+
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
6354
|
+
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
6301
6355
|
}
|
|
6302
6356
|
const record = node;
|
|
6303
6357
|
for (const key of Object.keys(record)) {
|
|
@@ -6306,12 +6360,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
|
6306
6360
|
if (Array.isArray(child)) {
|
|
6307
6361
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6308
6362
|
} else if (isAstNode(child)) visit(child);
|
|
6309
|
-
if (found) return;
|
|
6310
6363
|
}
|
|
6311
6364
|
};
|
|
6312
6365
|
visit(programRoot);
|
|
6313
|
-
|
|
6366
|
+
const index = {
|
|
6367
|
+
identifierTargets,
|
|
6368
|
+
memberPathSegments
|
|
6369
|
+
};
|
|
6370
|
+
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6371
|
+
return index;
|
|
6314
6372
|
};
|
|
6373
|
+
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6374
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6315
6375
|
const displayName = defineRule({
|
|
6316
6376
|
id: "display-name",
|
|
6317
6377
|
title: "Component missing display name",
|
|
@@ -7183,27 +7243,8 @@ const isDescendantScope = (inner, outer) => {
|
|
|
7183
7243
|
return false;
|
|
7184
7244
|
};
|
|
7185
7245
|
//#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
7246
|
//#region src/plugin/semantic/closure-captures.ts
|
|
7206
|
-
const
|
|
7247
|
+
const computeClosureCaptures = (functionNode, scopes) => {
|
|
7207
7248
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
7208
7249
|
const out = [];
|
|
7209
7250
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -7238,7 +7279,20 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
7238
7279
|
}
|
|
7239
7280
|
};
|
|
7240
7281
|
visit(functionNode);
|
|
7241
|
-
return out
|
|
7282
|
+
return out;
|
|
7283
|
+
};
|
|
7284
|
+
const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
7285
|
+
const closureCaptures = (functionNode, scopes) => {
|
|
7286
|
+
let capturesByFunction = capturesByAnalysis.get(scopes);
|
|
7287
|
+
if (!capturesByFunction) {
|
|
7288
|
+
capturesByFunction = /* @__PURE__ */ new WeakMap();
|
|
7289
|
+
capturesByAnalysis.set(scopes, capturesByFunction);
|
|
7290
|
+
}
|
|
7291
|
+
const memoizedCaptures = capturesByFunction.get(functionNode);
|
|
7292
|
+
if (memoizedCaptures) return memoizedCaptures;
|
|
7293
|
+
const computedCaptures = computeClosureCaptures(functionNode, scopes);
|
|
7294
|
+
capturesByFunction.set(functionNode, computedCaptures);
|
|
7295
|
+
return computedCaptures;
|
|
7242
7296
|
};
|
|
7243
7297
|
//#endregion
|
|
7244
7298
|
//#region src/plugin/utils/is-react-hook-name.ts
|
|
@@ -7367,6 +7421,25 @@ const resolveExhaustiveDepsSettings = (settings) => {
|
|
|
7367
7421
|
};
|
|
7368
7422
|
};
|
|
7369
7423
|
//#endregion
|
|
7424
|
+
//#region src/plugin/utils/is-ast-descendant.ts
|
|
7425
|
+
/**
|
|
7426
|
+
* True when `inner` is `outer` itself or any descendant in the AST
|
|
7427
|
+
* `parent` chain. Walks `inner.parent` upward and stops at either a
|
|
7428
|
+
* match (`true`) or the chain's root (`false`).
|
|
7429
|
+
*
|
|
7430
|
+
* Was duplicated byte-identical in:
|
|
7431
|
+
* - exhaustive-deps-symbol-stability.ts
|
|
7432
|
+
* - semantic/closure-captures.ts
|
|
7433
|
+
*/
|
|
7434
|
+
const isAstDescendant = (inner, outer) => {
|
|
7435
|
+
let current = inner;
|
|
7436
|
+
while (current) {
|
|
7437
|
+
if (current === outer) return true;
|
|
7438
|
+
current = current.parent ?? null;
|
|
7439
|
+
}
|
|
7440
|
+
return false;
|
|
7441
|
+
};
|
|
7442
|
+
//#endregion
|
|
7370
7443
|
//#region src/plugin/rules/react-builtins/exhaustive-deps-symbol-stability.ts
|
|
7371
7444
|
/**
|
|
7372
7445
|
* Symbol-stability helpers consumed by the `exhaustive-deps` rule.
|
|
@@ -8266,9 +8339,14 @@ const firebaseQueryFilterAsAuth = defineRule({
|
|
|
8266
8339
|
* `*Handler`), so the cheap escape-then-replace shape is enough
|
|
8267
8340
|
* and avoids pulling picomatch into the per-file rule path.
|
|
8268
8341
|
*/
|
|
8342
|
+
const compiledGlobs = /* @__PURE__ */ new Map();
|
|
8269
8343
|
const compileGlob = (pattern) => {
|
|
8344
|
+
const cached = compiledGlobs.get(pattern);
|
|
8345
|
+
if (cached) return cached;
|
|
8270
8346
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
8271
|
-
|
|
8347
|
+
const compiled = new RegExp(`^${escaped}$`);
|
|
8348
|
+
compiledGlobs.set(pattern, compiled);
|
|
8349
|
+
return compiled;
|
|
8272
8350
|
};
|
|
8273
8351
|
//#endregion
|
|
8274
8352
|
//#region src/plugin/rules/react-builtins/forbid-component-props.ts
|
|
@@ -9210,10 +9288,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9210
9288
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9211
9289
|
const settings = resolveSettings$37(context.settings);
|
|
9212
9290
|
return { JSXOpeningElement(node) {
|
|
9213
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9214
9291
|
const tag = getElementType(node, context.settings);
|
|
9215
9292
|
if (!settings.components.includes(tag)) return;
|
|
9216
9293
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9294
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9217
9295
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9218
9296
|
if (!altAttribute) return;
|
|
9219
9297
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9268,6 +9346,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9268
9346
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9269
9347
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9270
9348
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9349
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9271
9350
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9272
9351
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9273
9352
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9297,6 +9376,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9297
9376
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9298
9377
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9299
9378
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9379
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9300
9380
|
const content = getScannableContent(file);
|
|
9301
9381
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9302
9382
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9529,15 +9609,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9529
9609
|
const settings = resolveSettings$36(context.settings);
|
|
9530
9610
|
const tabbableSet = new Set(settings.tabbable);
|
|
9531
9611
|
return { JSXOpeningElement(node) {
|
|
9612
|
+
if (node.attributes.length === 0) return;
|
|
9532
9613
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9533
9614
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9534
9615
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9616
|
+
if (!role) return;
|
|
9617
|
+
if (!ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)))) return;
|
|
9535
9618
|
const elementType = getElementType(node, context.settings);
|
|
9536
9619
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9537
|
-
|
|
9620
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9538
9621
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9539
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9540
|
-
if (!role) return;
|
|
9541
9622
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9542
9623
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9543
9624
|
context.report({
|
|
@@ -10176,8 +10257,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10176
10257
|
walkAst(loopBody, (child) => {
|
|
10177
10258
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10178
10259
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10179
|
-
});
|
|
10180
|
-
walkAst(loopBody, (child) => {
|
|
10181
10260
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10182
10261
|
if (child.computed) return;
|
|
10183
10262
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10637,7 +10716,6 @@ const jsHoistIntl = defineRule({
|
|
|
10637
10716
|
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
10717
|
create: (context) => ({ NewExpression(node) {
|
|
10639
10718
|
if (!isIntlNewExpression(node)) return;
|
|
10640
|
-
if (isInsideCacheMemo(node)) return;
|
|
10641
10719
|
let cursor = node.parent ?? null;
|
|
10642
10720
|
let inFunctionBody = false;
|
|
10643
10721
|
while (cursor) {
|
|
@@ -10654,6 +10732,7 @@ const jsHoistIntl = defineRule({
|
|
|
10654
10732
|
cursor = cursor.parent ?? null;
|
|
10655
10733
|
}
|
|
10656
10734
|
if (!inFunctionBody) return;
|
|
10735
|
+
if (isInsideCacheMemo(node)) return;
|
|
10657
10736
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10658
10737
|
context.report({
|
|
10659
10738
|
node,
|
|
@@ -10728,7 +10807,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10728
10807
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10729
10808
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10730
10809
|
};
|
|
10731
|
-
const
|
|
10810
|
+
const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
10811
|
+
const getLoopBoundNames = (loop) => {
|
|
10812
|
+
const cached = loopBoundNamesCache.get(loop);
|
|
10813
|
+
if (cached) return cached;
|
|
10814
|
+
const names = /* @__PURE__ */ new Set();
|
|
10732
10815
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10733
10816
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10734
10817
|
});
|
|
@@ -10745,6 +10828,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10745
10828
|
if (targetRoot) names.add(targetRoot);
|
|
10746
10829
|
}
|
|
10747
10830
|
});
|
|
10831
|
+
loopBoundNamesCache.set(loop, names);
|
|
10832
|
+
return names;
|
|
10748
10833
|
};
|
|
10749
10834
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10750
10835
|
let cursor = receiver;
|
|
@@ -10770,7 +10855,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10770
10855
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10771
10856
|
let ancestor = node.parent;
|
|
10772
10857
|
while (ancestor) {
|
|
10773
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10858
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10774
10859
|
ancestor = ancestor.parent;
|
|
10775
10860
|
}
|
|
10776
10861
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -15441,16 +15526,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15441
15526
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15442
15527
|
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
15443
15528
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15444
|
-
const
|
|
15529
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15445
15530
|
const strippedSource = stripJsComments(sourceText);
|
|
15446
|
-
|
|
15447
|
-
|
|
15448
|
-
for (const match of strippedSource.matchAll(
|
|
15449
|
-
|
|
15531
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
15532
|
+
if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
|
|
15533
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
|
|
15534
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
|
|
15535
|
+
const specifiersText = match[1] ?? "";
|
|
15536
|
+
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15537
|
+
}
|
|
15538
|
+
return exportedNames;
|
|
15450
15539
|
};
|
|
15540
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15451
15541
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15452
15542
|
try {
|
|
15453
|
-
|
|
15543
|
+
const fileStat = fs.statSync(filePath);
|
|
15544
|
+
const cached = exportNamesCache.get(filePath);
|
|
15545
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15546
|
+
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15547
|
+
exportNamesCache.set(filePath, {
|
|
15548
|
+
mtimeMs: fileStat.mtimeMs,
|
|
15549
|
+
size: fileStat.size,
|
|
15550
|
+
exportedNames
|
|
15551
|
+
});
|
|
15552
|
+
return exportedNames.has(exportedName);
|
|
15454
15553
|
} catch {
|
|
15455
15554
|
return false;
|
|
15456
15555
|
}
|
|
@@ -15595,8 +15694,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15595
15694
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15596
15695
|
let didFindFetchCall = false;
|
|
15597
15696
|
walkAst(node, (child) => {
|
|
15697
|
+
if (didFindFetchCall) return false;
|
|
15598
15698
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15599
|
-
if (isFetchCall$1(child))
|
|
15699
|
+
if (isFetchCall$1(child)) {
|
|
15700
|
+
didFindFetchCall = true;
|
|
15701
|
+
return false;
|
|
15702
|
+
}
|
|
15600
15703
|
});
|
|
15601
15704
|
return didFindFetchCall;
|
|
15602
15705
|
};
|
|
@@ -15610,6 +15713,8 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15610
15713
|
severity: "warn",
|
|
15611
15714
|
recommendation: "Remove 'use client' and fetch directly in the Server Component. No API round-trip, and secrets stay on the server.",
|
|
15612
15715
|
create: (context) => {
|
|
15716
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
15717
|
+
if (!(PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages"))) return {};
|
|
15613
15718
|
let fileHasUseClient = false;
|
|
15614
15719
|
return {
|
|
15615
15720
|
Program(programNode) {
|
|
@@ -15619,8 +15724,7 @@ const nextjsNoClientFetchForServerData = defineRule({
|
|
|
15619
15724
|
if (!fileHasUseClient || !isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
15620
15725
|
const callback = getEffectCallback(node);
|
|
15621
15726
|
if (!callback || !containsFetchCall(callback, { stopAtFunctionBoundary: true })) return;
|
|
15622
|
-
|
|
15623
|
-
if (PAGE_OR_LAYOUT_FILE_PATTERN.test(filename) || isInProjectDirectory(context, "pages")) context.report({
|
|
15727
|
+
context.report({
|
|
15624
15728
|
node,
|
|
15625
15729
|
message: "useEffect + fetch in a page/layout makes your users wait through an extra round trip & loading spinner."
|
|
15626
15730
|
});
|
|
@@ -16285,16 +16389,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16285
16389
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16286
16390
|
create: (context) => {
|
|
16287
16391
|
let resolveBinding = () => null;
|
|
16392
|
+
let isRouteHandlerFile = false;
|
|
16393
|
+
let mutatingSegment = null;
|
|
16288
16394
|
return {
|
|
16289
16395
|
Program(node) {
|
|
16290
16396
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16397
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16398
|
+
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16399
|
+
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16291
16400
|
},
|
|
16292
16401
|
ExportNamedDeclaration(node) {
|
|
16293
|
-
|
|
16294
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16295
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16402
|
+
if (!isRouteHandlerFile) return;
|
|
16296
16403
|
if (!isExportedGetHandler(node)) return;
|
|
16297
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16298
16404
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16299
16405
|
for (const handlerBody of handlerBodies) {
|
|
16300
16406
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17326,8 +17432,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17326
17432
|
programToAnalysis.set(programNode, analysis);
|
|
17327
17433
|
return analysis;
|
|
17328
17434
|
};
|
|
17435
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17329
17436
|
const getScopeForNode = (node, manager) => {
|
|
17330
17437
|
if (!node.range) return null;
|
|
17438
|
+
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17439
|
+
if (!scopeByNode) {
|
|
17440
|
+
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17441
|
+
scopeByNodeCache.set(manager, scopeByNode);
|
|
17442
|
+
}
|
|
17443
|
+
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17331
17444
|
let bestScope = null;
|
|
17332
17445
|
let bestSize = Infinity;
|
|
17333
17446
|
for (const scope of manager.scopes) {
|
|
@@ -17340,6 +17453,7 @@ const getScopeForNode = (node, manager) => {
|
|
|
17340
17453
|
bestScope = scope;
|
|
17341
17454
|
}
|
|
17342
17455
|
}
|
|
17456
|
+
scopeByNode.set(node, bestScope);
|
|
17343
17457
|
return bestScope;
|
|
17344
17458
|
};
|
|
17345
17459
|
//#endregion
|
|
@@ -17374,11 +17488,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17374
17488
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17375
17489
|
}
|
|
17376
17490
|
};
|
|
17491
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17377
17492
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17493
|
+
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17494
|
+
if (!upstreamByRef) {
|
|
17495
|
+
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17496
|
+
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17497
|
+
}
|
|
17498
|
+
const cached = upstreamByRef.get(ref);
|
|
17499
|
+
if (cached) return cached;
|
|
17378
17500
|
const refs = [];
|
|
17379
17501
|
ascend(analysis, ref, (upRef) => {
|
|
17380
17502
|
refs.push(upRef);
|
|
17381
17503
|
});
|
|
17504
|
+
upstreamByRef.set(ref, refs);
|
|
17382
17505
|
return refs;
|
|
17383
17506
|
};
|
|
17384
17507
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17388,11 +17511,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17388
17511
|
});
|
|
17389
17512
|
return nodes;
|
|
17390
17513
|
};
|
|
17514
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17391
17515
|
const getRef = (analysis, identifier) => {
|
|
17516
|
+
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17517
|
+
if (!refByIdentifier) {
|
|
17518
|
+
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17519
|
+
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17520
|
+
}
|
|
17521
|
+
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17522
|
+
let resolvedReference = null;
|
|
17392
17523
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17393
|
-
if (
|
|
17394
|
-
|
|
17395
|
-
|
|
17524
|
+
if (scope) {
|
|
17525
|
+
for (const reference of scope.references) if (reference.identifier === identifier) {
|
|
17526
|
+
resolvedReference = reference;
|
|
17527
|
+
break;
|
|
17528
|
+
}
|
|
17529
|
+
}
|
|
17530
|
+
refByIdentifier.set(identifier, resolvedReference);
|
|
17531
|
+
return resolvedReference;
|
|
17396
17532
|
};
|
|
17397
17533
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17398
17534
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17467,22 +17603,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17467
17603
|
};
|
|
17468
17604
|
//#endregion
|
|
17469
17605
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17470
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17471
|
-
if (!node.range) return null;
|
|
17472
|
-
let best = null;
|
|
17473
|
-
let bestSize = Infinity;
|
|
17474
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17475
|
-
const block = scope.block;
|
|
17476
|
-
if (!block?.range) continue;
|
|
17477
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17478
|
-
const size = block.range[1] - block.range[0];
|
|
17479
|
-
if (size <= bestSize) {
|
|
17480
|
-
bestSize = size;
|
|
17481
|
-
best = scope;
|
|
17482
|
-
}
|
|
17483
|
-
}
|
|
17484
|
-
return best;
|
|
17485
|
-
};
|
|
17486
17606
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17487
17607
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17488
17608
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17513,7 +17633,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17513
17633
|
const isWrappedSeparately = () => {
|
|
17514
17634
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17515
17635
|
const bindingName = node.id.name;
|
|
17516
|
-
const containingScope =
|
|
17636
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17517
17637
|
if (!containingScope) return false;
|
|
17518
17638
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17519
17639
|
if (!variable) return false;
|
|
@@ -18863,7 +18983,17 @@ const containsRenderOutput = (node, rootNode, scopes) => {
|
|
|
18863
18983
|
}
|
|
18864
18984
|
return false;
|
|
18865
18985
|
};
|
|
18866
|
-
const
|
|
18986
|
+
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
18987
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
18988
|
+
const cachedEntry = renderOutputCache.get(functionNode);
|
|
18989
|
+
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
18990
|
+
const hasRenderOutput = containsRenderOutput(functionNode, functionNode, scopes);
|
|
18991
|
+
renderOutputCache.set(functionNode, {
|
|
18992
|
+
scopes,
|
|
18993
|
+
hasRenderOutput
|
|
18994
|
+
});
|
|
18995
|
+
return hasRenderOutput;
|
|
18996
|
+
};
|
|
18867
18997
|
//#endregion
|
|
18868
18998
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
18869
18999
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
@@ -19368,8 +19498,8 @@ const CONTEXT_MODULES = [
|
|
|
19368
19498
|
];
|
|
19369
19499
|
const isCreateContextCallee = (callee) => {
|
|
19370
19500
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19371
|
-
|
|
19372
|
-
return
|
|
19501
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19502
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19373
19503
|
}
|
|
19374
19504
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19375
19505
|
const namespaceIdentifier = callee.object;
|
|
@@ -19379,8 +19509,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19379
19509
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19380
19510
|
const namespaceName = namespaceIdentifier.name;
|
|
19381
19511
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19382
|
-
|
|
19383
|
-
return
|
|
19512
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19513
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19384
19514
|
}
|
|
19385
19515
|
return false;
|
|
19386
19516
|
};
|
|
@@ -19782,38 +19912,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19782
19912
|
};
|
|
19783
19913
|
const parseColorToRgb = (value) => {
|
|
19784
19914
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19785
|
-
|
|
19786
|
-
|
|
19787
|
-
|
|
19788
|
-
|
|
19789
|
-
|
|
19790
|
-
|
|
19791
|
-
|
|
19792
|
-
|
|
19793
|
-
|
|
19794
|
-
|
|
19795
|
-
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
19812
|
-
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19915
|
+
if (trimmed.startsWith("#")) {
|
|
19916
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
19917
|
+
if (hex8Match) return {
|
|
19918
|
+
red: parseInt(hex8Match[1], 16),
|
|
19919
|
+
green: parseInt(hex8Match[2], 16),
|
|
19920
|
+
blue: parseInt(hex8Match[3], 16)
|
|
19921
|
+
};
|
|
19922
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
19923
|
+
if (hex6Match) return {
|
|
19924
|
+
red: parseInt(hex6Match[1], 16),
|
|
19925
|
+
green: parseInt(hex6Match[2], 16),
|
|
19926
|
+
blue: parseInt(hex6Match[3], 16)
|
|
19927
|
+
};
|
|
19928
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
19929
|
+
if (hex4Match) return {
|
|
19930
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
19931
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
19932
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
19933
|
+
};
|
|
19934
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
19935
|
+
if (hex3Match) return {
|
|
19936
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
19937
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
19938
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
19939
|
+
};
|
|
19940
|
+
}
|
|
19941
|
+
if (trimmed.includes("rgb")) {
|
|
19942
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
19943
|
+
if (rgbMatch) return {
|
|
19944
|
+
red: parseInt(rgbMatch[1], 10),
|
|
19945
|
+
green: parseInt(rgbMatch[2], 10),
|
|
19946
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
19947
|
+
};
|
|
19948
|
+
}
|
|
19949
|
+
if (trimmed.includes("hsl")) {
|
|
19950
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19951
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19952
|
+
}
|
|
19817
19953
|
return null;
|
|
19818
19954
|
};
|
|
19819
19955
|
//#endregion
|
|
@@ -19841,9 +19977,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19841
19977
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19842
19978
|
return null;
|
|
19843
19979
|
};
|
|
19980
|
+
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
19981
|
+
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
19982
|
+
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
19983
|
+
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19844
19984
|
const parseShadowLayerBlur = (layer) => {
|
|
19845
|
-
const
|
|
19846
|
-
|
|
19985
|
+
const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
|
|
19986
|
+
let tokenIndex = 0;
|
|
19987
|
+
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
19988
|
+
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
19989
|
+
tokenIndex += 1;
|
|
19990
|
+
}
|
|
19991
|
+
return 0;
|
|
19847
19992
|
};
|
|
19848
19993
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19849
19994
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -19963,7 +20108,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
19963
20108
|
};
|
|
19964
20109
|
//#endregion
|
|
19965
20110
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20111
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
19966
20112
|
const collectComponentPropNames = (componentFunction) => {
|
|
20113
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20114
|
+
if (cached) return cached;
|
|
19967
20115
|
const propNames = /* @__PURE__ */ new Set();
|
|
19968
20116
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
19969
20117
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -19977,27 +20125,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
19977
20125
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
19978
20126
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
19979
20127
|
});
|
|
20128
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
19980
20129
|
return propNames;
|
|
19981
20130
|
};
|
|
19982
|
-
const
|
|
20131
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20132
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20133
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20134
|
+
if (cached) return cached;
|
|
19983
20135
|
const boundNames = /* @__PURE__ */ new Set();
|
|
19984
20136
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
19985
|
-
if (boundNames.has(bindingName)) return true;
|
|
19986
|
-
let declaresName = false;
|
|
19987
20137
|
walkAst(functionNode, (child) => {
|
|
19988
|
-
if (declaresName) return false;
|
|
19989
20138
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
19990
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
19991
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
19992
|
-
collectPatternNames(child.id, declaratorNames);
|
|
19993
|
-
if (declaratorNames.has(bindingName)) {
|
|
19994
|
-
declaresName = true;
|
|
19995
|
-
return false;
|
|
19996
|
-
}
|
|
19997
|
-
}
|
|
20139
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
19998
20140
|
});
|
|
19999
|
-
|
|
20141
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20142
|
+
return boundNames;
|
|
20000
20143
|
};
|
|
20144
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20001
20145
|
const isPropertyNamePosition = (identifier) => {
|
|
20002
20146
|
const parent = identifier.parent;
|
|
20003
20147
|
if (!parent) return false;
|
|
@@ -20420,36 +20564,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20420
20564
|
}
|
|
20421
20565
|
return hasNestedFunction;
|
|
20422
20566
|
};
|
|
20423
|
-
const
|
|
20424
|
-
|
|
20425
|
-
|
|
20426
|
-
|
|
20427
|
-
|
|
20428
|
-
|
|
20429
|
-
|
|
20430
|
-
if (isReseeded) return false;
|
|
20431
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20432
|
-
isReseeded = true;
|
|
20433
|
-
return false;
|
|
20434
|
-
}
|
|
20435
|
-
});
|
|
20436
|
-
return isReseeded;
|
|
20567
|
+
const isInRenderScope = (node, componentFunction) => {
|
|
20568
|
+
let cursor = node.parent ?? null;
|
|
20569
|
+
while (cursor && cursor !== componentFunction) {
|
|
20570
|
+
if (isFunctionLike$1(cursor)) return false;
|
|
20571
|
+
cursor = cursor.parent ?? null;
|
|
20572
|
+
}
|
|
20573
|
+
return true;
|
|
20437
20574
|
};
|
|
20438
|
-
const
|
|
20575
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20439
20576
|
const setterName = getStateSetterName(useStateCall);
|
|
20440
20577
|
if (!setterName) return false;
|
|
20441
20578
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20442
20579
|
if (!componentFunction) return false;
|
|
20443
|
-
let
|
|
20580
|
+
let isExempt = false;
|
|
20444
20581
|
walkAst(componentFunction, (child) => {
|
|
20445
|
-
if (
|
|
20446
|
-
if (child
|
|
20447
|
-
|
|
20448
|
-
isAdjusted = true;
|
|
20582
|
+
if (isExempt) return false;
|
|
20583
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
|
|
20584
|
+
isExempt = true;
|
|
20449
20585
|
return false;
|
|
20450
20586
|
}
|
|
20451
20587
|
});
|
|
20452
|
-
return
|
|
20588
|
+
return isExempt;
|
|
20453
20589
|
};
|
|
20454
20590
|
const noDerivedUseState = defineRule({
|
|
20455
20591
|
id: "no-derived-useState",
|
|
@@ -20466,8 +20602,7 @@ const noDerivedUseState = defineRule({
|
|
|
20466
20602
|
const initializer = node.arguments[0];
|
|
20467
20603
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20468
20604
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20469
|
-
if (
|
|
20470
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20605
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20471
20606
|
context.report({
|
|
20472
20607
|
node,
|
|
20473
20608
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20478,8 +20613,7 @@ const noDerivedUseState = defineRule({
|
|
|
20478
20613
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20479
20614
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20480
20615
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20481
|
-
if (
|
|
20482
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20616
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20483
20617
|
context.report({
|
|
20484
20618
|
node,
|
|
20485
20619
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22178,20 +22312,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22178
22312
|
return null;
|
|
22179
22313
|
};
|
|
22180
22314
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22315
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22316
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22317
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22318
|
+
walkAst(componentBody, (child) => {
|
|
22319
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22320
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22321
|
+
const setterName = child.callee.name;
|
|
22322
|
+
if (!setterNames.has(setterName)) return;
|
|
22323
|
+
settersWithAnyCall.add(setterName);
|
|
22324
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22325
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22326
|
+
});
|
|
22181
22327
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22182
|
-
for (const binding of useStateBindings)
|
|
22183
|
-
let didFindAnySetterCall = false;
|
|
22184
|
-
let areAllSetterCallsInHandlers = true;
|
|
22185
|
-
walkAst(componentBody, (child) => {
|
|
22186
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22187
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22188
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22189
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22190
|
-
didFindAnySetterCall = true;
|
|
22191
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22192
|
-
});
|
|
22193
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22194
|
-
}
|
|
22328
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22195
22329
|
return handlerOnlyWriteStateNames;
|
|
22196
22330
|
};
|
|
22197
22331
|
const noEventTriggerState = defineRule({
|
|
@@ -22531,6 +22665,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22531
22665
|
const TEXT_COLOR_PATTERN = /^text-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22532
22666
|
const BG_COLOR_PATTERN = /^bg-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22533
22667
|
const splitVariantScope = (token) => {
|
|
22668
|
+
if (!token.includes(":")) return {
|
|
22669
|
+
scope: "",
|
|
22670
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22671
|
+
};
|
|
22534
22672
|
const segments = token.split(":");
|
|
22535
22673
|
const rawUtility = segments[segments.length - 1];
|
|
22536
22674
|
return {
|
|
@@ -22554,6 +22692,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22554
22692
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22555
22693
|
for (const token of classStr.split(/\s+/)) {
|
|
22556
22694
|
if (!token) continue;
|
|
22695
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22557
22696
|
const { scope, utility } = splitVariantScope(token);
|
|
22558
22697
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22559
22698
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23283,6 +23422,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23283
23422
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23284
23423
|
});
|
|
23285
23424
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23425
|
+
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23426
|
+
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23286
23427
|
const noLongTransitionDuration = defineRule({
|
|
23287
23428
|
id: "no-long-transition-duration",
|
|
23288
23429
|
title: "Transition duration too long",
|
|
@@ -23306,11 +23447,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23306
23447
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23307
23448
|
let longestDurationPropertyMs = 0;
|
|
23308
23449
|
for (const segment of value.split(",")) {
|
|
23309
|
-
const
|
|
23310
|
-
|
|
23311
|
-
const
|
|
23312
|
-
|
|
23313
|
-
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23450
|
+
const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
|
|
23451
|
+
if (!durationMatch) continue;
|
|
23452
|
+
const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
|
|
23453
|
+
longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
|
|
23314
23454
|
}
|
|
23315
23455
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23316
23456
|
}
|
|
@@ -23318,7 +23458,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23318
23458
|
let longestDurationMs = 0;
|
|
23319
23459
|
for (const segment of value.split(",")) {
|
|
23320
23460
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23321
|
-
const firstTimeMatch = segment.match(
|
|
23461
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23322
23462
|
if (!firstTimeMatch) continue;
|
|
23323
23463
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23324
23464
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -26076,14 +26216,14 @@ const noRenderInRender = defineRule({
|
|
|
26076
26216
|
const expression = node.expression;
|
|
26077
26217
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26078
26218
|
let calleeName = null;
|
|
26219
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26220
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26221
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26079
26222
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26080
26223
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26081
|
-
|
|
26082
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26224
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26083
26225
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26084
|
-
calleeName = expression.callee.property.name;
|
|
26085
26226
|
}
|
|
26086
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26087
26227
|
context.report({
|
|
26088
26228
|
node: expression,
|
|
26089
26229
|
message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
|
|
@@ -28798,32 +28938,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28798
28938
|
if (!isHocCallee$1(parent)) return false;
|
|
28799
28939
|
return parent.arguments[0] === node;
|
|
28800
28940
|
};
|
|
28941
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
28942
|
+
"map",
|
|
28943
|
+
"forEach",
|
|
28944
|
+
"filter",
|
|
28945
|
+
"flatMap",
|
|
28946
|
+
"reduce",
|
|
28947
|
+
"reduceRight"
|
|
28948
|
+
]);
|
|
28801
28949
|
const isReturnOfMapCallback = (node) => {
|
|
28802
28950
|
const parent = node.parent;
|
|
28803
28951
|
if (!parent) return false;
|
|
28804
28952
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28805
28953
|
const callee = parent.callee;
|
|
28806
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28807
|
-
"map",
|
|
28808
|
-
"forEach",
|
|
28809
|
-
"filter",
|
|
28810
|
-
"flatMap",
|
|
28811
|
-
"reduce",
|
|
28812
|
-
"reduceRight"
|
|
28813
|
-
].includes(callee.property.name);
|
|
28954
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28814
28955
|
}
|
|
28815
28956
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28816
28957
|
const callbackParent = parent.parent;
|
|
28817
28958
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28818
28959
|
const callee = callbackParent.callee;
|
|
28819
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28820
|
-
"map",
|
|
28821
|
-
"forEach",
|
|
28822
|
-
"filter",
|
|
28823
|
-
"flatMap",
|
|
28824
|
-
"reduce",
|
|
28825
|
-
"reduceRight"
|
|
28826
|
-
].includes(callee.property.name);
|
|
28960
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28827
28961
|
}
|
|
28828
28962
|
}
|
|
28829
28963
|
return false;
|
|
@@ -28859,10 +28993,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28859
28993
|
valueNode = parent;
|
|
28860
28994
|
parent = parent.parent;
|
|
28861
28995
|
continue;
|
|
28862
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
28996
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
|
|
28863
28997
|
case "AssignmentExpression": {
|
|
28864
28998
|
const assignmentTarget = parent.left;
|
|
28865
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
28999
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28866
29000
|
}
|
|
28867
29001
|
case "Property":
|
|
28868
29002
|
if (parent.value !== valueNode) return false;
|
|
@@ -28955,14 +29089,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
28955
29089
|
};
|
|
28956
29090
|
return {
|
|
28957
29091
|
JSXOpeningElement(node) {
|
|
28958
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29092
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
28959
29093
|
recordInstantiation(node.name, node.name.name);
|
|
28960
29094
|
return;
|
|
28961
29095
|
}
|
|
28962
29096
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
28963
29097
|
},
|
|
28964
29098
|
Identifier(node) {
|
|
28965
|
-
if (!
|
|
29099
|
+
if (!isReactComponentName(node.name)) return;
|
|
28966
29100
|
if (!isRenderFlowingReadReference(node)) return;
|
|
28967
29101
|
recordInstantiation(node, node.name);
|
|
28968
29102
|
},
|
|
@@ -30772,13 +30906,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30772
30906
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30773
30907
|
},
|
|
30774
30908
|
JSXAttribute(node) {
|
|
30775
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30776
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30777
30909
|
if (!node.value) return;
|
|
30778
30910
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30779
30911
|
const innerExpression = node.value.expression;
|
|
30780
30912
|
if (!innerExpression) return;
|
|
30781
30913
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
30914
|
+
if (!isInsideFunctionScope(node)) return;
|
|
30915
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30782
30916
|
const parentJsxOpening = node.parent;
|
|
30783
30917
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30784
30918
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -32150,41 +32284,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32150
32284
|
});
|
|
32151
32285
|
return found;
|
|
32152
32286
|
};
|
|
32153
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32154
|
-
if (!componentBody) return false;
|
|
32155
|
-
let found = false;
|
|
32156
|
-
walkAst(componentBody, (child) => {
|
|
32157
|
-
if (found) return;
|
|
32158
|
-
if (!isFunctionLike$1(child)) return;
|
|
32159
|
-
const functionBody = child.body;
|
|
32160
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32161
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32162
|
-
});
|
|
32163
|
-
return found;
|
|
32164
|
-
};
|
|
32165
32287
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32166
32288
|
"then",
|
|
32167
32289
|
"catch",
|
|
32168
32290
|
"finally"
|
|
32169
32291
|
]);
|
|
32170
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32171
|
-
if (!componentBody) return false;
|
|
32172
|
-
let found = false;
|
|
32173
|
-
walkAst(componentBody, (child) => {
|
|
32174
|
-
if (found) return;
|
|
32175
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32176
|
-
const callee = child.callee;
|
|
32177
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32178
|
-
for (const argument of child.arguments ?? []) {
|
|
32179
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32180
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32181
|
-
found = true;
|
|
32182
|
-
return;
|
|
32183
|
-
}
|
|
32184
|
-
}
|
|
32185
|
-
});
|
|
32186
|
-
return found;
|
|
32187
|
-
};
|
|
32188
32292
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32189
32293
|
"useApolloClient",
|
|
32190
32294
|
"useMutation",
|
|
@@ -32197,20 +32301,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32197
32301
|
"fetch",
|
|
32198
32302
|
"axios"
|
|
32199
32303
|
]);
|
|
32200
|
-
const
|
|
32201
|
-
if (!body) return false;
|
|
32304
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32202
32305
|
let found = false;
|
|
32203
|
-
walkAst(
|
|
32204
|
-
if (found) return;
|
|
32306
|
+
walkAst(fnBody, (child) => {
|
|
32307
|
+
if (found) return false;
|
|
32205
32308
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32206
32309
|
const callee = child.callee;
|
|
32207
32310
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32208
32311
|
found = true;
|
|
32209
|
-
return;
|
|
32312
|
+
return false;
|
|
32210
32313
|
}
|
|
32211
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")
|
|
32314
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32315
|
+
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32316
|
+
found = true;
|
|
32317
|
+
return false;
|
|
32318
|
+
}
|
|
32319
|
+
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32320
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32321
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32322
|
+
found = true;
|
|
32323
|
+
return false;
|
|
32324
|
+
}
|
|
32325
|
+
}
|
|
32326
|
+
}
|
|
32327
|
+
return;
|
|
32328
|
+
}
|
|
32329
|
+
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32330
|
+
const functionBody = child.body;
|
|
32331
|
+
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32212
32332
|
found = true;
|
|
32213
|
-
return;
|
|
32333
|
+
return false;
|
|
32214
32334
|
}
|
|
32215
32335
|
}
|
|
32216
32336
|
});
|
|
@@ -32235,7 +32355,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32235
32355
|
const secondBinding = node.id.elements[1];
|
|
32236
32356
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32237
32357
|
const fnBody = enclosingFunctionBody(node);
|
|
32238
|
-
if (fnBody && (
|
|
32358
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32239
32359
|
context.report({
|
|
32240
32360
|
node: node.init,
|
|
32241
32361
|
message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
|
|
@@ -33029,17 +33149,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33029
33149
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33030
33150
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33031
33151
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33152
|
+
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33153
|
+
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33154
|
+
walkAst(componentBody, (child) => {
|
|
33155
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33156
|
+
});
|
|
33032
33157
|
for (const binding of bindings) {
|
|
33033
33158
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33034
33159
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33035
33160
|
const setterSuffix = binding.setterName.slice(3);
|
|
33036
33161
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33037
|
-
|
|
33038
|
-
walkAst(componentBody, (child) => {
|
|
33039
|
-
if (setterCalled) return;
|
|
33040
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33041
|
-
});
|
|
33042
|
-
if (!setterCalled) continue;
|
|
33162
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33043
33163
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33044
33164
|
context.report({
|
|
33045
33165
|
node: binding.declarator,
|
|
@@ -33371,6 +33491,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33371
33491
|
FlashList: ["@shopify/flash-list"],
|
|
33372
33492
|
LegendList: ["@legendapp/list"]
|
|
33373
33493
|
};
|
|
33494
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33374
33495
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33375
33496
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33376
33497
|
"renderItem",
|
|
@@ -33613,33 +33734,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33613
33734
|
requires: ["react-native"],
|
|
33614
33735
|
severity: "warn",
|
|
33615
33736
|
recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
|
|
33616
|
-
create: (context) =>
|
|
33617
|
-
|
|
33618
|
-
|
|
33619
|
-
|
|
33620
|
-
|
|
33621
|
-
|
|
33622
|
-
|
|
33623
|
-
|
|
33624
|
-
|
|
33625
|
-
|
|
33626
|
-
|
|
33627
|
-
|
|
33628
|
-
|
|
33629
|
-
|
|
33630
|
-
|
|
33631
|
-
hasDataProp =
|
|
33632
|
-
|
|
33737
|
+
create: (context) => {
|
|
33738
|
+
let fileImportsRecycler = false;
|
|
33739
|
+
return {
|
|
33740
|
+
Program(node) {
|
|
33741
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33742
|
+
},
|
|
33743
|
+
JSXOpeningElement(node) {
|
|
33744
|
+
if (!fileImportsRecycler) return;
|
|
33745
|
+
const localElementName = resolveJsxElementName(node);
|
|
33746
|
+
if (!localElementName) return;
|
|
33747
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
33748
|
+
if (canonicalRecyclerName === null) return;
|
|
33749
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
33750
|
+
let hasSizingHint = false;
|
|
33751
|
+
let dataIsEmptyLiteral = false;
|
|
33752
|
+
let hasDataProp = false;
|
|
33753
|
+
for (const attribute of node.attributes ?? []) {
|
|
33754
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
33755
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
33756
|
+
const attributeName = attribute.name.name;
|
|
33757
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
33758
|
+
if (attributeName === "data") {
|
|
33759
|
+
hasDataProp = true;
|
|
33760
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
33761
|
+
}
|
|
33762
|
+
}
|
|
33763
|
+
if (hasSizingHint) return;
|
|
33764
|
+
if (dataIsEmptyLiteral) return;
|
|
33765
|
+
if (!hasDataProp) return;
|
|
33766
|
+
context.report({
|
|
33767
|
+
node,
|
|
33768
|
+
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33769
|
+
});
|
|
33633
33770
|
}
|
|
33634
|
-
}
|
|
33635
|
-
|
|
33636
|
-
if (dataIsEmptyLiteral) return;
|
|
33637
|
-
if (!hasDataProp) return;
|
|
33638
|
-
context.report({
|
|
33639
|
-
node,
|
|
33640
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33641
|
-
});
|
|
33642
|
-
} })
|
|
33771
|
+
};
|
|
33772
|
+
}
|
|
33643
33773
|
});
|
|
33644
33774
|
//#endregion
|
|
33645
33775
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33650,25 +33780,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33650
33780
|
requires: ["react-native"],
|
|
33651
33781
|
severity: "warn",
|
|
33652
33782
|
recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
|
|
33653
|
-
create: (context) =>
|
|
33654
|
-
|
|
33655
|
-
|
|
33656
|
-
|
|
33657
|
-
|
|
33658
|
-
|
|
33659
|
-
|
|
33660
|
-
|
|
33661
|
-
|
|
33662
|
-
|
|
33663
|
-
|
|
33664
|
-
|
|
33665
|
-
|
|
33666
|
-
|
|
33667
|
-
|
|
33668
|
-
|
|
33669
|
-
|
|
33670
|
-
|
|
33671
|
-
|
|
33783
|
+
create: (context) => {
|
|
33784
|
+
let fileImportsRecycler = false;
|
|
33785
|
+
return {
|
|
33786
|
+
Program(node) {
|
|
33787
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33788
|
+
},
|
|
33789
|
+
JSXOpeningElement(node) {
|
|
33790
|
+
if (!fileImportsRecycler) return;
|
|
33791
|
+
const elementName = resolveJsxElementName(node);
|
|
33792
|
+
if (!elementName) return;
|
|
33793
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
33794
|
+
let hasRecycleItemsEnabled = false;
|
|
33795
|
+
let hasGetItemType = false;
|
|
33796
|
+
for (const attr of node.attributes ?? []) {
|
|
33797
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
33798
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
33799
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
33800
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
33801
|
+
else hasRecycleItemsEnabled = true;
|
|
33802
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
33803
|
+
}
|
|
33804
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
33805
|
+
node,
|
|
33806
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
33807
|
+
});
|
|
33808
|
+
}
|
|
33809
|
+
};
|
|
33810
|
+
}
|
|
33672
33811
|
});
|
|
33673
33812
|
//#endregion
|
|
33674
33813
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -34698,6 +34837,7 @@ const rnNoRawText = defineRule({
|
|
|
34698
34837
|
return {
|
|
34699
34838
|
Program(programNode) {
|
|
34700
34839
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
34840
|
+
if (!containsJsxElement(programNode)) return;
|
|
34701
34841
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34702
34842
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34703
34843
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38652,14 +38792,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38652
38792
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38653
38793
|
category: "Accessibility",
|
|
38654
38794
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38655
|
-
|
|
38656
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38657
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38658
|
-
if (!role) return;
|
|
38659
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38660
|
-
const isImplicit = !roleAttribute;
|
|
38661
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38662
|
-
if (!supported) return;
|
|
38795
|
+
let ariaAttributes = null;
|
|
38663
38796
|
for (const attribute of node.attributes) {
|
|
38664
38797
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38665
38798
|
const attributeNode = attribute;
|
|
@@ -38669,6 +38802,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38669
38802
|
const propName = propRawName.toLowerCase();
|
|
38670
38803
|
if (!propName.startsWith("aria-")) continue;
|
|
38671
38804
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
38805
|
+
(ariaAttributes ??= []).push({
|
|
38806
|
+
attribute,
|
|
38807
|
+
propName
|
|
38808
|
+
});
|
|
38809
|
+
}
|
|
38810
|
+
if (!ariaAttributes) return;
|
|
38811
|
+
const elementType = getElementType(node, context.settings);
|
|
38812
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38813
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38814
|
+
if (!role) return;
|
|
38815
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38816
|
+
const isImplicit = !roleAttribute;
|
|
38817
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38818
|
+
if (!supported) return;
|
|
38819
|
+
for (const { attribute, propName } of ariaAttributes) {
|
|
38672
38820
|
if (supported.has(propName)) continue;
|
|
38673
38821
|
context.report({
|
|
38674
38822
|
node: attribute,
|
|
@@ -39506,6 +39654,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39506
39654
|
cachedFunctionNames.add(node.id.name);
|
|
39507
39655
|
},
|
|
39508
39656
|
CallExpression(node) {
|
|
39657
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39509
39658
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39510
39659
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39511
39660
|
const firstArg = node.arguments?.[0];
|
|
@@ -40432,7 +40581,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40432
40581
|
//#endregion
|
|
40433
40582
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40434
40583
|
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
40435
|
-
const
|
|
40584
|
+
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;
|
|
40585
|
+
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40586
|
+
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40587
|
+
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40588
|
+
const tableName = match[1];
|
|
40589
|
+
if (tableName === void 0) continue;
|
|
40590
|
+
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40591
|
+
}
|
|
40592
|
+
return lastEnableIndexByTable;
|
|
40593
|
+
};
|
|
40436
40594
|
const supabaseTableMissingRls = defineRule({
|
|
40437
40595
|
id: "supabase-table-missing-rls",
|
|
40438
40596
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40443,11 +40601,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40443
40601
|
const content = sanitizeSqlForScan(file.content);
|
|
40444
40602
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40445
40603
|
const findings = [];
|
|
40604
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40446
40605
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40447
40606
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40448
40607
|
const tableName = match[1];
|
|
40449
40608
|
if (tableName === void 0) continue;
|
|
40450
|
-
|
|
40609
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40610
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40451
40611
|
const location = getLocationAtIndex(content, match.index);
|
|
40452
40612
|
findings.push({
|
|
40453
40613
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40723,6 +40883,7 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40723
40883
|
severity: "warn",
|
|
40724
40884
|
recommendation: "Add `<HeadContent />` inside `<head>` in your __root route. Without it, route `head()` meta tags are dropped.",
|
|
40725
40885
|
create: (context) => {
|
|
40886
|
+
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(context.filename ?? "")) return {};
|
|
40726
40887
|
let hasHeadContentElement = false;
|
|
40727
40888
|
let hasDocumentHeadElement = false;
|
|
40728
40889
|
let hasCustomHeadChildElement = false;
|
|
@@ -40759,8 +40920,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40759
40920
|
};
|
|
40760
40921
|
return {
|
|
40761
40922
|
Program(node) {
|
|
40762
|
-
const filename = context.filename ?? "";
|
|
40763
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40764
40923
|
const statements = node.body ?? [];
|
|
40765
40924
|
for (const statement of statements) collectImportBindings(statement);
|
|
40766
40925
|
for (const statement of statements) {
|
|
@@ -40769,18 +40928,12 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40769
40928
|
}
|
|
40770
40929
|
},
|
|
40771
40930
|
ImportDeclaration(node) {
|
|
40772
|
-
const filename = context.filename ?? "";
|
|
40773
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40774
40931
|
collectImportBindings(node);
|
|
40775
40932
|
},
|
|
40776
40933
|
VariableDeclarator(node) {
|
|
40777
|
-
const filename = context.filename ?? "";
|
|
40778
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40779
40934
|
collectVariableAlias(node);
|
|
40780
40935
|
},
|
|
40781
40936
|
JSXOpeningElement(node) {
|
|
40782
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40783
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40784
40937
|
if (isNodeOfType(node.name, "JSXIdentifier")) {
|
|
40785
40938
|
if (node.name.name === DOCUMENT_HEAD_ELEMENT_NAME) hasDocumentHeadElement = true;
|
|
40786
40939
|
if (headContentComponentNames.has(node.name.name)) hasHeadContentElement = true;
|
|
@@ -40794,8 +40947,6 @@ const tanstackStartMissingHeadContent = defineRule({
|
|
|
40794
40947
|
if (isInsideDocumentHeadElement(node) && isCustomJsxElementName(node.name)) hasCustomHeadChildElement = true;
|
|
40795
40948
|
},
|
|
40796
40949
|
"Program:exit"(programNode) {
|
|
40797
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
40798
|
-
if (!TANSTACK_ROOT_ROUTE_FILE_PATTERN.test(filename)) return;
|
|
40799
40950
|
if (hasDocumentHeadElement && !hasHeadContentElement && !hasCustomHeadChildElement) context.report({
|
|
40800
40951
|
node: programNode,
|
|
40801
40952
|
message: "Without <HeadContent /> in the __root route, your route head() meta tags never render."
|
|
@@ -40819,27 +40970,29 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
40819
40970
|
requires: ["tanstack-start"],
|
|
40820
40971
|
severity: "warn",
|
|
40821
40972
|
recommendation: "Use `Link` from `@tanstack/react-router` so internal navigation keeps client state, preloading, and typed routes.",
|
|
40822
|
-
create: (context) =>
|
|
40823
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
40824
|
-
|
|
40825
|
-
|
|
40826
|
-
|
|
40827
|
-
|
|
40828
|
-
|
|
40829
|
-
|
|
40830
|
-
|
|
40831
|
-
|
|
40832
|
-
|
|
40833
|
-
|
|
40834
|
-
|
|
40835
|
-
|
|
40836
|
-
|
|
40837
|
-
|
|
40838
|
-
|
|
40839
|
-
|
|
40840
|
-
|
|
40841
|
-
|
|
40842
|
-
|
|
40973
|
+
create: (context) => {
|
|
40974
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40975
|
+
return { JSXOpeningElement(node) {
|
|
40976
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a") return;
|
|
40977
|
+
const attributes = node.attributes ?? [];
|
|
40978
|
+
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
40979
|
+
if (!hrefAttribute?.value) return;
|
|
40980
|
+
let hrefValue = null;
|
|
40981
|
+
if (isNodeOfType(hrefAttribute.value, "Literal")) hrefValue = hrefAttribute.value.value;
|
|
40982
|
+
else if (isNodeOfType(hrefAttribute.value, "JSXExpressionContainer") && isNodeOfType(hrefAttribute.value.expression, "Literal")) hrefValue = hrefAttribute.value.expression.value;
|
|
40983
|
+
if (typeof hrefValue !== "string" || !hrefValue.startsWith("/")) return;
|
|
40984
|
+
if (hrefValue.startsWith("//")) return;
|
|
40985
|
+
const pathname = hrefValue.split(/[?#]/)[0] ?? hrefValue;
|
|
40986
|
+
if (pathname.startsWith("/api/")) return;
|
|
40987
|
+
if (/\.[a-z0-9]{1,8}$/i.test(pathname)) return;
|
|
40988
|
+
if (findJsxAttribute(attributes, "download")) return;
|
|
40989
|
+
if (getAttributeStringValue(findJsxAttribute(attributes, "target")) === "_blank") return;
|
|
40990
|
+
context.report({
|
|
40991
|
+
node,
|
|
40992
|
+
message: "Plain <a> reloads the whole page for internal navigation, so TanStack Router loses client state and preloading."
|
|
40993
|
+
});
|
|
40994
|
+
} };
|
|
40995
|
+
}
|
|
40843
40996
|
});
|
|
40844
40997
|
//#endregion
|
|
40845
40998
|
//#region src/plugin/rules/tanstack-start/tanstack-start-no-direct-fetch-in-loader.ts
|
|
@@ -40903,7 +41056,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40903
41056
|
severity: "warn",
|
|
40904
41057
|
recommendation: "Use `throw redirect({ to: '/path' })` in `beforeLoad` or `loader`. navigate() during render causes hydration issues.",
|
|
40905
41058
|
create: (context) => {
|
|
40906
|
-
|
|
41059
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
40907
41060
|
let deferredCallbackDepth = 0;
|
|
40908
41061
|
let eventHandlerDepth = 0;
|
|
40909
41062
|
const isDeferredHookCall = (node) => isHookCall$1(node, EFFECT_HOOK_NAMES$1) || isHookCall$1(node, "useCallback") || isHookCall$1(node, "useMemo");
|
|
@@ -40967,7 +41120,6 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40967
41120
|
};
|
|
40968
41121
|
return {
|
|
40969
41122
|
CallExpression(node) {
|
|
40970
|
-
if (!isRouteFile) return;
|
|
40971
41123
|
if (isDeferredHookCall(node)) deferredCallbackDepth++;
|
|
40972
41124
|
if (deferredCallbackDepth > 0 || eventHandlerDepth > 0) return;
|
|
40973
41125
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "navigate" && (node.arguments?.length ?? 0) > 0) {
|
|
@@ -40979,39 +41131,30 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40979
41131
|
}
|
|
40980
41132
|
},
|
|
40981
41133
|
"CallExpression:exit"(node) {
|
|
40982
|
-
if (!isRouteFile) return;
|
|
40983
41134
|
if (isDeferredHookCall(node)) deferredCallbackDepth = Math.max(0, deferredCallbackDepth - 1);
|
|
40984
41135
|
},
|
|
40985
41136
|
JSXAttribute(node) {
|
|
40986
|
-
if (!isRouteFile) return;
|
|
40987
41137
|
if (isEventHandlerAttribute(node)) eventHandlerDepth++;
|
|
40988
41138
|
},
|
|
40989
41139
|
"JSXAttribute:exit"(node) {
|
|
40990
|
-
if (!isRouteFile) return;
|
|
40991
41140
|
if (isEventHandlerAttribute(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
40992
41141
|
},
|
|
40993
41142
|
Property(node) {
|
|
40994
|
-
if (!isRouteFile) return;
|
|
40995
41143
|
if (isEventHandlerProperty(node)) eventHandlerDepth++;
|
|
40996
41144
|
},
|
|
40997
41145
|
"Property:exit"(node) {
|
|
40998
|
-
if (!isRouteFile) return;
|
|
40999
41146
|
if (isEventHandlerProperty(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41000
41147
|
},
|
|
41001
41148
|
VariableDeclarator(node) {
|
|
41002
|
-
if (!isRouteFile) return;
|
|
41003
41149
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth++;
|
|
41004
41150
|
},
|
|
41005
41151
|
"VariableDeclarator:exit"(node) {
|
|
41006
|
-
if (!isRouteFile) return;
|
|
41007
41152
|
if (isHandlerNamedVariableDeclarator(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41008
41153
|
},
|
|
41009
41154
|
FunctionDeclaration(node) {
|
|
41010
|
-
if (!isRouteFile) return;
|
|
41011
41155
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth++;
|
|
41012
41156
|
},
|
|
41013
41157
|
"FunctionDeclaration:exit"(node) {
|
|
41014
|
-
if (!isRouteFile) return;
|
|
41015
41158
|
if (isHandlerNamedFunctionDeclaration(node)) eventHandlerDepth = Math.max(0, eventHandlerDepth - 1);
|
|
41016
41159
|
}
|
|
41017
41160
|
};
|
|
@@ -41095,23 +41238,25 @@ const tanstackStartNoUseEffectFetch = defineRule({
|
|
|
41095
41238
|
requires: ["tanstack-start"],
|
|
41096
41239
|
severity: "warn",
|
|
41097
41240
|
recommendation: "Fetch data in the route `loader` instead. The router loads it before rendering and avoids waterfalls.",
|
|
41098
|
-
create: (context) =>
|
|
41099
|
-
if (!isInProjectDirectory(context, "routes")) return;
|
|
41100
|
-
|
|
41101
|
-
|
|
41102
|
-
|
|
41103
|
-
|
|
41104
|
-
|
|
41105
|
-
|
|
41106
|
-
|
|
41107
|
-
|
|
41108
|
-
|
|
41109
|
-
|
|
41110
|
-
|
|
41111
|
-
|
|
41112
|
-
|
|
41113
|
-
|
|
41114
|
-
|
|
41241
|
+
create: (context) => {
|
|
41242
|
+
if (!isInProjectDirectory(context, "routes")) return {};
|
|
41243
|
+
return { CallExpression(node) {
|
|
41244
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
41245
|
+
const callback = node.arguments?.[0];
|
|
41246
|
+
if (!callback) return;
|
|
41247
|
+
let hasFetchCall = false;
|
|
41248
|
+
const effectInvokedFunctions = collectEffectInvokedFunctions(callback);
|
|
41249
|
+
walkAst(callback, (child) => {
|
|
41250
|
+
if (hasFetchCall) return false;
|
|
41251
|
+
if (child !== callback && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
41252
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === "fetch") hasFetchCall = true;
|
|
41253
|
+
});
|
|
41254
|
+
if (hasFetchCall) context.report({
|
|
41255
|
+
node,
|
|
41256
|
+
message: "fetch() inside useEffect makes your users wait through a loading spinner after render."
|
|
41257
|
+
});
|
|
41258
|
+
} };
|
|
41259
|
+
}
|
|
41115
41260
|
});
|
|
41116
41261
|
//#endregion
|
|
41117
41262
|
//#region src/plugin/rules/tanstack-start/tanstack-start-redirect-in-try-catch.ts
|
|
@@ -41479,13 +41624,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41479
41624
|
//#endregion
|
|
41480
41625
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41481
41626
|
const ZOD_MODULE = "zod";
|
|
41627
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41482
41628
|
const getStaticPropertyName = (member) => {
|
|
41483
41629
|
const property = member.property;
|
|
41484
41630
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41485
41631
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41486
41632
|
return null;
|
|
41487
41633
|
};
|
|
41634
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41488
41635
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41636
|
+
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41637
|
+
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41638
|
+
importInfoCache.set(identifier, importInfo);
|
|
41639
|
+
return importInfo;
|
|
41640
|
+
};
|
|
41641
|
+
const computeImportInfoForIdentifier = (identifier) => {
|
|
41489
41642
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41490
41643
|
if (!specifier) return null;
|
|
41491
41644
|
const declaration = specifier.parent;
|
|
@@ -41622,25 +41775,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41622
41775
|
tags: ["migration-hint"],
|
|
41623
41776
|
severity: "warn",
|
|
41624
41777
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41625
|
-
create: (context) =>
|
|
41626
|
-
|
|
41627
|
-
|
|
41628
|
-
|
|
41629
|
-
|
|
41630
|
-
|
|
41631
|
-
|
|
41632
|
-
|
|
41633
|
-
|
|
41634
|
-
|
|
41635
|
-
|
|
41636
|
-
|
|
41637
|
-
|
|
41638
|
-
|
|
41639
|
-
|
|
41640
|
-
|
|
41641
|
-
|
|
41642
|
-
|
|
41643
|
-
|
|
41778
|
+
create: (context) => {
|
|
41779
|
+
let fileImportsZod = false;
|
|
41780
|
+
return {
|
|
41781
|
+
Program(node) {
|
|
41782
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
41783
|
+
},
|
|
41784
|
+
CallExpression(node) {
|
|
41785
|
+
if (!fileImportsZod) return;
|
|
41786
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
41787
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
41788
|
+
context.report({
|
|
41789
|
+
node,
|
|
41790
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41791
|
+
});
|
|
41792
|
+
},
|
|
41793
|
+
MemberExpression(node) {
|
|
41794
|
+
if (!fileImportsZod) return;
|
|
41795
|
+
const parent = node.parent;
|
|
41796
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41797
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
41798
|
+
context.report({
|
|
41799
|
+
node,
|
|
41800
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41801
|
+
});
|
|
41802
|
+
}
|
|
41803
|
+
};
|
|
41804
|
+
}
|
|
41644
41805
|
});
|
|
41645
41806
|
//#endregion
|
|
41646
41807
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41832,16 +41993,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41832
41993
|
tags: ["migration-hint"],
|
|
41833
41994
|
severity: "warn",
|
|
41834
41995
|
recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
|
|
41835
|
-
create: (context) =>
|
|
41836
|
-
|
|
41837
|
-
|
|
41838
|
-
|
|
41839
|
-
|
|
41840
|
-
|
|
41841
|
-
|
|
41842
|
-
|
|
41843
|
-
|
|
41844
|
-
|
|
41996
|
+
create: (context) => {
|
|
41997
|
+
let fileImportsZod = false;
|
|
41998
|
+
return {
|
|
41999
|
+
Program(node) {
|
|
42000
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42001
|
+
},
|
|
42002
|
+
CallExpression(node) {
|
|
42003
|
+
if (!fileImportsZod) return;
|
|
42004
|
+
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);
|
|
42005
|
+
},
|
|
42006
|
+
MemberExpression(node) {
|
|
42007
|
+
if (!fileImportsZod) return;
|
|
42008
|
+
const parent = node.parent;
|
|
42009
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42010
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
42011
|
+
}
|
|
42012
|
+
};
|
|
42013
|
+
}
|
|
41845
42014
|
});
|
|
41846
42015
|
//#endregion
|
|
41847
42016
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41876,13 +42045,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41876
42045
|
tags: ["migration-hint"],
|
|
41877
42046
|
severity: "warn",
|
|
41878
42047
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41879
|
-
create: (context) =>
|
|
41880
|
-
|
|
41881
|
-
|
|
41882
|
-
node
|
|
41883
|
-
|
|
41884
|
-
|
|
41885
|
-
|
|
42048
|
+
create: (context) => {
|
|
42049
|
+
let fileImportsZod = false;
|
|
42050
|
+
return {
|
|
42051
|
+
Program(node) {
|
|
42052
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42053
|
+
},
|
|
42054
|
+
CallExpression(node) {
|
|
42055
|
+
if (!fileImportsZod) return;
|
|
42056
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42057
|
+
context.report({
|
|
42058
|
+
node,
|
|
42059
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42060
|
+
});
|
|
42061
|
+
}
|
|
42062
|
+
};
|
|
42063
|
+
}
|
|
41886
42064
|
});
|
|
41887
42065
|
//#endregion
|
|
41888
42066
|
//#region src/plugin/rule-registry.ts
|