oxlint-plugin-react-doctor 0.6.2-dev.fc75a3e → 0.6.3-dev.1571119
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 +956 -555
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -378,13 +378,18 @@ const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
|
378
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);
|
|
379
379
|
//#endregion
|
|
380
380
|
//#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
|
|
381
|
+
const classificationCacheByPattern = /* @__PURE__ */ new Map();
|
|
381
382
|
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
382
|
-
|
|
383
|
-
if (
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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;
|
|
388
393
|
};
|
|
389
394
|
//#endregion
|
|
390
395
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
@@ -583,7 +588,7 @@ const SETTER_PATTERN = /^set[A-Z]/;
|
|
|
583
588
|
const RENDER_FUNCTION_PATTERN = /^render[A-Z]/;
|
|
584
589
|
const UPPERCASE_PATTERN = /^[A-Z]/;
|
|
585
590
|
const REACT_HANDLER_PROP_PATTERN = /^on[A-Z]/;
|
|
586
|
-
const HOOK_NAME_PATTERN = /^use[A-Z]/;
|
|
591
|
+
const HOOK_NAME_PATTERN$1 = /^use[A-Z]/;
|
|
587
592
|
const HANDLER_FUNCTION_NAME_PATTERN = /^(?:on|handle)[A-Z]/;
|
|
588
593
|
const EFFECT_HOOK_NAMES$1 = new Set(["useEffect", "useLayoutEffect"]);
|
|
589
594
|
const HOOKS_WITH_DEPS = new Set([
|
|
@@ -783,24 +788,6 @@ const collectChildComponentNames = (element, into) => {
|
|
|
783
788
|
into.add(name);
|
|
784
789
|
});
|
|
785
790
|
};
|
|
786
|
-
const findSameFileComponentBody = (programRoot, componentName) => {
|
|
787
|
-
let foundBody = null;
|
|
788
|
-
walkAst(programRoot, (node) => {
|
|
789
|
-
if (foundBody) return false;
|
|
790
|
-
if (isNodeOfType(node, "FunctionDeclaration") && node.id && node.id.name === componentName) {
|
|
791
|
-
foundBody = node.body;
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.id.name === componentName) {
|
|
795
|
-
const initializer = node.init;
|
|
796
|
-
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) {
|
|
797
|
-
foundBody = initializer.body;
|
|
798
|
-
return false;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
});
|
|
802
|
-
return foundBody;
|
|
803
|
-
};
|
|
804
791
|
const countEffectHookCalls = (body) => {
|
|
805
792
|
if (!body) return 0;
|
|
806
793
|
let count = 0;
|
|
@@ -810,6 +797,36 @@ const countEffectHookCalls = (body) => {
|
|
|
810
797
|
});
|
|
811
798
|
return count;
|
|
812
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
|
+
};
|
|
813
830
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
814
831
|
id: "activity-wraps-effect-heavy-subtree",
|
|
815
832
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -866,9 +883,7 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
866
883
|
let totalEffects = 0;
|
|
867
884
|
const effectfulChildren = [];
|
|
868
885
|
for (const componentName of childComponentNames) {
|
|
869
|
-
const
|
|
870
|
-
if (!body) continue;
|
|
871
|
-
const effectCount = countEffectHookCalls(body);
|
|
886
|
+
const effectCount = getSameFileComponentEffectCount(programRoot, componentName);
|
|
872
887
|
if (effectCount === 0) continue;
|
|
873
888
|
totalEffects += effectCount;
|
|
874
889
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1458,6 +1473,12 @@ const getElementType = (openingElement, settings) => {
|
|
|
1458
1473
|
return elementType;
|
|
1459
1474
|
};
|
|
1460
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
|
+
};
|
|
1481
|
+
//#endregion
|
|
1461
1482
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1462
1483
|
const collectFromProgram = (programRoot) => {
|
|
1463
1484
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -1661,7 +1682,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1661
1682
|
};
|
|
1662
1683
|
//#endregion
|
|
1663
1684
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1664
|
-
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1685
|
+
const normalizeFilename = (filename) => filename.includes("\\") ? filename.replaceAll("\\", "/") : filename;
|
|
1665
1686
|
//#endregion
|
|
1666
1687
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1667
1688
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -2010,7 +2031,12 @@ const altText = defineRule({
|
|
|
2010
2031
|
const objectAliases = new Set(settings.object ?? []);
|
|
2011
2032
|
const areaAliases = new Set(settings.area ?? []);
|
|
2012
2033
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2034
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2013
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
|
+
}
|
|
2014
2040
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
2015
2041
|
const tag = getElementType(node, context.settings);
|
|
2016
2042
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2193,7 +2219,9 @@ const anchorIsValid = defineRule({
|
|
|
2193
2219
|
category: "Accessibility",
|
|
2194
2220
|
create: (context) => {
|
|
2195
2221
|
const settings = resolveSettings$50(context.settings);
|
|
2222
|
+
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2196
2223
|
return { JSXOpeningElement(node) {
|
|
2224
|
+
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2197
2225
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2198
2226
|
let hrefAttribute;
|
|
2199
2227
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3619,8 +3647,9 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3619
3647
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3620
3648
|
//#endregion
|
|
3621
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");
|
|
3622
3651
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3623
|
-
for (const match of content.matchAll(
|
|
3652
|
+
for (const match of content.matchAll(PUBLIC_ENV_SECRET_NAME_GLOBAL_PATTERN)) {
|
|
3624
3653
|
const value = match[0] ?? "";
|
|
3625
3654
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3626
3655
|
}
|
|
@@ -4087,62 +4116,56 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4087
4116
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4088
4117
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4089
4118
|
};
|
|
4090
|
-
const collectAssignedIdentifiers = (block) => {
|
|
4091
|
-
const assigned = /* @__PURE__ */ new Set();
|
|
4092
|
-
walkAst(block, (child) => {
|
|
4093
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4094
|
-
if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
|
|
4095
|
-
});
|
|
4096
|
-
return assigned;
|
|
4097
|
-
};
|
|
4098
|
-
const collectAwaitedArgIdentifiers = (block) => {
|
|
4099
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
4100
|
-
walkAst(block, (child) => {
|
|
4101
|
-
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4102
|
-
if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
|
|
4103
|
-
collectReferenceIdentifierNames(child.argument, referenced);
|
|
4104
|
-
});
|
|
4105
|
-
return referenced;
|
|
4106
|
-
};
|
|
4107
4119
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4108
4120
|
"push",
|
|
4109
4121
|
"unshift",
|
|
4110
4122
|
"splice"
|
|
4111
4123
|
]);
|
|
4112
|
-
const
|
|
4113
|
-
const
|
|
4124
|
+
const addDerivedBindings = (block, names) => {
|
|
4125
|
+
const declaratorBindings = [];
|
|
4114
4126
|
walkAst(block, (child) => {
|
|
4115
4127
|
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4116
|
-
if (!isNodeOfType(child, "
|
|
4117
|
-
|
|
4118
|
-
|
|
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
|
+
});
|
|
4119
4136
|
});
|
|
4120
|
-
return mutated;
|
|
4121
|
-
};
|
|
4122
|
-
const addDerivedBindings = (block, names) => {
|
|
4123
4137
|
let didGrow = true;
|
|
4124
4138
|
while (didGrow) {
|
|
4125
4139
|
didGrow = false;
|
|
4126
|
-
|
|
4127
|
-
if (
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
const initReferences = /* @__PURE__ */ new Set();
|
|
4131
|
-
collectReferenceIdentifierNames(child.init, initReferences);
|
|
4132
|
-
for (const referenced of initReferences) if (names.has(referenced)) {
|
|
4133
|
-
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);
|
|
4134
4144
|
didGrow = true;
|
|
4135
4145
|
break;
|
|
4136
4146
|
}
|
|
4137
|
-
}
|
|
4147
|
+
}
|
|
4138
4148
|
}
|
|
4139
4149
|
};
|
|
4140
4150
|
const hasLoopCarriedDependency = (block) => {
|
|
4141
|
-
const carried =
|
|
4142
|
-
|
|
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
|
+
});
|
|
4143
4167
|
if (carried.size === 0) return false;
|
|
4144
4168
|
addDerivedBindings(block, carried);
|
|
4145
|
-
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4146
4169
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4147
4170
|
return false;
|
|
4148
4171
|
};
|
|
@@ -4820,7 +4843,8 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
4820
4843
|
]);
|
|
4821
4844
|
const resolveSettings$48 = (settings) => {
|
|
4822
4845
|
const reactDoctor = settings?.["react-doctor"];
|
|
4823
|
-
|
|
4846
|
+
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {};
|
|
4847
|
+
return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
|
|
4824
4848
|
};
|
|
4825
4849
|
const autocompleteValid = defineRule({
|
|
4826
4850
|
id: "autocomplete-valid",
|
|
@@ -4833,7 +4857,7 @@ const autocompleteValid = defineRule({
|
|
|
4833
4857
|
const settings = resolveSettings$48(context.settings);
|
|
4834
4858
|
return { JSXOpeningElement: (node) => {
|
|
4835
4859
|
const tag = getElementType(node, context.settings);
|
|
4836
|
-
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.
|
|
4860
|
+
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.has(tag)) return;
|
|
4837
4861
|
const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
|
|
4838
4862
|
if (!attribute) return;
|
|
4839
4863
|
const value = getJsxPropStringValue(attribute);
|
|
@@ -4877,9 +4901,8 @@ const isCreateElementCall = (node) => {
|
|
|
4877
4901
|
const callee = node.callee;
|
|
4878
4902
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
|
|
4879
4903
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
4880
|
-
if (
|
|
4881
|
-
|
|
4882
|
-
return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
|
|
4904
|
+
if (!(callee.computed ? isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement" : isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement")) return false;
|
|
4905
|
+
return !memberChainContainsDocument(callee.object);
|
|
4883
4906
|
}
|
|
4884
4907
|
return false;
|
|
4885
4908
|
};
|
|
@@ -4905,7 +4928,8 @@ const isValidTypeValue = (rawValue, settings) => {
|
|
|
4905
4928
|
if (rawValue === "reset") return settings.reset;
|
|
4906
4929
|
return false;
|
|
4907
4930
|
};
|
|
4908
|
-
const isProvenValidExpression = (
|
|
4931
|
+
const isProvenValidExpression = (rawExpression, settings, resolvedBindings = /* @__PURE__ */ new Set()) => {
|
|
4932
|
+
const expression = stripParenExpression(rawExpression);
|
|
4909
4933
|
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return isValidTypeValue(expression.value, settings);
|
|
4910
4934
|
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
4911
4935
|
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
@@ -5072,11 +5096,20 @@ const resolveSettings$46 = (settings) => {
|
|
|
5072
5096
|
ignoreExclusiveCheckedAttribute: ruleSettings.ignoreExclusiveCheckedAttribute ?? false
|
|
5073
5097
|
};
|
|
5074
5098
|
};
|
|
5099
|
+
const isTruthyDisabledJsxValue = (attribute) => {
|
|
5100
|
+
if (!attribute.value) return true;
|
|
5101
|
+
if (isNodeOfType(attribute.value, "JSXExpressionContainer")) {
|
|
5102
|
+
const expression = attribute.value.expression;
|
|
5103
|
+
return isNodeOfType(expression, "Literal") && expression.value === true;
|
|
5104
|
+
}
|
|
5105
|
+
return false;
|
|
5106
|
+
};
|
|
5075
5107
|
const collectFromJsxAttributes = (attributes) => {
|
|
5076
5108
|
let checkedNode = null;
|
|
5077
5109
|
let defaultCheckedNode = null;
|
|
5078
5110
|
let hasOnChangeOrReadOnly = false;
|
|
5079
5111
|
let hasSpread = false;
|
|
5112
|
+
let hasTruthyDisabled = false;
|
|
5080
5113
|
for (const attribute of attributes) {
|
|
5081
5114
|
if (isNodeOfType(attribute, "JSXSpreadAttribute")) {
|
|
5082
5115
|
hasSpread = true;
|
|
@@ -5087,12 +5120,14 @@ const collectFromJsxAttributes = (attributes) => {
|
|
|
5087
5120
|
if (name === "checked") checkedNode = attribute;
|
|
5088
5121
|
else if (name === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = attribute;
|
|
5089
5122
|
else if (name === "onChange" || name === "readOnly") hasOnChangeOrReadOnly = true;
|
|
5123
|
+
else if (name === "disabled" && isTruthyDisabledJsxValue(attribute)) hasTruthyDisabled = true;
|
|
5090
5124
|
}
|
|
5091
5125
|
return {
|
|
5092
5126
|
checkedNode,
|
|
5093
5127
|
defaultCheckedNode,
|
|
5094
5128
|
hasOnChangeOrReadOnly,
|
|
5095
|
-
hasSpread
|
|
5129
|
+
hasSpread,
|
|
5130
|
+
hasTruthyDisabled
|
|
5096
5131
|
};
|
|
5097
5132
|
};
|
|
5098
5133
|
const collectFromObjectProperties = (objectExpression) => {
|
|
@@ -5100,6 +5135,7 @@ const collectFromObjectProperties = (objectExpression) => {
|
|
|
5100
5135
|
let defaultCheckedNode = null;
|
|
5101
5136
|
let hasOnChangeOrReadOnly = false;
|
|
5102
5137
|
let hasSpread = false;
|
|
5138
|
+
let hasTruthyDisabled = false;
|
|
5103
5139
|
for (const property of objectExpression.properties) {
|
|
5104
5140
|
if (isNodeOfType(property, "SpreadElement")) {
|
|
5105
5141
|
hasSpread = true;
|
|
@@ -5114,12 +5150,17 @@ const collectFromObjectProperties = (objectExpression) => {
|
|
|
5114
5150
|
if (propertyName === "checked") checkedNode = property;
|
|
5115
5151
|
else if (propertyName === "defaultChecked" && !defaultCheckedNode) defaultCheckedNode = property;
|
|
5116
5152
|
else if (propertyName === "onChange" || propertyName === "readOnly") hasOnChangeOrReadOnly = true;
|
|
5153
|
+
else if (propertyName === "disabled") {
|
|
5154
|
+
const propertyValue = property.value;
|
|
5155
|
+
if (isNodeOfType(propertyValue, "Literal") && propertyValue.value === true) hasTruthyDisabled = true;
|
|
5156
|
+
}
|
|
5117
5157
|
}
|
|
5118
5158
|
return {
|
|
5119
5159
|
checkedNode,
|
|
5120
5160
|
defaultCheckedNode,
|
|
5121
5161
|
hasOnChangeOrReadOnly,
|
|
5122
|
-
hasSpread
|
|
5162
|
+
hasSpread,
|
|
5163
|
+
hasTruthyDisabled
|
|
5123
5164
|
};
|
|
5124
5165
|
};
|
|
5125
5166
|
const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
@@ -5135,7 +5176,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5135
5176
|
node: presence.checkedNode,
|
|
5136
5177
|
message: EXCLUSIVE_MESSAGE
|
|
5137
5178
|
});
|
|
5138
|
-
if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !settings.ignoreMissingProperties) context.report({
|
|
5179
|
+
if (presence.checkedNode && !presence.hasOnChangeOrReadOnly && !presence.hasSpread && !presence.hasTruthyDisabled && !settings.ignoreMissingProperties) context.report({
|
|
5139
5180
|
node: presence.checkedNode,
|
|
5140
5181
|
message: MISSING_MESSAGE$1
|
|
5141
5182
|
});
|
|
@@ -5696,12 +5737,20 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5696
5737
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5697
5738
|
};
|
|
5698
5739
|
const isInertParseTarget = (target, fileContent) => {
|
|
5740
|
+
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5741
|
+
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5742
|
+
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5699
5743
|
const escapedTarget = escapeRegExp(target);
|
|
5700
5744
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5701
5745
|
if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
|
|
5702
|
-
if (
|
|
5703
|
-
|
|
5704
|
-
|
|
5746
|
+
if (fileHasCreateElement) {
|
|
5747
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5748
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5749
|
+
}
|
|
5750
|
+
if (fileHasIsolatedDocument) {
|
|
5751
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5752
|
+
}
|
|
5753
|
+
if (!fileHasCreateElement) return false;
|
|
5705
5754
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5706
5755
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5707
5756
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5794,10 +5843,9 @@ const dangerousHtmlSink = defineRule({
|
|
|
5794
5843
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5795
5844
|
if (valueIdentifier !== void 0) {
|
|
5796
5845
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5846
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5847
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i").test(file.content)) continue;
|
|
5848
|
+
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
5801
5849
|
}
|
|
5802
5850
|
}
|
|
5803
5851
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5946,6 +5994,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5946
5994
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5947
5995
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5948
5996
|
if (!classNameLiteral) return;
|
|
5997
|
+
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5949
5998
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5950
5999
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5951
6000
|
if (matchedPairs.length === 0) return;
|
|
@@ -5970,6 +6019,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
5970
6019
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5971
6020
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5972
6021
|
if (!classNameLiteral) return;
|
|
6022
|
+
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
5973
6023
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
5974
6024
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
5975
6025
|
if (matchedPairs.length === 0) return;
|
|
@@ -5994,6 +6044,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
5994
6044
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5995
6045
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5996
6046
|
if (!classNameLiteral) return;
|
|
6047
|
+
if (!classNameLiteral.includes("space-")) return;
|
|
5997
6048
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
5998
6049
|
let hasFlexOrGridLayout = false;
|
|
5999
6050
|
for (const token of tokens) {
|
|
@@ -6185,7 +6236,10 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6185
6236
|
const actualMinor = Number(match[2]);
|
|
6186
6237
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6187
6238
|
};
|
|
6239
|
+
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6188
6240
|
const containsJsx$1 = (root) => {
|
|
6241
|
+
const cached = containsJsxCache.get(root);
|
|
6242
|
+
if (cached !== void 0) return cached;
|
|
6189
6243
|
let found = false;
|
|
6190
6244
|
const visit = (node) => {
|
|
6191
6245
|
if (found) return;
|
|
@@ -6208,6 +6262,7 @@ const containsJsx$1 = (root) => {
|
|
|
6208
6262
|
}
|
|
6209
6263
|
};
|
|
6210
6264
|
visit(root);
|
|
6265
|
+
containsJsxCache.set(root, found);
|
|
6211
6266
|
return found;
|
|
6212
6267
|
};
|
|
6213
6268
|
const getStaticMemberName = (node) => {
|
|
@@ -6299,27 +6354,6 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6299
6354
|
for (const member of members) if ((isNodeOfType(member, "PropertyDefinition") || isNodeOfType(member, "MethodDefinition")) && "static" in member && member.static && isNodeOfType(member.key, "Identifier") && member.key.name === "displayName") return true;
|
|
6300
6355
|
return false;
|
|
6301
6356
|
};
|
|
6302
|
-
const hasDisplayNameAssignment = (className, programRoot) => {
|
|
6303
|
-
let found = false;
|
|
6304
|
-
const visit = (node) => {
|
|
6305
|
-
if (found) return;
|
|
6306
|
-
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
|
|
6307
|
-
found = true;
|
|
6308
|
-
return;
|
|
6309
|
-
}
|
|
6310
|
-
const record = node;
|
|
6311
|
-
for (const key of Object.keys(record)) {
|
|
6312
|
-
if (key === "parent") continue;
|
|
6313
|
-
const child = record[key];
|
|
6314
|
-
if (Array.isArray(child)) {
|
|
6315
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6316
|
-
} else if (isAstNode(child)) visit(child);
|
|
6317
|
-
if (found) return;
|
|
6318
|
-
}
|
|
6319
|
-
};
|
|
6320
|
-
visit(programRoot);
|
|
6321
|
-
return found;
|
|
6322
|
-
};
|
|
6323
6357
|
const memberExpressionPath = (node) => {
|
|
6324
6358
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6325
6359
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6327,15 +6361,16 @@ const memberExpressionPath = (node) => {
|
|
|
6327
6361
|
const propertyName = getStaticMemberName(node);
|
|
6328
6362
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6329
6363
|
};
|
|
6330
|
-
const
|
|
6331
|
-
|
|
6364
|
+
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
6365
|
+
const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
6366
|
+
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6367
|
+
if (cached) return cached;
|
|
6368
|
+
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6369
|
+
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6332
6370
|
const visit = (node) => {
|
|
6333
|
-
if (found) return;
|
|
6334
6371
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6335
|
-
if (
|
|
6336
|
-
|
|
6337
|
-
return;
|
|
6338
|
-
}
|
|
6372
|
+
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
6373
|
+
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
6339
6374
|
}
|
|
6340
6375
|
const record = node;
|
|
6341
6376
|
for (const key of Object.keys(record)) {
|
|
@@ -6344,12 +6379,18 @@ const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
|
6344
6379
|
if (Array.isArray(child)) {
|
|
6345
6380
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6346
6381
|
} else if (isAstNode(child)) visit(child);
|
|
6347
|
-
if (found) return;
|
|
6348
6382
|
}
|
|
6349
6383
|
};
|
|
6350
6384
|
visit(programRoot);
|
|
6351
|
-
|
|
6385
|
+
const index = {
|
|
6386
|
+
identifierTargets,
|
|
6387
|
+
memberPathSegments
|
|
6388
|
+
};
|
|
6389
|
+
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6390
|
+
return index;
|
|
6352
6391
|
};
|
|
6392
|
+
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6393
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6353
6394
|
const displayName = defineRule({
|
|
6354
6395
|
id: "display-name",
|
|
6355
6396
|
title: "Component missing display name",
|
|
@@ -6409,7 +6450,7 @@ const displayName = defineRule({
|
|
|
6409
6450
|
},
|
|
6410
6451
|
ArrowFunctionExpression(node) {
|
|
6411
6452
|
if (!containsJsx$1(node)) return;
|
|
6412
|
-
if (isNodeOfType(node.parent, "ArrowFunctionExpression")) {
|
|
6453
|
+
if (isNodeOfType(node.parent, "ArrowFunctionExpression") || isNodeOfType(node.parent, "ReturnStatement")) {
|
|
6413
6454
|
reportAt(node);
|
|
6414
6455
|
return;
|
|
6415
6456
|
}
|
|
@@ -7326,7 +7367,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
7326
7367
|
"ParenthesizedExpression",
|
|
7327
7368
|
"ChainExpression"
|
|
7328
7369
|
]);
|
|
7329
|
-
const unwrapExpression$
|
|
7370
|
+
const unwrapExpression$3 = (node) => {
|
|
7330
7371
|
let current = node;
|
|
7331
7372
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
7332
7373
|
const inner = current.expression;
|
|
@@ -7452,7 +7493,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7452
7493
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
7453
7494
|
const initializerRaw = declarator.init;
|
|
7454
7495
|
if (!initializerRaw) return false;
|
|
7455
|
-
const initializer = unwrapExpression$
|
|
7496
|
+
const initializer = unwrapExpression$3(initializerRaw);
|
|
7456
7497
|
if (symbol.kind === "const") {
|
|
7457
7498
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
7458
7499
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -7472,13 +7513,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
7472
7513
|
return false;
|
|
7473
7514
|
};
|
|
7474
7515
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
7475
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7516
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7476
7517
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
7477
7518
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
7478
7519
|
};
|
|
7479
7520
|
const getFunctionValueNode = (symbol) => {
|
|
7480
7521
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
7481
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
7522
|
+
const initializer = symbol.initializer ? unwrapExpression$3(symbol.initializer) : null;
|
|
7482
7523
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
7483
7524
|
return null;
|
|
7484
7525
|
};
|
|
@@ -7574,6 +7615,14 @@ const flattenReferenceRootName = (reference) => {
|
|
|
7574
7615
|
if (isNodeOfType(referencedIdentifier, "JSXIdentifier")) return referencedIdentifier.name;
|
|
7575
7616
|
return "";
|
|
7576
7617
|
};
|
|
7618
|
+
const REF_CURRENT_SEGMENT = ".current";
|
|
7619
|
+
const truncateAtRefCurrent = (chain) => {
|
|
7620
|
+
const refCurrentIndex = chain.indexOf(REF_CURRENT_SEGMENT);
|
|
7621
|
+
if (refCurrentIndex === -1) return chain;
|
|
7622
|
+
const segmentEndIndex = refCurrentIndex + 8;
|
|
7623
|
+
if (segmentEndIndex === chain.length || chain[segmentEndIndex] === ".") return chain.slice(0, refCurrentIndex);
|
|
7624
|
+
return chain;
|
|
7625
|
+
};
|
|
7577
7626
|
const computeDepKey = (reference) => {
|
|
7578
7627
|
const referencedIdentifier = reference.identifier;
|
|
7579
7628
|
let parent = referencedIdentifier.parent ?? null;
|
|
@@ -7604,21 +7653,22 @@ const computeDepKey = (reference) => {
|
|
|
7604
7653
|
const declarator = outermost.parent;
|
|
7605
7654
|
if (declarator && isNodeOfType(declarator, "VariableDeclarator") && declarator.init === outermost) {
|
|
7606
7655
|
const destructuredPath = getDestructuredPropertyPath(declarator.id);
|
|
7607
|
-
if (destructuredPath) return `${fullName}.${destructuredPath}
|
|
7656
|
+
if (destructuredPath) return truncateAtRefCurrent(`${fullName}.${destructuredPath}`);
|
|
7608
7657
|
}
|
|
7658
|
+
const truncatedName = truncateAtRefCurrent(fullName);
|
|
7659
|
+
if (truncatedName !== fullName) return truncatedName;
|
|
7609
7660
|
if (reference.flag !== "read") {
|
|
7610
7661
|
const lastDotIndex = fullName.lastIndexOf(".");
|
|
7611
7662
|
if (lastDotIndex !== -1) return fullName.slice(0, lastDotIndex);
|
|
7612
7663
|
}
|
|
7613
|
-
if (fullName.endsWith(".current")) return fullName.slice(0, -8);
|
|
7614
7664
|
return fullName;
|
|
7615
7665
|
};
|
|
7616
7666
|
const computeDeclaredDepKey = (entry) => {
|
|
7617
|
-
const stripped = unwrapExpression$
|
|
7667
|
+
const stripped = unwrapExpression$3(entry);
|
|
7618
7668
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7619
7669
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
7620
7670
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) {
|
|
7621
|
-
const callee = unwrapExpression$
|
|
7671
|
+
const callee = unwrapExpression$3(stripped.callee);
|
|
7622
7672
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
7623
7673
|
if (isNodeOfType(callee, "MemberExpression") && !hasComputedMemberExpression(callee)) return stringifyMemberChain(callee);
|
|
7624
7674
|
}
|
|
@@ -7626,16 +7676,16 @@ const computeDeclaredDepKey = (entry) => {
|
|
|
7626
7676
|
};
|
|
7627
7677
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
7628
7678
|
if (!depsArgument) return false;
|
|
7629
|
-
const strippedDepsArgument = unwrapExpression$
|
|
7679
|
+
const strippedDepsArgument = unwrapExpression$3(depsArgument);
|
|
7630
7680
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
7631
7681
|
return strippedDepsArgument.elements.some((element) => {
|
|
7632
7682
|
if (!element) return false;
|
|
7633
|
-
const strippedElement = unwrapExpression$
|
|
7683
|
+
const strippedElement = unwrapExpression$3(element);
|
|
7634
7684
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
7635
7685
|
});
|
|
7636
7686
|
};
|
|
7637
7687
|
const stringifyMemberChain = (node) => {
|
|
7638
|
-
const stripped = unwrapExpression$
|
|
7688
|
+
const stripped = unwrapExpression$3(node);
|
|
7639
7689
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7640
7690
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
7641
7691
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -7677,13 +7727,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
7677
7727
|
return false;
|
|
7678
7728
|
};
|
|
7679
7729
|
const getMemberRootIdentifier = (node) => {
|
|
7680
|
-
const stripped = unwrapExpression$
|
|
7730
|
+
const stripped = unwrapExpression$3(node);
|
|
7681
7731
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
7682
7732
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
7683
7733
|
return null;
|
|
7684
7734
|
};
|
|
7685
7735
|
const hasComputedMemberExpression = (node) => {
|
|
7686
|
-
const stripped = unwrapExpression$
|
|
7736
|
+
const stripped = unwrapExpression$3(node);
|
|
7687
7737
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
7688
7738
|
if (stripped.computed) return true;
|
|
7689
7739
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -7699,7 +7749,7 @@ const getRootSymbol = (node, scopes) => {
|
|
|
7699
7749
|
return rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7700
7750
|
};
|
|
7701
7751
|
const getDeclaredDepSymbolSource = (node) => {
|
|
7702
|
-
const stripped = unwrapExpression$
|
|
7752
|
+
const stripped = unwrapExpression$3(node);
|
|
7703
7753
|
if (isNodeOfType(stripped, "CallExpression") && (stripped.arguments?.length ?? 0) === 0) return stripped.callee;
|
|
7704
7754
|
return node;
|
|
7705
7755
|
};
|
|
@@ -7709,7 +7759,7 @@ const isRegExpLiteral = (node) => {
|
|
|
7709
7759
|
};
|
|
7710
7760
|
const isUnstableInitializer = (node) => {
|
|
7711
7761
|
if (!node) return false;
|
|
7712
|
-
const stripped = unwrapExpression$
|
|
7762
|
+
const stripped = unwrapExpression$3(node);
|
|
7713
7763
|
if (isRegExpLiteral(stripped)) return true;
|
|
7714
7764
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
7715
7765
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -7801,9 +7851,9 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7801
7851
|
};
|
|
7802
7852
|
findReturn(callback);
|
|
7803
7853
|
if (!cleanupFunction) return null;
|
|
7804
|
-
let
|
|
7854
|
+
let found = null;
|
|
7805
7855
|
const visitCleanup = (node) => {
|
|
7806
|
-
if (
|
|
7856
|
+
if (found) return;
|
|
7807
7857
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
7808
7858
|
const candidateName = getRefCurrentNameFromMemberExpression(node);
|
|
7809
7859
|
if (candidateName) {
|
|
@@ -7811,7 +7861,10 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7811
7861
|
const symbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
7812
7862
|
const callbackScope = scopes.ownScopeFor(callback) ?? scopes.scopeFor(callback);
|
|
7813
7863
|
if (!symbol || !isDescendantScope(symbol.scope, callbackScope)) {
|
|
7814
|
-
|
|
7864
|
+
found = {
|
|
7865
|
+
refCurrentName: candidateName,
|
|
7866
|
+
refSymbol: symbol
|
|
7867
|
+
};
|
|
7815
7868
|
return;
|
|
7816
7869
|
}
|
|
7817
7870
|
}
|
|
@@ -7826,7 +7879,19 @@ const findRefCurrentInCleanup = (callback, scopes) => {
|
|
|
7826
7879
|
}
|
|
7827
7880
|
};
|
|
7828
7881
|
visitCleanup(cleanupFunction);
|
|
7829
|
-
return
|
|
7882
|
+
return found;
|
|
7883
|
+
};
|
|
7884
|
+
const hasRefCurrentAssignmentInComponent = (refSymbol) => {
|
|
7885
|
+
if (!refSymbol) return false;
|
|
7886
|
+
for (const reference of refSymbol.references) {
|
|
7887
|
+
const memberParent = reference.identifier.parent;
|
|
7888
|
+
if (!memberParent || !isNodeOfType(memberParent, "MemberExpression")) continue;
|
|
7889
|
+
if (memberParent.object !== reference.identifier) continue;
|
|
7890
|
+
if (getRefCurrentNameFromMemberExpression(memberParent) === null) continue;
|
|
7891
|
+
const assignmentParent = memberParent.parent;
|
|
7892
|
+
if (assignmentParent && isNodeOfType(assignmentParent, "AssignmentExpression") && assignmentParent.left === memberParent) return true;
|
|
7893
|
+
}
|
|
7894
|
+
return false;
|
|
7830
7895
|
};
|
|
7831
7896
|
const hasRefCurrentAssignment = (callback, refCurrentName) => {
|
|
7832
7897
|
let didAssignRefCurrent = false;
|
|
@@ -7865,9 +7930,21 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
7865
7930
|
const visit = (current) => {
|
|
7866
7931
|
if (didFindMemberCall) return;
|
|
7867
7932
|
if (isNodeOfType(current, "CallExpression")) {
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7933
|
+
const callee = unwrapExpression$3(current.callee);
|
|
7934
|
+
if (isNodeOfType(callee, "MemberExpression")) {
|
|
7935
|
+
let chainObject = unwrapExpression$3(callee.object);
|
|
7936
|
+
let doesChainPassThroughCurrent = false;
|
|
7937
|
+
while (chainObject && isNodeOfType(chainObject, "MemberExpression")) {
|
|
7938
|
+
if (isNodeOfType(chainObject.property, "Identifier") && chainObject.property.name === "current") {
|
|
7939
|
+
doesChainPassThroughCurrent = true;
|
|
7940
|
+
break;
|
|
7941
|
+
}
|
|
7942
|
+
chainObject = unwrapExpression$3(chainObject.object);
|
|
7943
|
+
}
|
|
7944
|
+
if (!doesChainPassThroughCurrent && chainObject && isNodeOfType(chainObject, "Identifier") && chainObject.name === rootName) {
|
|
7945
|
+
didFindMemberCall = true;
|
|
7946
|
+
return;
|
|
7947
|
+
}
|
|
7871
7948
|
}
|
|
7872
7949
|
}
|
|
7873
7950
|
const record = current;
|
|
@@ -7928,7 +8005,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7928
8005
|
return;
|
|
7929
8006
|
}
|
|
7930
8007
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
7931
|
-
const callbackExpression = unwrapExpression$
|
|
8008
|
+
const callbackExpression = unwrapExpression$3(callbackArgument);
|
|
7932
8009
|
let callbackToAnalyze = null;
|
|
7933
8010
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
7934
8011
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -7936,7 +8013,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7936
8013
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
7937
8014
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
7938
8015
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
7939
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$
|
|
8016
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$3(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
7940
8017
|
else if (depsArgumentRaw) {
|
|
7941
8018
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
7942
8019
|
context.report({
|
|
@@ -7969,11 +8046,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7969
8046
|
message: buildAssignmentMessage(assignment.name)
|
|
7970
8047
|
});
|
|
7971
8048
|
if (outerAssignments.length > 0) return;
|
|
7972
|
-
const
|
|
8049
|
+
const refCurrentInCleanup = findRefCurrentInCleanup(callbackToAnalyze, context.scopes);
|
|
7973
8050
|
const shouldCheckRefCleanup = EFFECT_HOOKS_ALLOWING_EXTRA_REACTIVE_DEPS.has(hookName) || Boolean(additionalHooksRegex && additionalHooksRegex.test(hookName));
|
|
7974
|
-
if (
|
|
8051
|
+
if (refCurrentInCleanup && shouldCheckRefCleanup && !hasRefCurrentAssignment(callbackToAnalyze, refCurrentInCleanup.refCurrentName) && !hasRefCurrentAssignmentInComponent(refCurrentInCleanup.refSymbol)) context.report({
|
|
7975
8052
|
node: callbackToAnalyze,
|
|
7976
|
-
message: buildRefCleanupMessage(refCurrentName)
|
|
8053
|
+
message: buildRefCleanupMessage(refCurrentInCleanup.refCurrentName)
|
|
7977
8054
|
});
|
|
7978
8055
|
}
|
|
7979
8056
|
if (!depsArgumentRaw) {
|
|
@@ -7993,8 +8070,16 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7993
8070
|
});
|
|
7994
8071
|
return;
|
|
7995
8072
|
}
|
|
7996
|
-
const depsArgument = unwrapExpression$
|
|
7997
|
-
if (isNodeOfType(depsArgument, "
|
|
8073
|
+
const depsArgument = unwrapExpression$3(depsArgumentRaw);
|
|
8074
|
+
if (isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
8075
|
+
if (isAutoDependenciesHook(hookName)) return;
|
|
8076
|
+
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) context.report({
|
|
8077
|
+
node: depsArgument,
|
|
8078
|
+
message: buildMissingDepArrayMessage(hookName)
|
|
8079
|
+
});
|
|
8080
|
+
return;
|
|
8081
|
+
}
|
|
8082
|
+
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null) {
|
|
7998
8083
|
if (isAutoDependenciesHook(hookName)) return;
|
|
7999
8084
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
8000
8085
|
context.report({
|
|
@@ -8003,19 +8088,6 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8003
8088
|
});
|
|
8004
8089
|
return;
|
|
8005
8090
|
}
|
|
8006
|
-
const nonArrayCaptureKeys = callbackToAnalyze !== null ? new Set(collectCaptureDepKeys(callbackToAnalyze, context.scopes).keys) : /* @__PURE__ */ new Set();
|
|
8007
|
-
for (const forcedCaptureKey of forcedCaptureKeys) nonArrayCaptureKeys.add(forcedCaptureKey);
|
|
8008
|
-
if (nonArrayCaptureKeys.size > 0) {
|
|
8009
|
-
context.report({
|
|
8010
|
-
node: depsArgument,
|
|
8011
|
-
message: buildNonArrayDepsMessage(hookName)
|
|
8012
|
-
});
|
|
8013
|
-
context.report({
|
|
8014
|
-
node: depsArgument,
|
|
8015
|
-
message: buildMissingDepMessage(hookName, [...nonArrayCaptureKeys].join(", "))
|
|
8016
|
-
});
|
|
8017
|
-
}
|
|
8018
|
-
return;
|
|
8019
8091
|
}
|
|
8020
8092
|
if (!isNodeOfType(depsArgument, "ArrayExpression")) {
|
|
8021
8093
|
context.report({
|
|
@@ -8034,11 +8106,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8034
8106
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
8035
8107
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
8036
8108
|
if (!element) return false;
|
|
8037
|
-
return isLiteralOrEmptyTemplate(unwrapExpression$
|
|
8109
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$3(element));
|
|
8038
8110
|
});
|
|
8039
8111
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
8040
8112
|
if (!element) return false;
|
|
8041
|
-
return isNonStringLiteral(unwrapExpression$
|
|
8113
|
+
return isNonStringLiteral(unwrapExpression$3(element));
|
|
8042
8114
|
});
|
|
8043
8115
|
if (hasNonStringLiteralDep) context.report({
|
|
8044
8116
|
node: depsArgument,
|
|
@@ -8059,7 +8131,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8059
8131
|
});
|
|
8060
8132
|
continue;
|
|
8061
8133
|
}
|
|
8062
|
-
const stripped = unwrapExpression$
|
|
8134
|
+
const stripped = unwrapExpression$3(elementNode);
|
|
8063
8135
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
8064
8136
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
8065
8137
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -8201,7 +8273,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
8201
8273
|
if (missingCaptureKeys.length > 0 && isOuterFunctionScopeDep(reportNode, callbackToAnalyze ?? callbackArgument, context.scopes)) continue;
|
|
8202
8274
|
const rootSymbol = getRootSymbol(reportNode, context.scopes);
|
|
8203
8275
|
if (rootSymbol && missingCaptureKeys.length > 0 && isRecursiveInitializerCapture(rootSymbol, callbackToAnalyze ?? callbackArgument)) continue;
|
|
8204
|
-
if (isNodeOfType(unwrapExpression$
|
|
8276
|
+
if (isNodeOfType(unwrapExpression$3(reportNode), "CallExpression")) {
|
|
8205
8277
|
context.report({
|
|
8206
8278
|
node: reportNode,
|
|
8207
8279
|
message: buildComplexDepMessage(hookName)
|
|
@@ -8614,7 +8686,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
8614
8686
|
const resolveSettings$40 = (settings) => {
|
|
8615
8687
|
const reactDoctor = settings?.["react-doctor"];
|
|
8616
8688
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
8617
|
-
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
8689
|
+
return { headingTags: new Set([...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []]) };
|
|
8618
8690
|
};
|
|
8619
8691
|
const headingHasContent = defineRule({
|
|
8620
8692
|
id: "heading-has-content",
|
|
@@ -8627,7 +8699,7 @@ const headingHasContent = defineRule({
|
|
|
8627
8699
|
const settings = resolveSettings$40(context.settings);
|
|
8628
8700
|
return { JSXOpeningElement(node) {
|
|
8629
8701
|
const elementType = getElementType(node, context.settings);
|
|
8630
|
-
if (!settings.headingTags.
|
|
8702
|
+
if (!settings.headingTags.has(elementType)) return;
|
|
8631
8703
|
const parent = node.parent;
|
|
8632
8704
|
if (parent && isNodeOfType(parent, "JSXElement")) {
|
|
8633
8705
|
if (objectHasAccessibleChild(parent, context.settings)) return;
|
|
@@ -9225,9 +9297,9 @@ const resolveSettings$37 = (settings) => {
|
|
|
9225
9297
|
};
|
|
9226
9298
|
};
|
|
9227
9299
|
const isWordBoundary = (text, start, end) => {
|
|
9228
|
-
const
|
|
9229
|
-
const startsBoundary = start === 0 || !
|
|
9230
|
-
const endsBoundary = end === text.length || !
|
|
9300
|
+
const isWordCharacter = (charCode) => charCode >= 48 && charCode <= 57 || charCode >= 65 && charCode <= 90 || charCode >= 97 && charCode <= 122 || charCode === 45 || charCode === 95;
|
|
9301
|
+
const startsBoundary = start === 0 || !isWordCharacter(text.charCodeAt(start - 1));
|
|
9302
|
+
const endsBoundary = end === text.length || !isWordCharacter(text.charCodeAt(end));
|
|
9231
9303
|
return startsBoundary && endsBoundary;
|
|
9232
9304
|
};
|
|
9233
9305
|
const containsRedundantWord = (altText, words) => {
|
|
@@ -9266,10 +9338,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9266
9338
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9267
9339
|
const settings = resolveSettings$37(context.settings);
|
|
9268
9340
|
return { JSXOpeningElement(node) {
|
|
9269
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9270
9341
|
const tag = getElementType(node, context.settings);
|
|
9271
9342
|
if (!settings.components.includes(tag)) return;
|
|
9272
9343
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9344
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9273
9345
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9274
9346
|
if (!altAttribute) return;
|
|
9275
9347
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9324,6 +9396,7 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9324
9396
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9325
9397
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9326
9398
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9399
|
+
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9327
9400
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9328
9401
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9329
9402
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9353,6 +9426,7 @@ const insecureCryptoRisk = defineRule({
|
|
|
9353
9426
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9354
9427
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9355
9428
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9429
|
+
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9356
9430
|
const content = getScannableContent(file);
|
|
9357
9431
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9358
9432
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9531,6 +9605,7 @@ const KEYBOARD_EVENT_HANDLERS = [
|
|
|
9531
9605
|
"onKeyUp"
|
|
9532
9606
|
];
|
|
9533
9607
|
const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
|
|
9608
|
+
const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
|
|
9534
9609
|
//#endregion
|
|
9535
9610
|
//#region src/plugin/utils/is-disabled-element.ts
|
|
9536
9611
|
const isDisabledElement = (openingElement) => {
|
|
@@ -9585,15 +9660,25 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9585
9660
|
const settings = resolveSettings$36(context.settings);
|
|
9586
9661
|
const tabbableSet = new Set(settings.tabbable);
|
|
9587
9662
|
return { JSXOpeningElement(node) {
|
|
9663
|
+
if (node.attributes.length === 0) return;
|
|
9588
9664
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9589
9665
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9590
9666
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9667
|
+
if (!role) return;
|
|
9668
|
+
let hasInteractiveHandler = false;
|
|
9669
|
+
for (const attribute of node.attributes) {
|
|
9670
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
9671
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
9672
|
+
if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
9673
|
+
hasInteractiveHandler = true;
|
|
9674
|
+
break;
|
|
9675
|
+
}
|
|
9676
|
+
}
|
|
9677
|
+
if (!hasInteractiveHandler) return;
|
|
9591
9678
|
const elementType = getElementType(node, context.settings);
|
|
9592
9679
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9593
|
-
|
|
9680
|
+
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9594
9681
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9595
|
-
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9596
|
-
if (!role) return;
|
|
9597
9682
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9598
9683
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9599
9684
|
context.report({
|
|
@@ -9611,7 +9696,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
9611
9696
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
9612
9697
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
9613
9698
|
};
|
|
9614
|
-
const isFunctionExpressionLike$
|
|
9699
|
+
const isFunctionExpressionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
9615
9700
|
const getFirstParameterName = (fn) => {
|
|
9616
9701
|
const parameters = fn.params ?? [];
|
|
9617
9702
|
if (parameters.length !== 1) return null;
|
|
@@ -9744,7 +9829,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
9744
9829
|
const args = node.arguments ?? [];
|
|
9745
9830
|
if (args.length === 0) return;
|
|
9746
9831
|
const reader = args[0];
|
|
9747
|
-
if (!isFunctionExpressionLike$
|
|
9832
|
+
if (!isFunctionExpressionLike$2(reader)) return;
|
|
9748
9833
|
const getParameterName = getFirstParameterName(reader);
|
|
9749
9834
|
if (!getParameterName) return;
|
|
9750
9835
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -9842,14 +9927,14 @@ const getHandlerNamedBindingName = (functionNode) => {
|
|
|
9842
9927
|
const containingFunctionIsComponentOrHook = (functionNode) => {
|
|
9843
9928
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) {
|
|
9844
9929
|
const declaredName = functionNode.id.name;
|
|
9845
|
-
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN.test(declaredName);
|
|
9930
|
+
return COMPONENT_NAME_PATTERN.test(declaredName) || HOOK_NAME_PATTERN$1.test(declaredName);
|
|
9846
9931
|
}
|
|
9847
9932
|
let cursor = functionNode.parent ?? null;
|
|
9848
9933
|
while (cursor && isNodeOfType(cursor, "CallExpression")) cursor = cursor.parent ?? null;
|
|
9849
9934
|
if (!cursor) return false;
|
|
9850
9935
|
if (!isNodeOfType(cursor, "VariableDeclarator")) return false;
|
|
9851
9936
|
if (!isNodeOfType(cursor.id, "Identifier")) return false;
|
|
9852
|
-
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN.test(cursor.id.name);
|
|
9937
|
+
return COMPONENT_NAME_PATTERN.test(cursor.id.name) || HOOK_NAME_PATTERN$1.test(cursor.id.name);
|
|
9853
9938
|
};
|
|
9854
9939
|
const isBindingInvokedOnRenderPath = (root, bindingName) => {
|
|
9855
9940
|
let didFindRenderPathInvocation = false;
|
|
@@ -10232,8 +10317,6 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10232
10317
|
walkAst(loopBody, (child) => {
|
|
10233
10318
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10234
10319
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10235
|
-
});
|
|
10236
|
-
walkAst(loopBody, (child) => {
|
|
10237
10320
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10238
10321
|
if (child.computed) return;
|
|
10239
10322
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10693,7 +10776,6 @@ const jsHoistIntl = defineRule({
|
|
|
10693
10776
|
recommendation: "Move `new Intl.NumberFormat(...)` to the top of the file or wrap it in `useMemo`. Building one is slow, so don't redo it on every call",
|
|
10694
10777
|
create: (context) => ({ NewExpression(node) {
|
|
10695
10778
|
if (!isIntlNewExpression(node)) return;
|
|
10696
|
-
if (isInsideCacheMemo(node)) return;
|
|
10697
10779
|
let cursor = node.parent ?? null;
|
|
10698
10780
|
let inFunctionBody = false;
|
|
10699
10781
|
while (cursor) {
|
|
@@ -10710,6 +10792,7 @@ const jsHoistIntl = defineRule({
|
|
|
10710
10792
|
cursor = cursor.parent ?? null;
|
|
10711
10793
|
}
|
|
10712
10794
|
if (!inFunctionBody) return;
|
|
10795
|
+
if (isInsideCacheMemo(node)) return;
|
|
10713
10796
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10714
10797
|
context.report({
|
|
10715
10798
|
node,
|
|
@@ -10784,7 +10867,11 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10784
10867
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10785
10868
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10786
10869
|
};
|
|
10787
|
-
const
|
|
10870
|
+
const loopBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
10871
|
+
const getLoopBoundNames = (loop) => {
|
|
10872
|
+
const cached = loopBoundNamesCache.get(loop);
|
|
10873
|
+
if (cached) return cached;
|
|
10874
|
+
const names = /* @__PURE__ */ new Set();
|
|
10788
10875
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10789
10876
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10790
10877
|
});
|
|
@@ -10801,6 +10888,8 @@ const collectLoopBoundNames = (loop, names) => {
|
|
|
10801
10888
|
if (targetRoot) names.add(targetRoot);
|
|
10802
10889
|
}
|
|
10803
10890
|
});
|
|
10891
|
+
loopBoundNamesCache.set(loop, names);
|
|
10892
|
+
return names;
|
|
10804
10893
|
};
|
|
10805
10894
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10806
10895
|
let cursor = receiver;
|
|
@@ -10826,7 +10915,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10826
10915
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10827
10916
|
let ancestor = node.parent;
|
|
10828
10917
|
while (ancestor) {
|
|
10829
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10918
|
+
if (LOOP_TYPES.includes(ancestor.type)) for (const name of getLoopBoundNames(ancestor)) loopBoundNames.add(name);
|
|
10830
10919
|
ancestor = ancestor.parent;
|
|
10831
10920
|
}
|
|
10832
10921
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -14434,14 +14523,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14434
14523
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14435
14524
|
const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
14436
14525
|
const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
|
|
14437
|
-
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14526
|
+
const DEFAULT_CONTROL_COMPONENTS = new Set([
|
|
14438
14527
|
"input",
|
|
14439
14528
|
"meter",
|
|
14440
14529
|
"output",
|
|
14441
14530
|
"progress",
|
|
14442
14531
|
"select",
|
|
14443
14532
|
"textarea"
|
|
14444
|
-
];
|
|
14533
|
+
]);
|
|
14445
14534
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14446
14535
|
"alt",
|
|
14447
14536
|
"aria-label",
|
|
@@ -14453,8 +14542,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14453
14542
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14454
14543
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14455
14544
|
return {
|
|
14456
|
-
labelComponents: ["label", ...ruleSettings.labelComponents ?? []]
|
|
14457
|
-
labelAttributes:
|
|
14545
|
+
labelComponents: new Set(["label", ...ruleSettings.labelComponents ?? []]),
|
|
14546
|
+
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14458
14547
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14459
14548
|
assert: ruleSettings.assert ?? "either",
|
|
14460
14549
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14462,7 +14551,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14462
14551
|
};
|
|
14463
14552
|
};
|
|
14464
14553
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14465
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14554
|
+
if (DEFAULT_CONTROL_COMPONENTS.has(tagName)) return true;
|
|
14466
14555
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14467
14556
|
};
|
|
14468
14557
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14488,7 +14577,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14488
14577
|
const attributeName = attribute.name;
|
|
14489
14578
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14490
14579
|
const propName = getJsxAttributeName(attributeName);
|
|
14491
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14580
|
+
if (!propName || !searchContext.labelAttributes.has(propName)) continue;
|
|
14492
14581
|
const attributeValue = attribute.value;
|
|
14493
14582
|
if (!attributeValue) continue;
|
|
14494
14583
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14510,7 +14599,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14510
14599
|
const attributeName = attribute.name;
|
|
14511
14600
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14512
14601
|
const propName = getJsxAttributeName(attributeName);
|
|
14513
|
-
if (propName && searchContext.labelAttributes.
|
|
14602
|
+
if (propName && searchContext.labelAttributes.has(propName)) return true;
|
|
14514
14603
|
}
|
|
14515
14604
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14516
14605
|
return false;
|
|
@@ -14533,7 +14622,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14533
14622
|
if (isTestlikeFile) return;
|
|
14534
14623
|
const opening = node.openingElement;
|
|
14535
14624
|
const tagName = getElementType(opening, context.settings);
|
|
14536
|
-
if (!settings.labelComponents.
|
|
14625
|
+
if (!settings.labelComponents.has(tagName)) return;
|
|
14537
14626
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14538
14627
|
const searchContext = {
|
|
14539
14628
|
depth: settings.depth,
|
|
@@ -15122,9 +15211,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15122
15211
|
const reactDoctor = settings?.["react-doctor"];
|
|
15123
15212
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15124
15213
|
return {
|
|
15125
|
-
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15126
|
-
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15127
|
-
track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
|
|
15214
|
+
audio: new Set([...DEFAULT_AUDIO, ...ruleSettings.audio ?? []]),
|
|
15215
|
+
video: new Set([...DEFAULT_VIDEO, ...ruleSettings.video ?? []]),
|
|
15216
|
+
track: new Set([...DEFAULT_TRACK, ...ruleSettings.track ?? []])
|
|
15128
15217
|
};
|
|
15129
15218
|
};
|
|
15130
15219
|
const evaluateMuted = (attribute) => {
|
|
@@ -15153,7 +15242,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15153
15242
|
let rendersCaptionTrack = false;
|
|
15154
15243
|
walkAst(expression, (inner) => {
|
|
15155
15244
|
if (rendersCaptionTrack) return false;
|
|
15156
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15245
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.has(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15157
15246
|
rendersCaptionTrack = true;
|
|
15158
15247
|
return false;
|
|
15159
15248
|
}
|
|
@@ -15171,7 +15260,7 @@ const mediaHasCaption = defineRule({
|
|
|
15171
15260
|
const settings = resolveSettings$23(context.settings);
|
|
15172
15261
|
return { JSXOpeningElement(node) {
|
|
15173
15262
|
const tag = getElementType(node, context.settings);
|
|
15174
|
-
if (!(settings.audio.
|
|
15263
|
+
if (!(settings.audio.has(tag) || settings.video.has(tag))) return;
|
|
15175
15264
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15176
15265
|
const parent = node.parent;
|
|
15177
15266
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15186,7 +15275,7 @@ const mediaHasCaption = defineRule({
|
|
|
15186
15275
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15187
15276
|
const opening = child.openingElement;
|
|
15188
15277
|
const childTag = getElementType(opening, context.settings);
|
|
15189
|
-
if (!settings.track.
|
|
15278
|
+
if (!settings.track.has(childTag)) return false;
|
|
15190
15279
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15191
15280
|
if (!kindAttribute) return false;
|
|
15192
15281
|
let kindValue = kindAttribute.value;
|
|
@@ -15497,16 +15586,30 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15497
15586
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15498
15587
|
const NAMED_EXPORT_DECLARATION_PATTERN = /^\s*export\s+(?:declare\s+)?(?:(?:async\s+)?function|(?:abstract\s+)?class|const|let|var|enum|interface|type)\s+([\w$]+)/gm;
|
|
15499
15588
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15500
|
-
const
|
|
15589
|
+
const collectSourceTextExportNames = (sourceText) => {
|
|
15501
15590
|
const strippedSource = stripJsComments(sourceText);
|
|
15502
|
-
|
|
15503
|
-
|
|
15504
|
-
for (const match of strippedSource.matchAll(
|
|
15505
|
-
|
|
15591
|
+
const exportedNames = /* @__PURE__ */ new Set();
|
|
15592
|
+
if (DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) exportedNames.add("default");
|
|
15593
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1]) exportedNames.add(match[1]);
|
|
15594
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) {
|
|
15595
|
+
const specifiersText = match[1] ?? "";
|
|
15596
|
+
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15597
|
+
}
|
|
15598
|
+
return exportedNames;
|
|
15506
15599
|
};
|
|
15600
|
+
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15507
15601
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15508
15602
|
try {
|
|
15509
|
-
|
|
15603
|
+
const fileStat = fs.statSync(filePath);
|
|
15604
|
+
const cached = exportNamesCache.get(filePath);
|
|
15605
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15606
|
+
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15607
|
+
exportNamesCache.set(filePath, {
|
|
15608
|
+
mtimeMs: fileStat.mtimeMs,
|
|
15609
|
+
size: fileStat.size,
|
|
15610
|
+
exportedNames
|
|
15611
|
+
});
|
|
15612
|
+
return exportedNames.has(exportedName);
|
|
15510
15613
|
} catch {
|
|
15511
15614
|
return false;
|
|
15512
15615
|
}
|
|
@@ -15545,6 +15648,7 @@ const nextjsMissingMetadata = defineRule({
|
|
|
15545
15648
|
const filename = normalizeFilename(context.filename ?? "");
|
|
15546
15649
|
if (!PAGE_FILE_PATTERN.test(filename)) return;
|
|
15547
15650
|
if (INTERNAL_PAGE_PATH_PATTERN.test(filename)) return;
|
|
15651
|
+
if ((programNode.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client")) return;
|
|
15548
15652
|
if (programNode.body?.some((statement) => {
|
|
15549
15653
|
if (!isNodeOfType(statement, "ExportNamedDeclaration")) return false;
|
|
15550
15654
|
const declaration = statement.declaration;
|
|
@@ -15651,8 +15755,12 @@ const containsFetchCall = (node, options) => {
|
|
|
15651
15755
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15652
15756
|
let didFindFetchCall = false;
|
|
15653
15757
|
walkAst(node, (child) => {
|
|
15758
|
+
if (didFindFetchCall) return false;
|
|
15654
15759
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15655
|
-
if (isFetchCall$1(child))
|
|
15760
|
+
if (isFetchCall$1(child)) {
|
|
15761
|
+
didFindFetchCall = true;
|
|
15762
|
+
return false;
|
|
15763
|
+
}
|
|
15656
15764
|
});
|
|
15657
15765
|
return didFindFetchCall;
|
|
15658
15766
|
};
|
|
@@ -16342,16 +16450,18 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16342
16450
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16343
16451
|
create: (context) => {
|
|
16344
16452
|
let resolveBinding = () => null;
|
|
16453
|
+
let isRouteHandlerFile = false;
|
|
16454
|
+
let mutatingSegment = null;
|
|
16345
16455
|
return {
|
|
16346
16456
|
Program(node) {
|
|
16347
16457
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16458
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16459
|
+
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16460
|
+
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16348
16461
|
},
|
|
16349
16462
|
ExportNamedDeclaration(node) {
|
|
16350
|
-
|
|
16351
|
-
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16352
|
-
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16463
|
+
if (!isRouteHandlerFile) return;
|
|
16353
16464
|
if (!isExportedGetHandler(node)) return;
|
|
16354
|
-
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16355
16465
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16356
16466
|
for (const handlerBody of handlerBodies) {
|
|
16357
16467
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17268,14 +17378,38 @@ const resolvesToRefFactoryCall = (identifier) => {
|
|
|
17268
17378
|
});
|
|
17269
17379
|
return found;
|
|
17270
17380
|
};
|
|
17271
|
-
const
|
|
17381
|
+
const unwrapExpression$2 = (node) => {
|
|
17382
|
+
if (!node) return null;
|
|
17383
|
+
if (isNodeOfType(node, "ChainExpression")) return unwrapExpression$2(node.expression);
|
|
17384
|
+
if (isNodeOfType(node, "TSNonNullExpression")) return unwrapExpression$2(node.expression);
|
|
17385
|
+
return node;
|
|
17386
|
+
};
|
|
17387
|
+
const resolvesToRefCurrentAlias = (identifier, visitedAliasNames) => {
|
|
17388
|
+
if (visitedAliasNames.has(identifier.name)) return false;
|
|
17389
|
+
const root = findProgramRoot(identifier);
|
|
17390
|
+
if (!root) return false;
|
|
17391
|
+
const nextVisited = new Set([...visitedAliasNames, identifier.name]);
|
|
17392
|
+
let found = false;
|
|
17393
|
+
walkAst(root, (child) => {
|
|
17394
|
+
if (found) return false;
|
|
17395
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.id.name === identifier.name) {
|
|
17396
|
+
const init = unwrapExpression$2(child.init);
|
|
17397
|
+
if (init && isNodeOfType(init, "MemberExpression") && isNodeOfType(init.property, "Identifier") && init.property.name === "current" && isRefLikeReceiver(init.object, nextVisited)) {
|
|
17398
|
+
found = true;
|
|
17399
|
+
return false;
|
|
17400
|
+
}
|
|
17401
|
+
}
|
|
17402
|
+
});
|
|
17403
|
+
return found;
|
|
17404
|
+
};
|
|
17405
|
+
const isRefLikeReceiver = (receiver, visitedAliasNames = /* @__PURE__ */ new Set()) => {
|
|
17272
17406
|
if (!receiver) return false;
|
|
17273
|
-
if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17274
|
-
if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression);
|
|
17275
|
-
if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver);
|
|
17407
|
+
if (isNodeOfType(receiver, "ChainExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
|
|
17408
|
+
if (isNodeOfType(receiver, "TSNonNullExpression")) return isRefLikeReceiver(receiver.expression, visitedAliasNames);
|
|
17409
|
+
if (isNodeOfType(receiver, "Identifier")) return hasRefLikeName(receiver.name) || resolvesToRefFactoryCall(receiver) || resolvesToRefCurrentAlias(receiver, visitedAliasNames);
|
|
17276
17410
|
if (isNodeOfType(receiver, "MemberExpression") && isNodeOfType(receiver.property, "Identifier")) {
|
|
17277
17411
|
if (hasRefLikeName(receiver.property.name)) return true;
|
|
17278
|
-
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object);
|
|
17412
|
+
if (receiver.property.name === "current") return isRefLikeReceiver(receiver.object, visitedAliasNames);
|
|
17279
17413
|
}
|
|
17280
17414
|
return false;
|
|
17281
17415
|
};
|
|
@@ -17383,8 +17517,15 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17383
17517
|
programToAnalysis.set(programNode, analysis);
|
|
17384
17518
|
return analysis;
|
|
17385
17519
|
};
|
|
17520
|
+
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17386
17521
|
const getScopeForNode = (node, manager) => {
|
|
17387
17522
|
if (!node.range) return null;
|
|
17523
|
+
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17524
|
+
if (!scopeByNode) {
|
|
17525
|
+
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17526
|
+
scopeByNodeCache.set(manager, scopeByNode);
|
|
17527
|
+
}
|
|
17528
|
+
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17388
17529
|
let bestScope = null;
|
|
17389
17530
|
let bestSize = Infinity;
|
|
17390
17531
|
for (const scope of manager.scopes) {
|
|
@@ -17397,11 +17538,25 @@ const getScopeForNode = (node, manager) => {
|
|
|
17397
17538
|
bestScope = scope;
|
|
17398
17539
|
}
|
|
17399
17540
|
}
|
|
17541
|
+
scopeByNode.set(node, bestScope);
|
|
17400
17542
|
return bestScope;
|
|
17401
17543
|
};
|
|
17402
17544
|
//#endregion
|
|
17403
17545
|
//#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
|
|
17404
17546
|
const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
17547
|
+
const HOOK_NAME_PATTERN = /^use[A-Z0-9]/;
|
|
17548
|
+
const isInsideCallbackArgumentOf = (identifier, initializer) => {
|
|
17549
|
+
if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
|
|
17550
|
+
if (isNodeOfType(initializer, "CallExpression") && isNodeOfType(initializer.callee, "Identifier") && HOOK_NAME_PATTERN.test(initializer.callee.name)) return false;
|
|
17551
|
+
const callbackArguments = (initializer.arguments ?? []).filter((argument) => isFunctionLike$1(argument));
|
|
17552
|
+
if (callbackArguments.length === 0) return false;
|
|
17553
|
+
let node = identifier;
|
|
17554
|
+
while (node && node !== initializer) {
|
|
17555
|
+
if (callbackArguments.includes(node)) return true;
|
|
17556
|
+
node = node.parent;
|
|
17557
|
+
}
|
|
17558
|
+
return false;
|
|
17559
|
+
};
|
|
17405
17560
|
const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
17406
17561
|
if (visited.has(ref)) return;
|
|
17407
17562
|
const result = visit(ref);
|
|
@@ -17414,7 +17569,10 @@ const ascend = (analysis, ref, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17414
17569
|
const defNode = def.node;
|
|
17415
17570
|
const next = defNode.init ?? defNode.body;
|
|
17416
17571
|
if (!next) continue;
|
|
17417
|
-
for (const innerRef of getDownstreamRefs(analysis, next))
|
|
17572
|
+
for (const innerRef of getDownstreamRefs(analysis, next)) {
|
|
17573
|
+
if (isInsideCallbackArgumentOf(innerRef.identifier, next)) continue;
|
|
17574
|
+
ascend(analysis, innerRef, visit, visited);
|
|
17575
|
+
}
|
|
17418
17576
|
}
|
|
17419
17577
|
};
|
|
17420
17578
|
const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
@@ -17431,11 +17589,20 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17431
17589
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17432
17590
|
}
|
|
17433
17591
|
};
|
|
17592
|
+
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17434
17593
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17594
|
+
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17595
|
+
if (!upstreamByRef) {
|
|
17596
|
+
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17597
|
+
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17598
|
+
}
|
|
17599
|
+
const cached = upstreamByRef.get(ref);
|
|
17600
|
+
if (cached) return cached;
|
|
17435
17601
|
const refs = [];
|
|
17436
17602
|
ascend(analysis, ref, (upRef) => {
|
|
17437
17603
|
refs.push(upRef);
|
|
17438
17604
|
});
|
|
17605
|
+
upstreamByRef.set(ref, refs);
|
|
17439
17606
|
return refs;
|
|
17440
17607
|
};
|
|
17441
17608
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17445,11 +17612,24 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17445
17612
|
});
|
|
17446
17613
|
return nodes;
|
|
17447
17614
|
};
|
|
17615
|
+
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17448
17616
|
const getRef = (analysis, identifier) => {
|
|
17617
|
+
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17618
|
+
if (!refByIdentifier) {
|
|
17619
|
+
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17620
|
+
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17621
|
+
}
|
|
17622
|
+
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17623
|
+
let resolvedReference = null;
|
|
17449
17624
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17450
|
-
if (
|
|
17451
|
-
|
|
17452
|
-
|
|
17625
|
+
if (scope) {
|
|
17626
|
+
for (const reference of scope.references) if (reference.identifier === identifier) {
|
|
17627
|
+
resolvedReference = reference;
|
|
17628
|
+
break;
|
|
17629
|
+
}
|
|
17630
|
+
}
|
|
17631
|
+
refByIdentifier.set(identifier, resolvedReference);
|
|
17632
|
+
return resolvedReference;
|
|
17453
17633
|
};
|
|
17454
17634
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17455
17635
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17524,22 +17704,6 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17524
17704
|
};
|
|
17525
17705
|
//#endregion
|
|
17526
17706
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17527
|
-
const getOuterScopeContaining = (analysis, node) => {
|
|
17528
|
-
if (!node.range) return null;
|
|
17529
|
-
let best = null;
|
|
17530
|
-
let bestSize = Infinity;
|
|
17531
|
-
for (const scope of analysis.scopeManager.scopes) {
|
|
17532
|
-
const block = scope.block;
|
|
17533
|
-
if (!block?.range) continue;
|
|
17534
|
-
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17535
|
-
const size = block.range[1] - block.range[0];
|
|
17536
|
-
if (size <= bestSize) {
|
|
17537
|
-
bestSize = size;
|
|
17538
|
-
best = scope;
|
|
17539
|
-
}
|
|
17540
|
-
}
|
|
17541
|
-
return best;
|
|
17542
|
-
};
|
|
17543
17707
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17544
17708
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17545
17709
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17570,7 +17734,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17570
17734
|
const isWrappedSeparately = () => {
|
|
17571
17735
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17572
17736
|
const bindingName = node.id.name;
|
|
17573
|
-
const containingScope =
|
|
17737
|
+
const containingScope = getScopeForNode(node, analysis.scopeManager);
|
|
17574
17738
|
if (!containingScope) return false;
|
|
17575
17739
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17576
17740
|
if (!variable) return false;
|
|
@@ -17857,7 +18021,8 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
17857
18021
|
if (isNodeOfType(value, "Literal") && value.value !== "true") return;
|
|
17858
18022
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
17859
18023
|
const expression = value.expression;
|
|
17860
|
-
if (isNodeOfType(expression, "Literal")
|
|
18024
|
+
if (!isNodeOfType(expression, "Literal")) return;
|
|
18025
|
+
if (expression.value !== true && expression.value !== "true") return;
|
|
17861
18026
|
}
|
|
17862
18027
|
}
|
|
17863
18028
|
const tag = getElementType(node, context.settings);
|
|
@@ -18112,10 +18277,30 @@ const isArrayFromCall = (node) => {
|
|
|
18112
18277
|
*
|
|
18113
18278
|
* Used both for `<receiver>.map(...)` and for `Array.from(<length>, fn)`.
|
|
18114
18279
|
*/
|
|
18115
|
-
const
|
|
18280
|
+
const isBindingReassigned = (referenceNode, bindingName) => {
|
|
18281
|
+
const programRoot = findProgramRoot(referenceNode);
|
|
18282
|
+
if (!programRoot) return false;
|
|
18283
|
+
let didFindReassignment = false;
|
|
18284
|
+
walkAst(programRoot, (child) => {
|
|
18285
|
+
if (didFindReassignment) return false;
|
|
18286
|
+
if (isNodeOfType(child, "AssignmentExpression") && isNodeOfType(child.left, "Identifier") && child.left.name === bindingName) {
|
|
18287
|
+
didFindReassignment = true;
|
|
18288
|
+
return false;
|
|
18289
|
+
}
|
|
18290
|
+
});
|
|
18291
|
+
return didFindReassignment;
|
|
18292
|
+
};
|
|
18293
|
+
const isStaticPlaceholderReceiver = (receiver, depth = 0) => {
|
|
18116
18294
|
if (isArrayFromCall(receiver)) return true;
|
|
18117
18295
|
if (isArrayConstructorCallWithNumericLength(receiver)) return true;
|
|
18118
18296
|
if (isAllLiteralArrayExpression(receiver)) return true;
|
|
18297
|
+
if (isNodeOfType(receiver, "Identifier")) {
|
|
18298
|
+
if (depth >= TYPE_RESOLUTION_DEPTH_LIMIT) return false;
|
|
18299
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
18300
|
+
if (!binding?.initializer) return false;
|
|
18301
|
+
if (isBindingReassigned(receiver, receiver.name)) return false;
|
|
18302
|
+
return isStaticPlaceholderReceiver(binding.initializer, depth + 1);
|
|
18303
|
+
}
|
|
18119
18304
|
if (isNodeOfType(receiver, "CallExpression")) {
|
|
18120
18305
|
const callee = receiver.callee;
|
|
18121
18306
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && callee.property.name === "fill" && isArrayConstructorCallWithNumericLength(callee.object)) return true;
|
|
@@ -18141,6 +18326,7 @@ const isArrayFromLengthObjectCall = (node) => {
|
|
|
18141
18326
|
if (!(isNodeOfType(key, "Identifier") && key.name === "length" || isNodeOfType(key, "Literal") && key.value === "length")) continue;
|
|
18142
18327
|
if (isNumericLiteralOrUndefined(prop.value)) return true;
|
|
18143
18328
|
if (isNodeOfType(prop.value, "Identifier")) return true;
|
|
18329
|
+
if (isNodeOfType(prop.value, "MemberExpression") && isNodeOfType(prop.value.property, "Identifier") && prop.value.property.name === "length") return true;
|
|
18144
18330
|
}
|
|
18145
18331
|
return false;
|
|
18146
18332
|
};
|
|
@@ -18249,10 +18435,11 @@ const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node,
|
|
|
18249
18435
|
/**
|
|
18250
18436
|
* Walk up from a JSXAttribute node looking for the enclosing iterator
|
|
18251
18437
|
* callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
|
|
18252
|
-
* and return the
|
|
18253
|
-
*
|
|
18438
|
+
* and return the names bound by the first parameter — `item` in
|
|
18439
|
+
* `arr.map((item, index) => …)`, or every destructured field in
|
|
18440
|
+
* `arr.map(({ id, label }, index) => …)`.
|
|
18254
18441
|
*/
|
|
18255
|
-
const
|
|
18442
|
+
const findIteratorItemNames = (node) => {
|
|
18256
18443
|
let current = node;
|
|
18257
18444
|
while (current.parent) {
|
|
18258
18445
|
const parent = current.parent;
|
|
@@ -18262,18 +18449,20 @@ const findIteratorItemName$1 = (node) => {
|
|
|
18262
18449
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
18263
18450
|
if (isIteratorMethodCall || isArrayFromCallback) {
|
|
18264
18451
|
const first = (current.params ?? [])[0];
|
|
18265
|
-
if (first
|
|
18266
|
-
|
|
18452
|
+
if (!first) return null;
|
|
18453
|
+
const names = /* @__PURE__ */ new Set();
|
|
18454
|
+
collectPatternNames(first, names);
|
|
18455
|
+
return names.size > 0 ? names : null;
|
|
18267
18456
|
}
|
|
18268
18457
|
}
|
|
18269
18458
|
current = parent;
|
|
18270
18459
|
}
|
|
18271
18460
|
return null;
|
|
18272
18461
|
};
|
|
18273
|
-
const templateLiteralHasIteratorIdentity = (template,
|
|
18462
|
+
const templateLiteralHasIteratorIdentity = (template, itemNames) => {
|
|
18274
18463
|
for (const expression of template.expressions ?? []) {
|
|
18275
|
-
|
|
18276
|
-
if (
|
|
18464
|
+
const rootName = getRootIdentifierName(expression, { followCallChains: true });
|
|
18465
|
+
if (rootName !== null && itemNames.has(rootName)) return true;
|
|
18277
18466
|
}
|
|
18278
18467
|
return false;
|
|
18279
18468
|
};
|
|
@@ -18286,9 +18475,32 @@ const templateLiteralHasIteratorIdentity = (template, itemName) => {
|
|
|
18286
18475
|
const isCompositeKeyWithIteratorIdentity = (keyExpression, attributeNode) => {
|
|
18287
18476
|
if (!isNodeOfType(keyExpression, "TemplateLiteral")) return false;
|
|
18288
18477
|
if ((keyExpression.expressions ?? []).length < 2) return false;
|
|
18289
|
-
const
|
|
18290
|
-
if (!
|
|
18291
|
-
return templateLiteralHasIteratorIdentity(keyExpression,
|
|
18478
|
+
const itemNames = findIteratorItemNames(attributeNode);
|
|
18479
|
+
if (!itemNames) return false;
|
|
18480
|
+
return templateLiteralHasIteratorIdentity(keyExpression, itemNames);
|
|
18481
|
+
};
|
|
18482
|
+
const forLoopTestReadsDataLength = (test) => {
|
|
18483
|
+
let didFindLengthRead = false;
|
|
18484
|
+
walkAst(test, (child) => {
|
|
18485
|
+
if (didFindLengthRead) return false;
|
|
18486
|
+
if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.property, "Identifier") && child.property.name === "length") {
|
|
18487
|
+
didFindLengthRead = true;
|
|
18488
|
+
return false;
|
|
18489
|
+
}
|
|
18490
|
+
});
|
|
18491
|
+
return didFindLengthRead;
|
|
18492
|
+
};
|
|
18493
|
+
const isNumericForLoopCounter = (attributeNode, indexName) => {
|
|
18494
|
+
const binding = findVariableInitializer(attributeNode, indexName);
|
|
18495
|
+
if (!binding) return false;
|
|
18496
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
18497
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
18498
|
+
const declaration = declarator.parent;
|
|
18499
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
18500
|
+
const forStatement = declaration.parent;
|
|
18501
|
+
if (!forStatement || !isNodeOfType(forStatement, "ForStatement") || forStatement.init !== declaration || !declarator.init || !isNodeOfType(declarator.init, "Literal") || typeof declarator.init.value !== "number") return false;
|
|
18502
|
+
if (forStatement.test && forLoopTestReadsDataLength(forStatement.test)) return false;
|
|
18503
|
+
return true;
|
|
18292
18504
|
};
|
|
18293
18505
|
const noArrayIndexAsKey = defineRule({
|
|
18294
18506
|
id: "no-array-index-as-key",
|
|
@@ -18300,6 +18512,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
18300
18512
|
if (!node.value || !isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
18301
18513
|
const indexName = extractIndexName(node.value.expression);
|
|
18302
18514
|
if (!indexName) return;
|
|
18515
|
+
if (isNumericForLoopCounter(node, indexName)) return;
|
|
18303
18516
|
if (isInsideStaticPlaceholderMap(node)) return;
|
|
18304
18517
|
if (isInsideStringDerivedMap(node)) return;
|
|
18305
18518
|
if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
|
|
@@ -19435,8 +19648,8 @@ const CONTEXT_MODULES = [
|
|
|
19435
19648
|
];
|
|
19436
19649
|
const isCreateContextCallee = (callee) => {
|
|
19437
19650
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19438
|
-
|
|
19439
|
-
return
|
|
19651
|
+
const binding = getImportBindingForName(callee, callee.name);
|
|
19652
|
+
return binding !== null && binding.exportedName === "createContext" && CONTEXT_MODULES.includes(binding.source);
|
|
19440
19653
|
}
|
|
19441
19654
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19442
19655
|
const namespaceIdentifier = callee.object;
|
|
@@ -19446,8 +19659,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19446
19659
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19447
19660
|
const namespaceName = namespaceIdentifier.name;
|
|
19448
19661
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19449
|
-
|
|
19450
|
-
return
|
|
19662
|
+
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
19663
|
+
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
19451
19664
|
}
|
|
19452
19665
|
return false;
|
|
19453
19666
|
};
|
|
@@ -19849,38 +20062,44 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19849
20062
|
};
|
|
19850
20063
|
const parseColorToRgb = (value) => {
|
|
19851
20064
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19852
|
-
|
|
19853
|
-
|
|
19854
|
-
|
|
19855
|
-
|
|
19856
|
-
|
|
19857
|
-
|
|
19858
|
-
|
|
19859
|
-
|
|
19860
|
-
|
|
19861
|
-
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
19865
|
-
|
|
19866
|
-
|
|
19867
|
-
|
|
19868
|
-
|
|
19869
|
-
|
|
19870
|
-
|
|
19871
|
-
|
|
19872
|
-
|
|
19873
|
-
|
|
19874
|
-
|
|
19875
|
-
|
|
19876
|
-
|
|
19877
|
-
|
|
19878
|
-
|
|
19879
|
-
|
|
19880
|
-
|
|
19881
|
-
|
|
19882
|
-
|
|
19883
|
-
|
|
20065
|
+
if (trimmed.startsWith("#")) {
|
|
20066
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
20067
|
+
if (hex8Match) return {
|
|
20068
|
+
red: parseInt(hex8Match[1], 16),
|
|
20069
|
+
green: parseInt(hex8Match[2], 16),
|
|
20070
|
+
blue: parseInt(hex8Match[3], 16)
|
|
20071
|
+
};
|
|
20072
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
20073
|
+
if (hex6Match) return {
|
|
20074
|
+
red: parseInt(hex6Match[1], 16),
|
|
20075
|
+
green: parseInt(hex6Match[2], 16),
|
|
20076
|
+
blue: parseInt(hex6Match[3], 16)
|
|
20077
|
+
};
|
|
20078
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
20079
|
+
if (hex4Match) return {
|
|
20080
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
20081
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
20082
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
20083
|
+
};
|
|
20084
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
20085
|
+
if (hex3Match) return {
|
|
20086
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
20087
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
20088
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
20089
|
+
};
|
|
20090
|
+
}
|
|
20091
|
+
if (trimmed.includes("rgb")) {
|
|
20092
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
20093
|
+
if (rgbMatch) return {
|
|
20094
|
+
red: parseInt(rgbMatch[1], 10),
|
|
20095
|
+
green: parseInt(rgbMatch[2], 10),
|
|
20096
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
20097
|
+
};
|
|
20098
|
+
}
|
|
20099
|
+
if (trimmed.includes("hsl")) {
|
|
20100
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
20101
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
20102
|
+
}
|
|
19884
20103
|
return null;
|
|
19885
20104
|
};
|
|
19886
20105
|
//#endregion
|
|
@@ -19908,9 +20127,18 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19908
20127
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19909
20128
|
return null;
|
|
19910
20129
|
};
|
|
20130
|
+
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
20131
|
+
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
20132
|
+
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
20133
|
+
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19911
20134
|
const parseShadowLayerBlur = (layer) => {
|
|
19912
|
-
const
|
|
19913
|
-
|
|
20135
|
+
const withoutColors = layer.replace(RGB_FUNCTION_PATTERN, "").replace(HEX_COLOR_PATTERN, "");
|
|
20136
|
+
let tokenIndex = 0;
|
|
20137
|
+
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
20138
|
+
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
20139
|
+
tokenIndex += 1;
|
|
20140
|
+
}
|
|
20141
|
+
return 0;
|
|
19914
20142
|
};
|
|
19915
20143
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19916
20144
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -20030,7 +20258,10 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
20030
20258
|
};
|
|
20031
20259
|
//#endregion
|
|
20032
20260
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20261
|
+
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20033
20262
|
const collectComponentPropNames = (componentFunction) => {
|
|
20263
|
+
const cached = componentPropNamesCache.get(componentFunction);
|
|
20264
|
+
if (cached) return cached;
|
|
20034
20265
|
const propNames = /* @__PURE__ */ new Set();
|
|
20035
20266
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
20036
20267
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -20044,27 +20275,23 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
20044
20275
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
20045
20276
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
20046
20277
|
});
|
|
20278
|
+
componentPropNamesCache.set(componentFunction, propNames);
|
|
20047
20279
|
return propNames;
|
|
20048
20280
|
};
|
|
20049
|
-
const
|
|
20281
|
+
const ownScopeBoundNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20282
|
+
const getOwnScopeBoundNames = (functionNode) => {
|
|
20283
|
+
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20284
|
+
if (cached) return cached;
|
|
20050
20285
|
const boundNames = /* @__PURE__ */ new Set();
|
|
20051
20286
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
20052
|
-
if (boundNames.has(bindingName)) return true;
|
|
20053
|
-
let declaresName = false;
|
|
20054
20287
|
walkAst(functionNode, (child) => {
|
|
20055
|
-
if (declaresName) return false;
|
|
20056
20288
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
20057
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
20058
|
-
const declaratorNames = /* @__PURE__ */ new Set();
|
|
20059
|
-
collectPatternNames(child.id, declaratorNames);
|
|
20060
|
-
if (declaratorNames.has(bindingName)) {
|
|
20061
|
-
declaresName = true;
|
|
20062
|
-
return false;
|
|
20063
|
-
}
|
|
20064
|
-
}
|
|
20289
|
+
if (isNodeOfType(child, "VariableDeclarator")) collectPatternNames(child.id, boundNames);
|
|
20065
20290
|
});
|
|
20066
|
-
|
|
20291
|
+
ownScopeBoundNamesCache.set(functionNode, boundNames);
|
|
20292
|
+
return boundNames;
|
|
20067
20293
|
};
|
|
20294
|
+
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20068
20295
|
const isPropertyNamePosition = (identifier) => {
|
|
20069
20296
|
const parent = identifier.parent;
|
|
20070
20297
|
if (!parent) return false;
|
|
@@ -20487,36 +20714,28 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20487
20714
|
}
|
|
20488
20715
|
return hasNestedFunction;
|
|
20489
20716
|
};
|
|
20490
|
-
const
|
|
20491
|
-
|
|
20492
|
-
|
|
20493
|
-
|
|
20494
|
-
|
|
20495
|
-
|
|
20496
|
-
|
|
20497
|
-
if (isReseeded) return false;
|
|
20498
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20499
|
-
isReseeded = true;
|
|
20500
|
-
return false;
|
|
20501
|
-
}
|
|
20502
|
-
});
|
|
20503
|
-
return isReseeded;
|
|
20717
|
+
const isInRenderScope = (node, componentFunction) => {
|
|
20718
|
+
let cursor = node.parent ?? null;
|
|
20719
|
+
while (cursor && cursor !== componentFunction) {
|
|
20720
|
+
if (isFunctionLike$1(cursor)) return false;
|
|
20721
|
+
cursor = cursor.parent ?? null;
|
|
20722
|
+
}
|
|
20723
|
+
return true;
|
|
20504
20724
|
};
|
|
20505
|
-
const
|
|
20725
|
+
const isDraftReseedOrRenderAdjusted = (useStateCall, isPropName) => {
|
|
20506
20726
|
const setterName = getStateSetterName(useStateCall);
|
|
20507
20727
|
if (!setterName) return false;
|
|
20508
20728
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20509
20729
|
if (!componentFunction) return false;
|
|
20510
|
-
let
|
|
20730
|
+
let isExempt = false;
|
|
20511
20731
|
walkAst(componentFunction, (child) => {
|
|
20512
|
-
if (
|
|
20513
|
-
if (child
|
|
20514
|
-
|
|
20515
|
-
isAdjusted = true;
|
|
20732
|
+
if (isExempt) return false;
|
|
20733
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && (isHandlerShapedReseed(child, componentFunction) || isInRenderScope(child, componentFunction))) {
|
|
20734
|
+
isExempt = true;
|
|
20516
20735
|
return false;
|
|
20517
20736
|
}
|
|
20518
20737
|
});
|
|
20519
|
-
return
|
|
20738
|
+
return isExempt;
|
|
20520
20739
|
};
|
|
20521
20740
|
const noDerivedUseState = defineRule({
|
|
20522
20741
|
id: "no-derived-useState",
|
|
@@ -20533,8 +20752,7 @@ const noDerivedUseState = defineRule({
|
|
|
20533
20752
|
const initializer = node.arguments[0];
|
|
20534
20753
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20535
20754
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20536
|
-
if (
|
|
20537
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20755
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20538
20756
|
context.report({
|
|
20539
20757
|
node,
|
|
20540
20758
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20545,8 +20763,7 @@ const noDerivedUseState = defineRule({
|
|
|
20545
20763
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20546
20764
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20547
20765
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20548
|
-
if (
|
|
20549
|
-
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20766
|
+
if (isDraftReseedOrRenderAdjusted(node, propStackTracker.isPropName)) return;
|
|
20550
20767
|
context.report({
|
|
20551
20768
|
node,
|
|
20552
20769
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -20661,7 +20878,7 @@ const isStateMemberExpression = (node) => {
|
|
|
20661
20878
|
};
|
|
20662
20879
|
//#endregion
|
|
20663
20880
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
20664
|
-
const MESSAGE$24 = "
|
|
20881
|
+
const MESSAGE$24 = "Mutating `this.state` by hand never triggers a redraw on its own & a later setState can overwrite it, so use `this.setState` instead.";
|
|
20665
20882
|
const shouldIgnoreMutation = (node) => {
|
|
20666
20883
|
let isConstructor = false;
|
|
20667
20884
|
let isInsideCallExpression = false;
|
|
@@ -20755,6 +20972,18 @@ const initializerMarksPlainState = (initializerArgument) => {
|
|
|
20755
20972
|
}
|
|
20756
20973
|
return producesPlainStateValue(unwrapped);
|
|
20757
20974
|
};
|
|
20975
|
+
const collectCallbackRefSetterNames = (componentBody) => {
|
|
20976
|
+
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
20977
|
+
walkAst(componentBody, (node) => {
|
|
20978
|
+
if (!isNodeOfType(node, "JSXAttribute")) return;
|
|
20979
|
+
const attributeName = node.name;
|
|
20980
|
+
if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
|
|
20981
|
+
const expression = stripParenExpression(node.value.expression);
|
|
20982
|
+
if (isNodeOfType(expression, "Identifier")) callbackRefSetterNames.add(expression.name);
|
|
20983
|
+
}
|
|
20984
|
+
});
|
|
20985
|
+
return callbackRefSetterNames;
|
|
20986
|
+
};
|
|
20758
20987
|
const collectFunctionLocalBindings = (functionNode) => {
|
|
20759
20988
|
const localBindings = /* @__PURE__ */ new Set();
|
|
20760
20989
|
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
|
|
@@ -20797,8 +21026,10 @@ const noDirectStateMutation = defineRule({
|
|
|
20797
21026
|
const bindings = collectUseStateBindings(componentBody);
|
|
20798
21027
|
if (bindings.length === 0) return;
|
|
20799
21028
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
21029
|
+
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
20800
21030
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
20801
21031
|
for (const binding of bindings) {
|
|
21032
|
+
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
20802
21033
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
20803
21034
|
if (initializerMarksPlainState(binding.declarator.init.arguments?.[0])) plainObjectStateValueNames.add(binding.valueName);
|
|
20804
21035
|
}
|
|
@@ -20811,7 +21042,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20811
21042
|
if (currentlyShadowed.has(rootName)) return;
|
|
20812
21043
|
context.report({
|
|
20813
21044
|
node: child,
|
|
20814
|
-
message: `
|
|
21045
|
+
message: `React can't tell you changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20815
21046
|
});
|
|
20816
21047
|
return;
|
|
20817
21048
|
}
|
|
@@ -20827,7 +21058,7 @@ const noDirectStateMutation = defineRule({
|
|
|
20827
21058
|
if (currentlyShadowed.has(rootName)) return;
|
|
20828
21059
|
context.report({
|
|
20829
21060
|
node: child,
|
|
20830
|
-
message: `
|
|
21061
|
+
message: `React can't tell .${methodName}() changed "${rootName}" in place, so this update can be skipped or lost.`
|
|
20831
21062
|
});
|
|
20832
21063
|
}
|
|
20833
21064
|
});
|
|
@@ -22245,20 +22476,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22245
22476
|
return null;
|
|
22246
22477
|
};
|
|
22247
22478
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22479
|
+
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22480
|
+
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22481
|
+
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22482
|
+
walkAst(componentBody, (child) => {
|
|
22483
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22484
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22485
|
+
const setterName = child.callee.name;
|
|
22486
|
+
if (!setterNames.has(setterName)) return;
|
|
22487
|
+
settersWithAnyCall.add(setterName);
|
|
22488
|
+
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22489
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22490
|
+
});
|
|
22248
22491
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22249
|
-
for (const binding of useStateBindings)
|
|
22250
|
-
let didFindAnySetterCall = false;
|
|
22251
|
-
let areAllSetterCallsInHandlers = true;
|
|
22252
|
-
walkAst(componentBody, (child) => {
|
|
22253
|
-
if (!areAllSetterCallsInHandlers) return false;
|
|
22254
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22255
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22256
|
-
if (child.callee.name !== binding.setterName) return;
|
|
22257
|
-
didFindAnySetterCall = true;
|
|
22258
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22259
|
-
});
|
|
22260
|
-
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22261
|
-
}
|
|
22492
|
+
for (const binding of useStateBindings) if (settersWithAnyCall.has(binding.setterName) && !settersWithNonHandlerCall.has(binding.setterName)) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22262
22493
|
return handlerOnlyWriteStateNames;
|
|
22263
22494
|
};
|
|
22264
22495
|
const noEventTriggerState = defineRule({
|
|
@@ -22598,6 +22829,10 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22598
22829
|
const TEXT_COLOR_PATTERN = /^text-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22599
22830
|
const BG_COLOR_PATTERN = /^bg-(?:white|black|transparent|current|inherit|\[|(?:gray|slate|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-)/;
|
|
22600
22831
|
const splitVariantScope = (token) => {
|
|
22832
|
+
if (!token.includes(":")) return {
|
|
22833
|
+
scope: "",
|
|
22834
|
+
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22835
|
+
};
|
|
22601
22836
|
const segments = token.split(":");
|
|
22602
22837
|
const rawUtility = segments[segments.length - 1];
|
|
22603
22838
|
return {
|
|
@@ -22621,6 +22856,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22621
22856
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22622
22857
|
for (const token of classStr.split(/\s+/)) {
|
|
22623
22858
|
if (!token) continue;
|
|
22859
|
+
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22624
22860
|
const { scope, utility } = splitVariantScope(token);
|
|
22625
22861
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22626
22862
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -22691,11 +22927,16 @@ const NON_DETERMINISTIC_ID_GENERATOR_NAMES = new Set([
|
|
|
22691
22927
|
"ulid",
|
|
22692
22928
|
"createId"
|
|
22693
22929
|
]);
|
|
22930
|
+
const isZeroArgDateConstruction = (node) => isNodeOfType(node, "NewExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "Date" && (node.arguments?.length ?? 0) === 0;
|
|
22694
22931
|
const containsNonDeterministicSource = (root) => {
|
|
22695
22932
|
let found = false;
|
|
22696
22933
|
walkAst(root, (child) => {
|
|
22697
22934
|
if (found) return false;
|
|
22698
22935
|
if (isFunctionLike$1(child)) return false;
|
|
22936
|
+
if (isZeroArgDateConstruction(child)) {
|
|
22937
|
+
found = true;
|
|
22938
|
+
return false;
|
|
22939
|
+
}
|
|
22699
22940
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22700
22941
|
const callee = child.callee;
|
|
22701
22942
|
if (isNodeOfType(callee, "Identifier") && NON_DETERMINISTIC_ID_GENERATOR_NAMES.has(callee.name)) {
|
|
@@ -22756,6 +22997,53 @@ const argumentReadsPostMountMeasurement = (argument, effectFn, visitedLocalNames
|
|
|
22756
22997
|
});
|
|
22757
22998
|
return found;
|
|
22758
22999
|
};
|
|
23000
|
+
const isResourceLikeInitializer = (initializer) => {
|
|
23001
|
+
if (isNodeOfType(initializer, "AwaitExpression")) return isResourceLikeInitializer(initializer.argument);
|
|
23002
|
+
return isNodeOfType(initializer, "NewExpression") || isNodeOfType(initializer, "CallExpression");
|
|
23003
|
+
};
|
|
23004
|
+
const collectArgumentSourceLocalNames = (argument, effectFn, sourceLocalNames = /* @__PURE__ */ new Set()) => {
|
|
23005
|
+
walkAst(argument, (child) => {
|
|
23006
|
+
if (!isNodeOfType(child, "Identifier")) return;
|
|
23007
|
+
if (sourceLocalNames.has(child.name)) return;
|
|
23008
|
+
const localInitializer = findEffectLocalInitializer(effectFn, child.name);
|
|
23009
|
+
if (!localInitializer || !isResourceLikeInitializer(localInitializer)) return;
|
|
23010
|
+
sourceLocalNames.add(child.name);
|
|
23011
|
+
collectArgumentSourceLocalNames(localInitializer, effectFn, sourceLocalNames);
|
|
23012
|
+
});
|
|
23013
|
+
return sourceLocalNames;
|
|
23014
|
+
};
|
|
23015
|
+
const isFunctionExpressionLike$1 = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression");
|
|
23016
|
+
const findCleanupFunction = (effectFn) => {
|
|
23017
|
+
if (!isNodeOfType(effectFn, "ArrowFunctionExpression") && !isNodeOfType(effectFn, "FunctionExpression")) return null;
|
|
23018
|
+
const body = effectFn.body;
|
|
23019
|
+
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
23020
|
+
let cleanupFunction = null;
|
|
23021
|
+
walkAst(body, (child) => {
|
|
23022
|
+
if (cleanupFunction) return false;
|
|
23023
|
+
if (isNodeOfType(child, "ReturnStatement")) {
|
|
23024
|
+
if (child.argument && isFunctionExpressionLike$1(child.argument)) cleanupFunction = child.argument;
|
|
23025
|
+
return false;
|
|
23026
|
+
}
|
|
23027
|
+
if (child !== body && isFunctionExpressionLike$1(child)) return false;
|
|
23028
|
+
if (isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
23029
|
+
});
|
|
23030
|
+
return cleanupFunction;
|
|
23031
|
+
};
|
|
23032
|
+
const cleanupDisposesArgumentSource = (argument, effectFn) => {
|
|
23033
|
+
const cleanupFunction = findCleanupFunction(effectFn);
|
|
23034
|
+
if (!cleanupFunction) return false;
|
|
23035
|
+
const sourceLocalNames = collectArgumentSourceLocalNames(argument, effectFn);
|
|
23036
|
+
if (sourceLocalNames.size === 0) return false;
|
|
23037
|
+
let referencesSource = false;
|
|
23038
|
+
walkAst(cleanupFunction, (child) => {
|
|
23039
|
+
if (referencesSource) return false;
|
|
23040
|
+
if (isNodeOfType(child, "Identifier") && sourceLocalNames.has(child.name)) {
|
|
23041
|
+
referencesSource = true;
|
|
23042
|
+
return false;
|
|
23043
|
+
}
|
|
23044
|
+
});
|
|
23045
|
+
return referencesSource;
|
|
23046
|
+
};
|
|
22759
23047
|
const noInitializeState = defineRule({
|
|
22760
23048
|
id: "no-initialize-state",
|
|
22761
23049
|
title: "State initialized from a mount effect",
|
|
@@ -22778,6 +23066,7 @@ const noInitializeState = defineRule({
|
|
|
22778
23066
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
22779
23067
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && containsNonDeterministicSource(argument))) continue;
|
|
22780
23068
|
if (callExpr.arguments?.some((argument) => Boolean(argument) && argumentReadsPostMountMeasurement(argument, effectFn))) continue;
|
|
23069
|
+
if (callExpr.arguments?.some((argument) => Boolean(argument) && cleanupDisposesArgumentSource(argument, effectFn))) continue;
|
|
22781
23070
|
const useStateDecl = getUseStateDecl(analysis, ref);
|
|
22782
23071
|
if (!useStateDecl || !isNodeOfType(useStateDecl, "VariableDeclarator")) continue;
|
|
22783
23072
|
if (!isNodeOfType(useStateDecl.id, "ArrayPattern")) continue;
|
|
@@ -23350,6 +23639,8 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23350
23639
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23351
23640
|
});
|
|
23352
23641
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23642
|
+
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23643
|
+
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23353
23644
|
const noLongTransitionDuration = defineRule({
|
|
23354
23645
|
id: "no-long-transition-duration",
|
|
23355
23646
|
title: "Transition duration too long",
|
|
@@ -23373,11 +23664,10 @@ const noLongTransitionDuration = defineRule({
|
|
|
23373
23664
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23374
23665
|
let longestDurationPropertyMs = 0;
|
|
23375
23666
|
for (const segment of value.split(",")) {
|
|
23376
|
-
const
|
|
23377
|
-
|
|
23378
|
-
const
|
|
23379
|
-
|
|
23380
|
-
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23667
|
+
const durationMatch = segment.trim().match(DURATION_SEGMENT_PATTERN);
|
|
23668
|
+
if (!durationMatch) continue;
|
|
23669
|
+
const segmentDurationMs = durationMatch[2] === "ms" ? parseFloat(durationMatch[1]) : parseFloat(durationMatch[1]) * 1e3;
|
|
23670
|
+
longestDurationPropertyMs = Math.max(longestDurationPropertyMs, segmentDurationMs);
|
|
23381
23671
|
}
|
|
23382
23672
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23383
23673
|
}
|
|
@@ -23385,7 +23675,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23385
23675
|
let longestDurationMs = 0;
|
|
23386
23676
|
for (const segment of value.split(",")) {
|
|
23387
23677
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23388
|
-
const firstTimeMatch = segment.match(
|
|
23678
|
+
const firstTimeMatch = segment.match(FIRST_TIME_TOKEN_PATTERN);
|
|
23389
23679
|
if (!firstTimeMatch) continue;
|
|
23390
23680
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23391
23681
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24557,14 +24847,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24557
24847
|
out.hasOpaqueBranch = true;
|
|
24558
24848
|
};
|
|
24559
24849
|
const buildMessage$12 = (tag) => `Keyboard & screen reader users can't trigger this \`<${tag}>\` because it isn't interactive, so use a button or link or add an interactive role.`;
|
|
24560
|
-
const
|
|
24850
|
+
const INTERACTIVE_HANDLERS_LOWER = new Set([
|
|
24561
24851
|
"onClick",
|
|
24562
24852
|
"onMouseDown",
|
|
24563
24853
|
"onMouseUp",
|
|
24564
24854
|
"onKeyDown",
|
|
24565
24855
|
"onKeyPress",
|
|
24566
24856
|
"onKeyUp"
|
|
24567
|
-
];
|
|
24857
|
+
].map((handlerName) => handlerName.toLowerCase()));
|
|
24568
24858
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24569
24859
|
id: "no-noninteractive-element-interactions",
|
|
24570
24860
|
title: "Handler on non-interactive element",
|
|
@@ -24579,7 +24869,16 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24579
24869
|
const tag = getElementType(node, context.settings);
|
|
24580
24870
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24581
24871
|
if (tag === "label") return;
|
|
24582
|
-
|
|
24872
|
+
let hasHandler = false;
|
|
24873
|
+
for (const attribute of node.attributes) {
|
|
24874
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24875
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
24876
|
+
if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
24877
|
+
hasHandler = true;
|
|
24878
|
+
break;
|
|
24879
|
+
}
|
|
24880
|
+
}
|
|
24881
|
+
if (!hasHandler) return;
|
|
24583
24882
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24584
24883
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24585
24884
|
if (roleAttr) {
|
|
@@ -24685,6 +24984,18 @@ const KEYBOARD_HANDLER_PROP_NAMES = [
|
|
|
24685
24984
|
"onKeyPress"
|
|
24686
24985
|
];
|
|
24687
24986
|
const isKeyboardOperable = (node) => KEYBOARD_HANDLER_PROP_NAMES.some((propName) => Boolean(hasJsxPropIgnoreCase(node.attributes, propName)));
|
|
24987
|
+
const parseNumericBranch = (expression) => {
|
|
24988
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "number") return expression.value;
|
|
24989
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "-" && isNodeOfType(expression.argument, "Literal") && typeof expression.argument.value === "number") return -expression.argument.value;
|
|
24990
|
+
return null;
|
|
24991
|
+
};
|
|
24992
|
+
const isRovingTabindexValue = (value) => {
|
|
24993
|
+
if (!isNodeOfType(value, "JSXExpressionContainer")) return false;
|
|
24994
|
+
const expression = value.expression;
|
|
24995
|
+
if (!isNodeOfType(expression, "ConditionalExpression")) return false;
|
|
24996
|
+
if (isNodeOfType(expression.test, "Literal")) return false;
|
|
24997
|
+
return [parseNumericBranch(expression.consequent), parseNumericBranch(expression.alternate)].some((branchValue) => branchValue !== null && branchValue < 0);
|
|
24998
|
+
};
|
|
24688
24999
|
const resolveSettings$14 = (settings) => {
|
|
24689
25000
|
const reactDoctor = settings?.["react-doctor"];
|
|
24690
25001
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -24708,6 +25019,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
24708
25019
|
if (!tabIndex) return;
|
|
24709
25020
|
const tabIndexValue = tabIndex.value;
|
|
24710
25021
|
if (!tabIndexValue) return;
|
|
25022
|
+
if (isRovingTabindexValue(tabIndexValue)) return;
|
|
24711
25023
|
const numeric = parseJsxValue(tabIndexValue);
|
|
24712
25024
|
if (numeric === null) {
|
|
24713
25025
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues && !isKeyboardOperable(node)) context.report({
|
|
@@ -25892,16 +26204,22 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25892
26204
|
["tr", "row"],
|
|
25893
26205
|
["ul", "list"]
|
|
25894
26206
|
];
|
|
25895
|
-
const
|
|
25896
|
-
|
|
25897
|
-
|
|
25898
|
-
|
|
25899
|
-
|
|
25900
|
-
const
|
|
25901
|
-
|
|
25902
|
-
|
|
25903
|
-
|
|
26207
|
+
const EMPTY_ROLE_LIST = [];
|
|
26208
|
+
const buildLookup = (pairs, keyIndex) => {
|
|
26209
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
26210
|
+
for (const pair of pairs) {
|
|
26211
|
+
const key = pair[keyIndex];
|
|
26212
|
+
const value = pair[keyIndex === 0 ? 1 : 0];
|
|
26213
|
+
const values = lookup.get(key);
|
|
26214
|
+
if (!values) lookup.set(key, [value]);
|
|
26215
|
+
else if (!values.includes(value)) values.push(value);
|
|
26216
|
+
}
|
|
26217
|
+
return lookup;
|
|
25904
26218
|
};
|
|
26219
|
+
const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
|
|
26220
|
+
const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
|
|
26221
|
+
const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
|
|
26222
|
+
const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
|
|
25905
26223
|
//#endregion
|
|
25906
26224
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
25907
26225
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26138,22 +26456,22 @@ const noRenderInRender = defineRule({
|
|
|
26138
26456
|
title: "Component rendered by inline function call",
|
|
26139
26457
|
severity: "warn",
|
|
26140
26458
|
tags: ["test-noise"],
|
|
26141
|
-
recommendation: "Make it a named component so React
|
|
26459
|
+
recommendation: "Make it a named component rendered as JSX so React can track it and preserve its state.",
|
|
26142
26460
|
create: (context) => ({ JSXExpressionContainer(node) {
|
|
26143
26461
|
const expression = node.expression;
|
|
26144
26462
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26145
26463
|
let calleeName = null;
|
|
26464
|
+
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26465
|
+
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26466
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26146
26467
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26147
26468
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26148
|
-
|
|
26149
|
-
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26469
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression")) {
|
|
26150
26470
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26151
|
-
calleeName = expression.callee.property.name;
|
|
26152
26471
|
}
|
|
26153
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26154
26472
|
context.report({
|
|
26155
26473
|
node: expression,
|
|
26156
|
-
message: `
|
|
26474
|
+
message: `"${calleeName}()" hides a component behind an inline call, so pull it into its own component and render it as JSX so React can track it.`
|
|
26157
26475
|
});
|
|
26158
26476
|
} })
|
|
26159
26477
|
});
|
|
@@ -26256,8 +26574,8 @@ const countUseStates = (analysis, componentNode) => {
|
|
|
26256
26574
|
for (const ref of getDownstreamRefs(analysis, componentNode)) if (isState(analysis, ref)) stateVariables.add(ref.resolved);
|
|
26257
26575
|
return stateVariables.size;
|
|
26258
26576
|
};
|
|
26259
|
-
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode) => {
|
|
26260
|
-
const stateSetterRefs = effectFnRefs.filter((ref) =>
|
|
26577
|
+
const findPropUsedToResetAllState = (analysis, effectFnRefs, depsRefs, useEffectNode, effectFn) => {
|
|
26578
|
+
const stateSetterRefs = effectFnRefs.filter((ref) => isSyncStateSetterCall(analysis, ref, effectFn));
|
|
26261
26579
|
if (stateSetterRefs.length === 0) return null;
|
|
26262
26580
|
if (!stateSetterRefs.every((ref) => isSetStateToInitialValue(analysis, ref))) return null;
|
|
26263
26581
|
const containing = findContainingNode(analysis, useEffectNode);
|
|
@@ -26280,7 +26598,9 @@ const noResetAllStateOnPropChange = defineRule({
|
|
|
26280
26598
|
if (!effectFnRefs || !depsRefs) return;
|
|
26281
26599
|
const containing = findContainingNode(analysis, node);
|
|
26282
26600
|
if (containing && isCustomHook(containing)) return;
|
|
26283
|
-
|
|
26601
|
+
const effectFn = getEffectFn(analysis, node);
|
|
26602
|
+
if (!effectFn) return;
|
|
26603
|
+
if (!findPropUsedToResetAllState(analysis, effectFnRefs, depsRefs, node, effectFn)) return;
|
|
26284
26604
|
context.report({
|
|
26285
26605
|
node,
|
|
26286
26606
|
message: `Your users briefly see stale state when a prop changes because this useEffect clears all state.`
|
|
@@ -26401,6 +26721,7 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26401
26721
|
"headers",
|
|
26402
26722
|
"remountDeps"
|
|
26403
26723
|
];
|
|
26724
|
+
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26404
26725
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26405
26726
|
"createFileRoute",
|
|
26406
26727
|
"createRoute",
|
|
@@ -26416,6 +26737,7 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26416
26737
|
"server",
|
|
26417
26738
|
"handler"
|
|
26418
26739
|
];
|
|
26740
|
+
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26419
26741
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26420
26742
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26421
26743
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27120,14 +27442,21 @@ const noStaticElementInteractions = defineRule({
|
|
|
27120
27442
|
category: "Accessibility",
|
|
27121
27443
|
create: (context) => {
|
|
27122
27444
|
const settings = resolveSettings$12(context.settings);
|
|
27445
|
+
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27123
27446
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27124
27447
|
return { JSXOpeningElement(node) {
|
|
27125
27448
|
if (isTestlikeFile) return;
|
|
27126
27449
|
let hasNonBlockerHandler = false;
|
|
27127
27450
|
let hasAnyHandler = false;
|
|
27128
|
-
|
|
27129
|
-
|
|
27130
|
-
if (!attribute) continue;
|
|
27451
|
+
let seenHandlerNames = null;
|
|
27452
|
+
for (const attribute of node.attributes) {
|
|
27453
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
27454
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
27455
|
+
if (!attributeName) continue;
|
|
27456
|
+
const handlerNameLower = attributeName.toLowerCase();
|
|
27457
|
+
if (!handlersLower.has(handlerNameLower)) continue;
|
|
27458
|
+
if (seenHandlerNames?.has(handlerNameLower)) continue;
|
|
27459
|
+
(seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
27131
27460
|
if (isNullValue(attribute)) continue;
|
|
27132
27461
|
hasAnyHandler = true;
|
|
27133
27462
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -27139,6 +27468,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
27139
27468
|
if (!hasNonBlockerHandler) return;
|
|
27140
27469
|
const elementType = getElementType(node, context.settings);
|
|
27141
27470
|
if (!HTML_TAGS.has(elementType)) return;
|
|
27471
|
+
if (elementType === "svg") return;
|
|
27142
27472
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
27143
27473
|
if (isPresentationRole(node)) return;
|
|
27144
27474
|
if (isInteractiveElement(elementType, node)) return;
|
|
@@ -27152,7 +27482,8 @@ const noStaticElementInteractions = defineRule({
|
|
|
27152
27482
|
});
|
|
27153
27483
|
return;
|
|
27154
27484
|
}
|
|
27155
|
-
|
|
27485
|
+
let attributeValue = roleAttribute.value;
|
|
27486
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && isNodeOfType(attributeValue.expression, "Literal") && typeof attributeValue.expression.value === "string") attributeValue = attributeValue.expression;
|
|
27156
27487
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
27157
27488
|
const firstRole = attributeValue.value.toLowerCase().trim().split(/\s+/)[0];
|
|
27158
27489
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
@@ -28308,13 +28639,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28308
28639
|
["xml:lang", "xmlLang"],
|
|
28309
28640
|
["xml:space", "xmlSpace"]
|
|
28310
28641
|
]);
|
|
28311
|
-
const
|
|
28642
|
+
const DOM_PROPERTIES_IGNORE_CASE_BY_LOWER = new Map([
|
|
28312
28643
|
"charset",
|
|
28313
28644
|
"allowFullScreen",
|
|
28314
28645
|
"webkitAllowFullScreen",
|
|
28315
28646
|
"mozAllowFullScreen",
|
|
28316
28647
|
"webkitDirectory"
|
|
28317
|
-
];
|
|
28648
|
+
].map((name) => [name.toLowerCase(), name]));
|
|
28318
28649
|
//#endregion
|
|
28319
28650
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28320
28651
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28518,10 +28849,7 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28518
28849
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28519
28850
|
return !tagName.includes("-");
|
|
28520
28851
|
};
|
|
28521
|
-
const normalizeAttributeCase = (name) =>
|
|
28522
|
-
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28523
|
-
return name;
|
|
28524
|
-
};
|
|
28852
|
+
const normalizeAttributeCase = (name) => DOM_PROPERTIES_IGNORE_CASE_BY_LOWER.get(name.toLowerCase()) ?? name;
|
|
28525
28853
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28526
28854
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28527
28855
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28865,32 +29193,26 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28865
29193
|
if (!isHocCallee$1(parent)) return false;
|
|
28866
29194
|
return parent.arguments[0] === node;
|
|
28867
29195
|
};
|
|
29196
|
+
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
29197
|
+
"map",
|
|
29198
|
+
"forEach",
|
|
29199
|
+
"filter",
|
|
29200
|
+
"flatMap",
|
|
29201
|
+
"reduce",
|
|
29202
|
+
"reduceRight"
|
|
29203
|
+
]);
|
|
28868
29204
|
const isReturnOfMapCallback = (node) => {
|
|
28869
29205
|
const parent = node.parent;
|
|
28870
29206
|
if (!parent) return false;
|
|
28871
29207
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28872
29208
|
const callee = parent.callee;
|
|
28873
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28874
|
-
"map",
|
|
28875
|
-
"forEach",
|
|
28876
|
-
"filter",
|
|
28877
|
-
"flatMap",
|
|
28878
|
-
"reduce",
|
|
28879
|
-
"reduceRight"
|
|
28880
|
-
].includes(callee.property.name);
|
|
29209
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28881
29210
|
}
|
|
28882
29211
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28883
29212
|
const callbackParent = parent.parent;
|
|
28884
29213
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28885
29214
|
const callee = callbackParent.callee;
|
|
28886
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
28887
|
-
"map",
|
|
28888
|
-
"forEach",
|
|
28889
|
-
"filter",
|
|
28890
|
-
"flatMap",
|
|
28891
|
-
"reduce",
|
|
28892
|
-
"reduceRight"
|
|
28893
|
-
].includes(callee.property.name);
|
|
29215
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return MAP_LIKE_METHOD_NAMES.has(callee.property.name);
|
|
28894
29216
|
}
|
|
28895
29217
|
}
|
|
28896
29218
|
return false;
|
|
@@ -28926,10 +29248,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
28926
29248
|
valueNode = parent;
|
|
28927
29249
|
parent = parent.parent;
|
|
28928
29250
|
continue;
|
|
28929
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
29251
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isReactComponentName(parent.id.name);
|
|
28930
29252
|
case "AssignmentExpression": {
|
|
28931
29253
|
const assignmentTarget = parent.left;
|
|
28932
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
29254
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isReactComponentName(assignmentTarget.name);
|
|
28933
29255
|
}
|
|
28934
29256
|
case "Property":
|
|
28935
29257
|
if (parent.value !== valueNode) return false;
|
|
@@ -29022,14 +29344,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
29022
29344
|
};
|
|
29023
29345
|
return {
|
|
29024
29346
|
JSXOpeningElement(node) {
|
|
29025
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29347
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isReactComponentName(node.name.name)) {
|
|
29026
29348
|
recordInstantiation(node.name, node.name.name);
|
|
29027
29349
|
return;
|
|
29028
29350
|
}
|
|
29029
29351
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
29030
29352
|
},
|
|
29031
29353
|
Identifier(node) {
|
|
29032
|
-
if (!
|
|
29354
|
+
if (!isReactComponentName(node.name)) return;
|
|
29033
29355
|
if (!isRenderFlowingReadReference(node)) return;
|
|
29034
29356
|
recordInstantiation(node, node.name);
|
|
29035
29357
|
},
|
|
@@ -29960,7 +30282,7 @@ const containsVnodeFactoryCall = (root) => {
|
|
|
29960
30282
|
};
|
|
29961
30283
|
const isComponentLikeFunction = (functionNode) => {
|
|
29962
30284
|
const bindingName = getFunctionBindingName(functionNode);
|
|
29963
|
-
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN.test(bindingName))) return true;
|
|
30285
|
+
if (bindingName && (isReactComponentName(bindingName) || HOOK_NAME_PATTERN$1.test(bindingName))) return true;
|
|
29964
30286
|
const body = "body" in functionNode ? functionNode.body : null;
|
|
29965
30287
|
if (!body || !isAstNode(body)) return false;
|
|
29966
30288
|
return containsJsxElement(body) || containsVnodeFactoryCall(body);
|
|
@@ -30839,13 +31161,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30839
31161
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30840
31162
|
},
|
|
30841
31163
|
JSXAttribute(node) {
|
|
30842
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30843
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30844
31164
|
if (!node.value) return;
|
|
30845
31165
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30846
31166
|
const innerExpression = node.value.expression;
|
|
30847
31167
|
if (!innerExpression) return;
|
|
30848
31168
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
31169
|
+
if (!isInsideFunctionScope(node)) return;
|
|
31170
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30849
31171
|
const parentJsxOpening = node.parent;
|
|
30850
31172
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30851
31173
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -31417,7 +31739,7 @@ const queryMutationMissingInvalidation = defineRule({
|
|
|
31417
31739
|
});
|
|
31418
31740
|
if (!hasCacheUpdate) context.report({
|
|
31419
31741
|
node,
|
|
31420
|
-
message: "useMutation with no cache update
|
|
31742
|
+
message: "useMutation with no cache update here can leave your users looking at stale data after it runs."
|
|
31421
31743
|
});
|
|
31422
31744
|
} })
|
|
31423
31745
|
});
|
|
@@ -32066,6 +32388,7 @@ const renderingHydrationMismatchTime = defineRule({
|
|
|
32066
32388
|
return { JSXExpressionContainer(node) {
|
|
32067
32389
|
if (isTestlikeFile) return;
|
|
32068
32390
|
if (!node.expression) return;
|
|
32391
|
+
if (isGeneratedImageRenderContext(context, findOpeningElementOfChild(node) ?? node)) return;
|
|
32069
32392
|
const matched = NONDETERMINISTIC_RENDER_PATTERNS.find((pattern) => pattern.matches(node.expression));
|
|
32070
32393
|
if (matched) {
|
|
32071
32394
|
if (hasSuppressHydrationWarningAttribute(findOpeningElementOfChild(node))) return;
|
|
@@ -32217,41 +32540,11 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32217
32540
|
});
|
|
32218
32541
|
return found;
|
|
32219
32542
|
};
|
|
32220
|
-
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32221
|
-
if (!componentBody) return false;
|
|
32222
|
-
let found = false;
|
|
32223
|
-
walkAst(componentBody, (child) => {
|
|
32224
|
-
if (found) return;
|
|
32225
|
-
if (!isFunctionLike$1(child)) return;
|
|
32226
|
-
const functionBody = child.body;
|
|
32227
|
-
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32228
|
-
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32229
|
-
});
|
|
32230
|
-
return found;
|
|
32231
|
-
};
|
|
32232
32543
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32233
32544
|
"then",
|
|
32234
32545
|
"catch",
|
|
32235
32546
|
"finally"
|
|
32236
32547
|
]);
|
|
32237
|
-
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32238
|
-
if (!componentBody) return false;
|
|
32239
|
-
let found = false;
|
|
32240
|
-
walkAst(componentBody, (child) => {
|
|
32241
|
-
if (found) return;
|
|
32242
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32243
|
-
const callee = child.callee;
|
|
32244
|
-
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32245
|
-
for (const argument of child.arguments ?? []) {
|
|
32246
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32247
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32248
|
-
found = true;
|
|
32249
|
-
return;
|
|
32250
|
-
}
|
|
32251
|
-
}
|
|
32252
|
-
});
|
|
32253
|
-
return found;
|
|
32254
|
-
};
|
|
32255
32548
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32256
32549
|
"useApolloClient",
|
|
32257
32550
|
"useMutation",
|
|
@@ -32264,20 +32557,36 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32264
32557
|
"fetch",
|
|
32265
32558
|
"axios"
|
|
32266
32559
|
]);
|
|
32267
|
-
const
|
|
32268
|
-
if (!body) return false;
|
|
32560
|
+
const hasAsyncLoadingWork = (fnBody, setterName) => {
|
|
32269
32561
|
let found = false;
|
|
32270
|
-
walkAst(
|
|
32271
|
-
if (found) return;
|
|
32562
|
+
walkAst(fnBody, (child) => {
|
|
32563
|
+
if (found) return false;
|
|
32272
32564
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32273
32565
|
const callee = child.callee;
|
|
32274
32566
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32275
32567
|
found = true;
|
|
32276
|
-
return;
|
|
32568
|
+
return false;
|
|
32277
32569
|
}
|
|
32278
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")
|
|
32570
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32571
|
+
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32572
|
+
found = true;
|
|
32573
|
+
return false;
|
|
32574
|
+
}
|
|
32575
|
+
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32576
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32577
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32578
|
+
found = true;
|
|
32579
|
+
return false;
|
|
32580
|
+
}
|
|
32581
|
+
}
|
|
32582
|
+
}
|
|
32583
|
+
return;
|
|
32584
|
+
}
|
|
32585
|
+
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32586
|
+
const functionBody = child.body;
|
|
32587
|
+
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32279
32588
|
found = true;
|
|
32280
|
-
return;
|
|
32589
|
+
return false;
|
|
32281
32590
|
}
|
|
32282
32591
|
}
|
|
32283
32592
|
});
|
|
@@ -32302,7 +32611,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32302
32611
|
const secondBinding = node.id.elements[1];
|
|
32303
32612
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32304
32613
|
const fnBody = enclosingFunctionBody(node);
|
|
32305
|
-
if (fnBody && (
|
|
32614
|
+
if (fnBody && hasAsyncLoadingWork(fnBody, setterName)) return;
|
|
32306
32615
|
context.report({
|
|
32307
32616
|
node: node.init,
|
|
32308
32617
|
message: `This adds an extra render because useState for "${stateVariableName}" re-renders just for the loading flag, so if it's a state change & not a data fetch, use useTransition instead`
|
|
@@ -33096,17 +33405,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33096
33405
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33097
33406
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33098
33407
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33408
|
+
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33409
|
+
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33410
|
+
walkAst(componentBody, (child) => {
|
|
33411
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33412
|
+
});
|
|
33099
33413
|
for (const binding of bindings) {
|
|
33100
33414
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33101
33415
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33102
33416
|
const setterSuffix = binding.setterName.slice(3);
|
|
33103
33417
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33104
|
-
|
|
33105
|
-
walkAst(componentBody, (child) => {
|
|
33106
|
-
if (setterCalled) return;
|
|
33107
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33108
|
-
});
|
|
33109
|
-
if (!setterCalled) continue;
|
|
33418
|
+
if (!calledSetterNames.has(binding.setterName)) continue;
|
|
33110
33419
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33111
33420
|
context.report({
|
|
33112
33421
|
node: binding.declarator,
|
|
@@ -33438,6 +33747,7 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33438
33747
|
FlashList: ["@shopify/flash-list"],
|
|
33439
33748
|
LegendList: ["@legendapp/list"]
|
|
33440
33749
|
};
|
|
33750
|
+
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33441
33751
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33442
33752
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33443
33753
|
"renderItem",
|
|
@@ -33680,33 +33990,42 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33680
33990
|
requires: ["react-native"],
|
|
33681
33991
|
severity: "warn",
|
|
33682
33992
|
recommendation: "Without `estimatedItemSize` the list guesses row height and can flash blank cells on fast scroll. Add `estimatedItemSize={<avg-row-height-in-px>}` so it matches your rows.",
|
|
33683
|
-
create: (context) =>
|
|
33684
|
-
|
|
33685
|
-
|
|
33686
|
-
|
|
33687
|
-
|
|
33688
|
-
|
|
33689
|
-
|
|
33690
|
-
|
|
33691
|
-
|
|
33692
|
-
|
|
33693
|
-
|
|
33694
|
-
|
|
33695
|
-
|
|
33696
|
-
|
|
33697
|
-
|
|
33698
|
-
hasDataProp =
|
|
33699
|
-
|
|
33993
|
+
create: (context) => {
|
|
33994
|
+
let fileImportsRecycler = false;
|
|
33995
|
+
return {
|
|
33996
|
+
Program(node) {
|
|
33997
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
33998
|
+
},
|
|
33999
|
+
JSXOpeningElement(node) {
|
|
34000
|
+
if (!fileImportsRecycler) return;
|
|
34001
|
+
const localElementName = resolveJsxElementName(node);
|
|
34002
|
+
if (!localElementName) return;
|
|
34003
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
34004
|
+
if (canonicalRecyclerName === null) return;
|
|
34005
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
34006
|
+
let hasSizingHint = false;
|
|
34007
|
+
let dataIsEmptyLiteral = false;
|
|
34008
|
+
let hasDataProp = false;
|
|
34009
|
+
for (const attribute of node.attributes ?? []) {
|
|
34010
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
34011
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
34012
|
+
const attributeName = attribute.name.name;
|
|
34013
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
34014
|
+
if (attributeName === "data") {
|
|
34015
|
+
hasDataProp = true;
|
|
34016
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
34017
|
+
}
|
|
34018
|
+
}
|
|
34019
|
+
if (hasSizingHint) return;
|
|
34020
|
+
if (dataIsEmptyLiteral) return;
|
|
34021
|
+
if (!hasDataProp) return;
|
|
34022
|
+
context.report({
|
|
34023
|
+
node,
|
|
34024
|
+
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
34025
|
+
});
|
|
33700
34026
|
}
|
|
33701
|
-
}
|
|
33702
|
-
|
|
33703
|
-
if (dataIsEmptyLiteral) return;
|
|
33704
|
-
if (!hasDataProp) return;
|
|
33705
|
-
context.report({
|
|
33706
|
-
node,
|
|
33707
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33708
|
-
});
|
|
33709
|
-
} })
|
|
34027
|
+
};
|
|
34028
|
+
}
|
|
33710
34029
|
});
|
|
33711
34030
|
//#endregion
|
|
33712
34031
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33717,25 +34036,34 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33717
34036
|
requires: ["react-native"],
|
|
33718
34037
|
severity: "warn",
|
|
33719
34038
|
recommendation: "When rows have different shapes, reused cells can show the wrong layout. Add `getItemType={item => item.kind}` so FlashList keeps a separate pool per row type.",
|
|
33720
|
-
create: (context) =>
|
|
33721
|
-
|
|
33722
|
-
|
|
33723
|
-
|
|
33724
|
-
|
|
33725
|
-
|
|
33726
|
-
|
|
33727
|
-
|
|
33728
|
-
|
|
33729
|
-
|
|
33730
|
-
|
|
33731
|
-
|
|
33732
|
-
|
|
33733
|
-
|
|
33734
|
-
|
|
33735
|
-
|
|
33736
|
-
|
|
33737
|
-
|
|
33738
|
-
|
|
34039
|
+
create: (context) => {
|
|
34040
|
+
let fileImportsRecycler = false;
|
|
34041
|
+
return {
|
|
34042
|
+
Program(node) {
|
|
34043
|
+
fileImportsRecycler = hasImportFromModules(node, RECYCLABLE_LIST_PACKAGE_SOURCES);
|
|
34044
|
+
},
|
|
34045
|
+
JSXOpeningElement(node) {
|
|
34046
|
+
if (!fileImportsRecycler) return;
|
|
34047
|
+
const elementName = resolveJsxElementName(node);
|
|
34048
|
+
if (!elementName) return;
|
|
34049
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
34050
|
+
let hasRecycleItemsEnabled = false;
|
|
34051
|
+
let hasGetItemType = false;
|
|
34052
|
+
for (const attr of node.attributes ?? []) {
|
|
34053
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
34054
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
34055
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
34056
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
34057
|
+
else hasRecycleItemsEnabled = true;
|
|
34058
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
34059
|
+
}
|
|
34060
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
34061
|
+
node,
|
|
34062
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
34063
|
+
});
|
|
34064
|
+
}
|
|
34065
|
+
};
|
|
34066
|
+
}
|
|
33739
34067
|
});
|
|
33740
34068
|
//#endregion
|
|
33741
34069
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -33876,7 +34204,7 @@ const rnNoDimensionsGet = defineRule({
|
|
|
33876
34204
|
if (binding !== null && !isBindingReactNativeDimensions(node, binding)) return;
|
|
33877
34205
|
if (isMemberProperty(node.callee, "get")) context.report({
|
|
33878
34206
|
node,
|
|
33879
|
-
message: "
|
|
34207
|
+
message: "Dimensions.get() reads the size once and never updates, so layouts built from it go stale on rotation or resize."
|
|
33880
34208
|
});
|
|
33881
34209
|
if (isMemberProperty(node.callee, "addEventListener")) context.report({
|
|
33882
34210
|
node,
|
|
@@ -34698,7 +35026,10 @@ const isExpoUiComponentElement = (openingElement, contextNode, componentName) =>
|
|
|
34698
35026
|
};
|
|
34699
35027
|
//#endregion
|
|
34700
35028
|
//#region src/plugin/rules/react-native/rn-no-raw-text.ts
|
|
34701
|
-
const truncateText = (text) =>
|
|
35029
|
+
const truncateText = (text) => {
|
|
35030
|
+
const collapsedText = text.replace(/\s+/g, " ");
|
|
35031
|
+
return collapsedText.length > 30 ? `${collapsedText.slice(0, 30)}...` : collapsedText;
|
|
35032
|
+
};
|
|
34702
35033
|
const isRawTextContent = (child) => {
|
|
34703
35034
|
if (isNodeOfType(child, "JSXText")) return Boolean(child.value?.trim());
|
|
34704
35035
|
if (!isNodeOfType(child, "JSXExpressionContainer") || !child.expression) return false;
|
|
@@ -34719,9 +35050,10 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34719
35050
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34720
35051
|
return resolveJsxElementName(openingElement);
|
|
34721
35052
|
};
|
|
35053
|
+
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34722
35054
|
const isTextHandlingComponent = (elementName) => {
|
|
34723
35055
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34724
|
-
return
|
|
35056
|
+
return TEXT_COMPONENT_KEYWORDS.some((keyword) => elementName.includes(keyword));
|
|
34725
35057
|
};
|
|
34726
35058
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34727
35059
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34765,6 +35097,7 @@ const rnNoRawText = defineRule({
|
|
|
34765
35097
|
return {
|
|
34766
35098
|
Program(programNode) {
|
|
34767
35099
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
35100
|
+
if (!containsJsxElement(programNode)) return;
|
|
34768
35101
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34769
35102
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34770
35103
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38719,14 +39052,7 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38719
39052
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38720
39053
|
category: "Accessibility",
|
|
38721
39054
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38722
|
-
|
|
38723
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38724
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38725
|
-
if (!role) return;
|
|
38726
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38727
|
-
const isImplicit = !roleAttribute;
|
|
38728
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38729
|
-
if (!supported) return;
|
|
39055
|
+
let ariaAttributes = null;
|
|
38730
39056
|
for (const attribute of node.attributes) {
|
|
38731
39057
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38732
39058
|
const attributeNode = attribute;
|
|
@@ -38736,6 +39062,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38736
39062
|
const propName = propRawName.toLowerCase();
|
|
38737
39063
|
if (!propName.startsWith("aria-")) continue;
|
|
38738
39064
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
39065
|
+
(ariaAttributes ??= []).push({
|
|
39066
|
+
attribute,
|
|
39067
|
+
propName
|
|
39068
|
+
});
|
|
39069
|
+
}
|
|
39070
|
+
if (!ariaAttributes) return;
|
|
39071
|
+
const elementType = getElementType(node, context.settings);
|
|
39072
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
39073
|
+
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
39074
|
+
if (!role) return;
|
|
39075
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
39076
|
+
const isImplicit = !roleAttribute;
|
|
39077
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
39078
|
+
if (!supported) return;
|
|
39079
|
+
for (const { attribute, propName } of ariaAttributes) {
|
|
38739
39080
|
if (supported.has(propName)) continue;
|
|
38740
39081
|
context.report({
|
|
38741
39082
|
node: attribute,
|
|
@@ -38957,7 +39298,7 @@ const isInsideClassComponent = (node) => {
|
|
|
38957
39298
|
const hasShortCircuitAncestor = (descendant, ancestor) => {
|
|
38958
39299
|
let current = descendant.parent;
|
|
38959
39300
|
while (current && current !== ancestor) {
|
|
38960
|
-
if (isNodeOfType(current, "ConditionalExpression")) return true;
|
|
39301
|
+
if (isNodeOfType(current, "ConditionalExpression") && (isWithinRange(descendant, current.consequent) || isWithinRange(descendant, current.alternate))) return true;
|
|
38961
39302
|
if (isNodeOfType(current, "LogicalExpression") && (current.operator === "&&" || current.operator === "||" || current.operator === "??") && isWithinRange(descendant, current.right)) return true;
|
|
38962
39303
|
current = current.parent ?? null;
|
|
38963
39304
|
}
|
|
@@ -39083,6 +39424,7 @@ const rulesOfHooks = defineRule({
|
|
|
39083
39424
|
if (parentInfo.isComponentOrHook) isInsideComponentOrHook = true;
|
|
39084
39425
|
}
|
|
39085
39426
|
if (!isInsideComponentOrHook) {
|
|
39427
|
+
if (!enclosing.hasResolvedName) return;
|
|
39086
39428
|
if (isLocalNonHookFunctionCallee(node, context.scopes, settings)) return;
|
|
39087
39429
|
context.report({
|
|
39088
39430
|
node: node.callee,
|
|
@@ -39573,6 +39915,7 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39573
39915
|
cachedFunctionNames.add(node.id.name);
|
|
39574
39916
|
},
|
|
39575
39917
|
CallExpression(node) {
|
|
39918
|
+
if (cachedFunctionNames.size === 0) return;
|
|
39576
39919
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39577
39920
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39578
39921
|
const firstArg = node.arguments?.[0];
|
|
@@ -39656,6 +39999,14 @@ const objectExpressionHasCachingConfig = (objectExpression) => (objectExpression
|
|
|
39656
39999
|
const objectExpressionHasSpread = (objectExpression) => (objectExpression.properties ?? []).some((property) => isNodeOfType(property, "SpreadElement"));
|
|
39657
40000
|
const APP_ROUTER_FILE_PATTERN = new RegExp(`/app/(?:[^/]+/)*(?:route|page|layout|template|loading|error|default)\\.${NEXTJS_SOURCE_FILE_EXTENSION_GROUP}$`);
|
|
39658
40001
|
const NON_PROJECT_PATH_PATTERN = /\/(?:node_modules|dist|build|\.next)\//;
|
|
40002
|
+
const REMIX_IMPORT_SOURCE_PATTERN = /^(?:@remix-run\/|@react-router\/|react-router(?:-dom)?$)/;
|
|
40003
|
+
const programImportsRemixRouter = (programNode) => (programNode.body ?? []).some((statement) => isNodeOfType(statement, "ImportDeclaration") && !isTypeOnlyImport(statement) && typeof statement.source?.value === "string" && REMIX_IMPORT_SOURCE_PATTERN.test(statement.source.value));
|
|
40004
|
+
const isImportMetaUrlAssetArgument = (urlArg) => {
|
|
40005
|
+
if (!isNodeOfType(urlArg, "NewExpression")) return false;
|
|
40006
|
+
if (!isNodeOfType(urlArg.callee, "Identifier") || urlArg.callee.name !== "URL") return false;
|
|
40007
|
+
const baseArg = urlArg.arguments?.[1];
|
|
40008
|
+
return isNodeOfType(baseArg, "MemberExpression") && isNodeOfType(baseArg.object, "MetaProperty") && isNodeOfType(baseArg.property, "Identifier") && baseArg.property.name === "url";
|
|
40009
|
+
};
|
|
39659
40010
|
const serverFetchWithoutRevalidate = defineRule({
|
|
39660
40011
|
id: "server-fetch-without-revalidate",
|
|
39661
40012
|
title: "Fetch without revalidate",
|
|
@@ -39675,7 +40026,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39675
40026
|
isServerSideFile = false;
|
|
39676
40027
|
return;
|
|
39677
40028
|
}
|
|
39678
|
-
isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client");
|
|
40029
|
+
isServerSideFile = !(node.body ?? []).some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === "use client") && !programImportsRemixRouter(node);
|
|
39679
40030
|
},
|
|
39680
40031
|
CallExpression(node) {
|
|
39681
40032
|
if (!isServerSideFile) return;
|
|
@@ -39688,6 +40039,7 @@ const serverFetchWithoutRevalidate = defineRule({
|
|
|
39688
40039
|
if (objectExpressionHasSpread(optionsArg)) return;
|
|
39689
40040
|
}
|
|
39690
40041
|
const urlArg = node.arguments?.[0];
|
|
40042
|
+
if (isImportMetaUrlAssetArgument(urlArg)) return;
|
|
39691
40043
|
const urlText = isNodeOfType(urlArg, "Literal") && typeof urlArg.value === "string" ? `"${urlArg.value}"` : "url";
|
|
39692
40044
|
context.report({
|
|
39693
40045
|
node,
|
|
@@ -39916,7 +40268,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
39916
40268
|
if (node.kind === "let" || node.kind === "var") {
|
|
39917
40269
|
context.report({
|
|
39918
40270
|
node: declarator,
|
|
39919
|
-
message: `Module-scoped ${node.kind} "${variableName}"
|
|
40271
|
+
message: `Module-scoped ${node.kind} "${variableName}" is shared by every request, so any write to it leaks state between your users.`
|
|
39920
40272
|
});
|
|
39921
40273
|
continue;
|
|
39922
40274
|
}
|
|
@@ -39977,25 +40329,29 @@ const GATE_LEADING_VERBS = new Set([
|
|
|
39977
40329
|
"authorize",
|
|
39978
40330
|
"authenticate"
|
|
39979
40331
|
]);
|
|
39980
|
-
const
|
|
39981
|
-
|
|
39982
|
-
|
|
39983
|
-
|
|
39984
|
-
|
|
39985
|
-
|
|
39986
|
-
|
|
39987
|
-
if (declarator.init && !isNodeOfType(declarator.init, "AwaitExpression")) return true;
|
|
39988
|
-
}
|
|
40332
|
+
const declarationAwaitsExistingPromise = (declaration) => {
|
|
40333
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
40334
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
40335
|
+
const init = declarator.init;
|
|
40336
|
+
if (!isNodeOfType(init, "AwaitExpression")) continue;
|
|
40337
|
+
const argument = init.argument;
|
|
40338
|
+
if (isNodeOfType(argument, "Identifier") || isNodeOfType(argument, "MemberExpression")) return true;
|
|
39989
40339
|
}
|
|
39990
40340
|
return false;
|
|
39991
40341
|
};
|
|
39992
|
-
const
|
|
40342
|
+
const REQUEST_SCOPED_IMPORT_SOURCES = ["next/headers", "next-intl/server"];
|
|
40343
|
+
const isRequestScopedCallee = (callee) => {
|
|
40344
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
40345
|
+
if (REQUEST_SCOPED_IMPORT_SOURCES.some((moduleSource) => getImportedNameFromModule(callee, callee.name, moduleSource) !== null)) return true;
|
|
40346
|
+
return getImportedNameFromModule(callee, callee.name, "next/server") === "connection";
|
|
40347
|
+
};
|
|
40348
|
+
const declarationAwaitsRequestScopedCall = (declaration) => {
|
|
39993
40349
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
39994
40350
|
for (const declarator of declaration.declarations ?? []) {
|
|
39995
40351
|
const init = declarator.init;
|
|
39996
40352
|
if (!isNodeOfType(init, "AwaitExpression")) continue;
|
|
39997
40353
|
const argument = init.argument;
|
|
39998
|
-
if (isNodeOfType(argument, "
|
|
40354
|
+
if (isNodeOfType(argument, "CallExpression") && isRequestScopedCallee(argument.callee)) return true;
|
|
39999
40355
|
}
|
|
40000
40356
|
return false;
|
|
40001
40357
|
};
|
|
@@ -40030,7 +40386,8 @@ const serverSequentialIndependentAwait = defineRule({
|
|
|
40030
40386
|
if (!isNodeOfType(nextStatement, "VariableDeclaration")) continue;
|
|
40031
40387
|
if (!declarationStartsWithAwait(nextStatement)) continue;
|
|
40032
40388
|
if (declarationReadsAnyName(nextStatement, declaredNames)) continue;
|
|
40033
|
-
if (
|
|
40389
|
+
if (declarationAwaitsExistingPromise(nextStatement)) continue;
|
|
40390
|
+
if (declarationAwaitsRequestScopedCall(currentStatement) || declarationAwaitsRequestScopedCall(nextStatement)) continue;
|
|
40034
40391
|
if (declarationAwaitsGate(currentStatement)) continue;
|
|
40035
40392
|
context.report({
|
|
40036
40393
|
node: nextStatement,
|
|
@@ -40499,7 +40856,16 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40499
40856
|
//#endregion
|
|
40500
40857
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40501
40858
|
const CREATE_PUBLIC_TABLE_PATTERN = /create\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(?!(?:auth|storage|realtime|vault|extensions|graphql|graphql_public|pgbouncer|net|supabase_functions|supabase_migrations|cron|pgsodium|pgmq|information_schema|pg_catalog|pg_temp|private|internal)\s*\.)(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?(?:\s*\(|\s+as\b)/gi;
|
|
40502
|
-
const
|
|
40859
|
+
const ENABLE_RLS_PATTERN = /alter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(?:public\s*\.\s*)?["`]?([A-Za-z_][\w$]*)["`]?\s+(?:force\s+)?enable\s+row\s+level\s+security/gi;
|
|
40860
|
+
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40861
|
+
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40862
|
+
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40863
|
+
const tableName = match[1];
|
|
40864
|
+
if (tableName === void 0) continue;
|
|
40865
|
+
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40866
|
+
}
|
|
40867
|
+
return lastEnableIndexByTable;
|
|
40868
|
+
};
|
|
40503
40869
|
const supabaseTableMissingRls = defineRule({
|
|
40504
40870
|
id: "supabase-table-missing-rls",
|
|
40505
40871
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40510,11 +40876,13 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40510
40876
|
const content = sanitizeSqlForScan(file.content);
|
|
40511
40877
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40512
40878
|
const findings = [];
|
|
40879
|
+
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40513
40880
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40514
40881
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40515
40882
|
const tableName = match[1];
|
|
40516
40883
|
if (tableName === void 0) continue;
|
|
40517
|
-
|
|
40884
|
+
const lastEnableIndex = lastEnableIndexByTable.get(tableName.toLowerCase());
|
|
40885
|
+
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40518
40886
|
const location = getLocationAtIndex(content, match.index);
|
|
40519
40887
|
findings.push({
|
|
40520
40888
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -40972,7 +41340,7 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
40972
41340
|
if (!isNodeOfType(callParent, "CallExpression")) return false;
|
|
40973
41341
|
if (callParent.callee === functionNode) return false;
|
|
40974
41342
|
if (isNodeOfType(callParent.callee, "MemberExpression") && !callParent.callee.computed && isNodeOfType(callParent.callee.property, "Identifier") && PROMISE_CONTINUATION_METHODS.has(callParent.callee.property.name)) return true;
|
|
40975
|
-
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
|
|
41343
|
+
return isNodeOfType(callParent.callee, "Identifier") && HOOK_NAME_PATTERN$1.test(callParent.callee.name) && !RENDER_SYNCHRONOUS_HOOK_NAMES.has(callParent.callee.name) && callParent.arguments?.[0] === functionNode;
|
|
40976
41344
|
};
|
|
40977
41345
|
const isEventHandlerAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && REACT_HANDLER_PROP_PATTERN.test(node.name.name);
|
|
40978
41346
|
const isEventHandlerNamedProperty = (node) => isNodeOfType(node, "Property") && (isNodeOfType(node.key, "Identifier") && typeof node.key.name === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.name) || isNodeOfType(node.key, "Literal") && typeof node.key.value === "string" && REACT_HANDLER_PROP_PATTERN.test(node.key.value));
|
|
@@ -41000,11 +41368,11 @@ const tanstackStartNoNavigateInRender = defineRule({
|
|
|
41000
41368
|
if (isNodeOfType(parent, "ReturnStatement")) {
|
|
41001
41369
|
const outerFunction = findEnclosingFunction(parent);
|
|
41002
41370
|
const hookName = outerFunction ? getFunctionBindingName(outerFunction) : null;
|
|
41003
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41371
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
41004
41372
|
}
|
|
41005
41373
|
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === functionNode) {
|
|
41006
41374
|
const hookName = getFunctionBindingName(parent);
|
|
41007
|
-
return Boolean(hookName && HOOK_NAME_PATTERN.test(hookName));
|
|
41375
|
+
return Boolean(hookName && HOOK_NAME_PATTERN$1.test(hookName));
|
|
41008
41376
|
}
|
|
41009
41377
|
return false;
|
|
41010
41378
|
};
|
|
@@ -41204,10 +41572,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41204
41572
|
const propertyName = getPropertyKeyName(property);
|
|
41205
41573
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41206
41574
|
}
|
|
41207
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41575
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_INDEX.has(propertyName));
|
|
41208
41576
|
let lastIndex = -1;
|
|
41209
41577
|
for (const propertyName of sensitiveProperties) {
|
|
41210
|
-
const currentIndex =
|
|
41578
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_INDEX.get(propertyName) ?? -1;
|
|
41211
41579
|
if (currentIndex < lastIndex) {
|
|
41212
41580
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41213
41581
|
context.report({
|
|
@@ -41244,10 +41612,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41244
41612
|
} else return;
|
|
41245
41613
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41246
41614
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41247
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41615
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_INDEX.has(toMethodOrderToken(name)));
|
|
41248
41616
|
let lastIndex = -1;
|
|
41249
41617
|
for (const methodName of orderSensitiveMethods) {
|
|
41250
|
-
const currentIndex =
|
|
41618
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_INDEX.get(toMethodOrderToken(methodName)) ?? -1;
|
|
41251
41619
|
if (currentIndex < lastIndex) {
|
|
41252
41620
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41253
41621
|
context.report({
|
|
@@ -41531,13 +41899,21 @@ const webhookSignatureRisk = defineRule({
|
|
|
41531
41899
|
//#endregion
|
|
41532
41900
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41533
41901
|
const ZOD_MODULE = "zod";
|
|
41902
|
+
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41534
41903
|
const getStaticPropertyName = (member) => {
|
|
41535
41904
|
const property = member.property;
|
|
41536
41905
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41537
41906
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41538
41907
|
return null;
|
|
41539
41908
|
};
|
|
41909
|
+
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41540
41910
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41911
|
+
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41912
|
+
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41913
|
+
importInfoCache.set(identifier, importInfo);
|
|
41914
|
+
return importInfo;
|
|
41915
|
+
};
|
|
41916
|
+
const computeImportInfoForIdentifier = (identifier) => {
|
|
41541
41917
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41542
41918
|
if (!specifier) return null;
|
|
41543
41919
|
const declaration = specifier.parent;
|
|
@@ -41674,25 +42050,33 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41674
42050
|
tags: ["migration-hint"],
|
|
41675
42051
|
severity: "warn",
|
|
41676
42052
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41677
|
-
create: (context) =>
|
|
41678
|
-
|
|
41679
|
-
|
|
41680
|
-
|
|
41681
|
-
|
|
41682
|
-
|
|
41683
|
-
|
|
41684
|
-
|
|
41685
|
-
|
|
41686
|
-
|
|
41687
|
-
|
|
41688
|
-
|
|
41689
|
-
|
|
41690
|
-
|
|
41691
|
-
|
|
41692
|
-
|
|
41693
|
-
|
|
41694
|
-
|
|
41695
|
-
|
|
42053
|
+
create: (context) => {
|
|
42054
|
+
let fileImportsZod = false;
|
|
42055
|
+
return {
|
|
42056
|
+
Program(node) {
|
|
42057
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42058
|
+
},
|
|
42059
|
+
CallExpression(node) {
|
|
42060
|
+
if (!fileImportsZod) return;
|
|
42061
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
42062
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
42063
|
+
context.report({
|
|
42064
|
+
node,
|
|
42065
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
42066
|
+
});
|
|
42067
|
+
},
|
|
42068
|
+
MemberExpression(node) {
|
|
42069
|
+
if (!fileImportsZod) return;
|
|
42070
|
+
const parent = node.parent;
|
|
42071
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42072
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
42073
|
+
context.report({
|
|
42074
|
+
node,
|
|
42075
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
42076
|
+
});
|
|
42077
|
+
}
|
|
42078
|
+
};
|
|
42079
|
+
}
|
|
41696
42080
|
});
|
|
41697
42081
|
//#endregion
|
|
41698
42082
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -41884,16 +42268,24 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
41884
42268
|
tags: ["migration-hint"],
|
|
41885
42269
|
severity: "warn",
|
|
41886
42270
|
recommendation: "Switch to the Zod 4 versions: top-level factories like `z.enum()`, object helpers like `z.strictObject()`, the new `z.function({ input, output })` form, and explicit key/value schemas for `z.record()`.",
|
|
41887
|
-
create: (context) =>
|
|
41888
|
-
|
|
41889
|
-
|
|
41890
|
-
|
|
41891
|
-
|
|
41892
|
-
|
|
41893
|
-
|
|
41894
|
-
|
|
41895
|
-
|
|
41896
|
-
|
|
42271
|
+
create: (context) => {
|
|
42272
|
+
let fileImportsZod = false;
|
|
42273
|
+
return {
|
|
42274
|
+
Program(node) {
|
|
42275
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42276
|
+
},
|
|
42277
|
+
CallExpression(node) {
|
|
42278
|
+
if (!fileImportsZod) return;
|
|
42279
|
+
if (isCallToDeprecatedTopLevelFactory(node) || isCallToDroppedCreateFactory(node) || isSingleArgumentRecordCall(node) || isLiteralSymbolCall(node) || isDeprecatedFunctionChainCall(node) || isDirectMethodCallOnZodFactory(node, OBJECT_FACTORY, OBJECT_METHODS) || isDirectMethodCallOnZodFactory(node, NUMBER_FACTORY, NUMBER_METHODS) || isRefineSecondArgumentFunction(node)) reportSchemaMigration(context, node);
|
|
42280
|
+
},
|
|
42281
|
+
MemberExpression(node) {
|
|
42282
|
+
if (!fileImportsZod) return;
|
|
42283
|
+
const parent = node.parent;
|
|
42284
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42285
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
42286
|
+
}
|
|
42287
|
+
};
|
|
42288
|
+
}
|
|
41897
42289
|
});
|
|
41898
42290
|
//#endregion
|
|
41899
42291
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -41928,13 +42320,22 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
41928
42320
|
tags: ["migration-hint"],
|
|
41929
42321
|
severity: "warn",
|
|
41930
42322
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
41931
|
-
create: (context) =>
|
|
41932
|
-
|
|
41933
|
-
|
|
41934
|
-
node
|
|
41935
|
-
|
|
41936
|
-
|
|
41937
|
-
|
|
42323
|
+
create: (context) => {
|
|
42324
|
+
let fileImportsZod = false;
|
|
42325
|
+
return {
|
|
42326
|
+
Program(node) {
|
|
42327
|
+
fileImportsZod = hasImportFromModules(node, ZOD_MODULE_SOURCES);
|
|
42328
|
+
},
|
|
42329
|
+
CallExpression(node) {
|
|
42330
|
+
if (!fileImportsZod) return;
|
|
42331
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42332
|
+
context.report({
|
|
42333
|
+
node,
|
|
42334
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42335
|
+
});
|
|
42336
|
+
}
|
|
42337
|
+
};
|
|
42338
|
+
}
|
|
41938
42339
|
});
|
|
41939
42340
|
//#endregion
|
|
41940
42341
|
//#region src/plugin/rule-registry.ts
|