oxlint-plugin-react-doctor 0.6.2-dev.da3b19c → 0.6.2-dev.fc75a3e
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 +446 -605
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -378,18 +378,13 @@ 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();
|
|
382
381
|
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
383
|
-
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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;
|
|
382
|
+
if (!sourceFilePattern.test(relativePath)) return false;
|
|
383
|
+
if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
384
|
+
if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
385
|
+
if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
386
|
+
if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
387
|
+
return true;
|
|
393
388
|
};
|
|
394
389
|
//#endregion
|
|
395
390
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
@@ -788,6 +783,24 @@ const collectChildComponentNames = (element, into) => {
|
|
|
788
783
|
into.add(name);
|
|
789
784
|
});
|
|
790
785
|
};
|
|
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
|
+
};
|
|
791
804
|
const countEffectHookCalls = (body) => {
|
|
792
805
|
if (!body) return 0;
|
|
793
806
|
let count = 0;
|
|
@@ -797,36 +810,6 @@ const countEffectHookCalls = (body) => {
|
|
|
797
810
|
});
|
|
798
811
|
return count;
|
|
799
812
|
};
|
|
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
|
-
};
|
|
830
813
|
const activityWrapsEffectHeavySubtree = defineRule({
|
|
831
814
|
id: "activity-wraps-effect-heavy-subtree",
|
|
832
815
|
title: "Activity wraps an effect-heavy subtree",
|
|
@@ -883,7 +866,9 @@ const activityWrapsEffectHeavySubtree = defineRule({
|
|
|
883
866
|
let totalEffects = 0;
|
|
884
867
|
const effectfulChildren = [];
|
|
885
868
|
for (const componentName of childComponentNames) {
|
|
886
|
-
const
|
|
869
|
+
const body = findSameFileComponentBody(programRoot, componentName);
|
|
870
|
+
if (!body) continue;
|
|
871
|
+
const effectCount = countEffectHookCalls(body);
|
|
887
872
|
if (effectCount === 0) continue;
|
|
888
873
|
totalEffects += effectCount;
|
|
889
874
|
effectfulChildren.push(`<${componentName}>`);
|
|
@@ -1473,12 +1458,6 @@ const getElementType = (openingElement, settings) => {
|
|
|
1473
1458
|
return elementType;
|
|
1474
1459
|
};
|
|
1475
1460
|
//#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
|
|
1482
1461
|
//#region src/plugin/utils/find-import-source-for-name.ts
|
|
1483
1462
|
const collectFromProgram = (programRoot) => {
|
|
1484
1463
|
const lookup = /* @__PURE__ */ new Map();
|
|
@@ -1682,7 +1661,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1682
1661
|
};
|
|
1683
1662
|
//#endregion
|
|
1684
1663
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1685
|
-
const normalizeFilename = (filename) => filename.
|
|
1664
|
+
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1686
1665
|
//#endregion
|
|
1687
1666
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1688
1667
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -2031,12 +2010,7 @@ const altText = defineRule({
|
|
|
2031
2010
|
const objectAliases = new Set(settings.object ?? []);
|
|
2032
2011
|
const areaAliases = new Set(settings.area ?? []);
|
|
2033
2012
|
const inputImageAliases = new Set(settings["input[type=\"image\"]"] ?? []);
|
|
2034
|
-
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2035
2013
|
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
|
-
}
|
|
2040
2014
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
2041
2015
|
const tag = getElementType(node, context.settings);
|
|
2042
2016
|
if (checkImg && (tag === "img" || imgAliases.has(tag))) {
|
|
@@ -2219,9 +2193,7 @@ const anchorIsValid = defineRule({
|
|
|
2219
2193
|
category: "Accessibility",
|
|
2220
2194
|
create: (context) => {
|
|
2221
2195
|
const settings = resolveSettings$50(context.settings);
|
|
2222
|
-
const fileHasJsxA11ySettings = hasJsxA11ySettings(context.settings);
|
|
2223
2196
|
return { JSXOpeningElement(node) {
|
|
2224
|
-
if (!fileHasJsxA11ySettings && (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "a")) return;
|
|
2225
2197
|
if (getElementType(node, context.settings) !== "a") return;
|
|
2226
2198
|
let hrefAttribute;
|
|
2227
2199
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
@@ -3647,9 +3619,8 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3647
3619
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3648
3620
|
//#endregion
|
|
3649
3621
|
//#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");
|
|
3651
3622
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3652
|
-
for (const match of content.matchAll(
|
|
3623
|
+
for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
|
|
3653
3624
|
const value = match[0] ?? "";
|
|
3654
3625
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3655
3626
|
}
|
|
@@ -4116,56 +4087,62 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4116
4087
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4117
4088
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4118
4089
|
};
|
|
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
|
+
};
|
|
4119
4107
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4120
4108
|
"push",
|
|
4121
4109
|
"unshift",
|
|
4122
4110
|
"splice"
|
|
4123
4111
|
]);
|
|
4124
|
-
const
|
|
4125
|
-
const
|
|
4112
|
+
const collectMutatedArrayNames = (block) => {
|
|
4113
|
+
const mutated = /* @__PURE__ */ new Set();
|
|
4126
4114
|
walkAst(block, (child) => {
|
|
4127
4115
|
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4128
|
-
if (!isNodeOfType(child, "
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
collectReferenceIdentifierNames(child.init, referencedNames);
|
|
4132
|
-
declaratorBindings.push({
|
|
4133
|
-
declaredName: child.id.name,
|
|
4134
|
-
referencedNames
|
|
4135
|
-
});
|
|
4116
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
4117
|
+
const callee = child.callee;
|
|
4118
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && ARRAY_MUTATION_METHOD_NAMES.has(callee.property.name) && isNodeOfType(callee.object, "Identifier")) mutated.add(callee.object.name);
|
|
4136
4119
|
});
|
|
4120
|
+
return mutated;
|
|
4121
|
+
};
|
|
4122
|
+
const addDerivedBindings = (block, names) => {
|
|
4137
4123
|
let didGrow = true;
|
|
4138
4124
|
while (didGrow) {
|
|
4139
4125
|
didGrow = false;
|
|
4140
|
-
|
|
4141
|
-
if (
|
|
4142
|
-
|
|
4143
|
-
|
|
4126
|
+
walkAst(block, (child) => {
|
|
4127
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4128
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
4129
|
+
if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
|
|
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);
|
|
4144
4134
|
didGrow = true;
|
|
4145
4135
|
break;
|
|
4146
4136
|
}
|
|
4147
|
-
}
|
|
4137
|
+
});
|
|
4148
4138
|
}
|
|
4149
4139
|
};
|
|
4150
4140
|
const hasLoopCarriedDependency = (block) => {
|
|
4151
|
-
const carried =
|
|
4152
|
-
const
|
|
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
|
-
});
|
|
4141
|
+
const carried = collectAssignedIdentifiers(block);
|
|
4142
|
+
for (const name of collectMutatedArrayNames(block)) carried.add(name);
|
|
4167
4143
|
if (carried.size === 0) return false;
|
|
4168
4144
|
addDerivedBindings(block, carried);
|
|
4145
|
+
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4169
4146
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4170
4147
|
return false;
|
|
4171
4148
|
};
|
|
@@ -4843,8 +4820,7 @@ const FORM_CONTROL_TAGS = new Set([
|
|
|
4843
4820
|
]);
|
|
4844
4821
|
const resolveSettings$48 = (settings) => {
|
|
4845
4822
|
const reactDoctor = settings?.["react-doctor"];
|
|
4846
|
-
|
|
4847
|
-
return { inputComponents: new Set(ruleSettings.inputComponents ?? []) };
|
|
4823
|
+
return { inputComponents: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.autocompleteValid ?? {} : {}).inputComponents ?? [] };
|
|
4848
4824
|
};
|
|
4849
4825
|
const autocompleteValid = defineRule({
|
|
4850
4826
|
id: "autocomplete-valid",
|
|
@@ -4857,7 +4833,7 @@ const autocompleteValid = defineRule({
|
|
|
4857
4833
|
const settings = resolveSettings$48(context.settings);
|
|
4858
4834
|
return { JSXOpeningElement: (node) => {
|
|
4859
4835
|
const tag = getElementType(node, context.settings);
|
|
4860
|
-
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.
|
|
4836
|
+
if (!FORM_CONTROL_TAGS.has(tag) && !settings.inputComponents.includes(tag)) return;
|
|
4861
4837
|
const attribute = hasJsxPropIgnoreCase(node.attributes, "autoComplete");
|
|
4862
4838
|
if (!attribute) return;
|
|
4863
4839
|
const value = getJsxPropStringValue(attribute);
|
|
@@ -4901,8 +4877,9 @@ const isCreateElementCall = (node) => {
|
|
|
4901
4877
|
const callee = node.callee;
|
|
4902
4878
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createElement";
|
|
4903
4879
|
if (isNodeOfType(callee, "MemberExpression")) {
|
|
4904
|
-
if (
|
|
4905
|
-
return
|
|
4880
|
+
if (memberChainContainsDocument(callee.object)) return false;
|
|
4881
|
+
if (callee.computed) return isNodeOfType(callee.property, "Literal") && callee.property.value === "createElement";
|
|
4882
|
+
return isNodeOfType(callee.property, "Identifier") && callee.property.name === "createElement";
|
|
4906
4883
|
}
|
|
4907
4884
|
return false;
|
|
4908
4885
|
};
|
|
@@ -5719,20 +5696,12 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5719
5696
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5720
5697
|
};
|
|
5721
5698
|
const isInertParseTarget = (target, fileContent) => {
|
|
5722
|
-
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5723
|
-
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5724
|
-
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5725
5699
|
const escapedTarget = escapeRegExp(target);
|
|
5726
5700
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5727
5701
|
if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
|
|
5728
|
-
if (
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
}
|
|
5732
|
-
if (fileHasIsolatedDocument) {
|
|
5733
|
-
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5734
|
-
}
|
|
5735
|
-
if (!fileHasCreateElement) return false;
|
|
5702
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5703
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5704
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5736
5705
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5737
5706
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5738
5707
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5825,9 +5794,10 @@ const dangerousHtmlSink = defineRule({
|
|
|
5825
5794
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5826
5795
|
if (valueIdentifier !== void 0) {
|
|
5827
5796
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5797
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
5798
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
5799
|
+
const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
|
|
5800
|
+
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5831
5801
|
}
|
|
5832
5802
|
}
|
|
5833
5803
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -5976,7 +5946,6 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
5976
5946
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
5977
5947
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
5978
5948
|
if (!classNameLiteral) return;
|
|
5979
|
-
if (!classNameLiteral.includes("px-") || !classNameLiteral.includes("py-")) return;
|
|
5980
5949
|
if (hasResponsivePrefix(classNameLiteral, "px") || hasResponsivePrefix(classNameLiteral, "py")) return;
|
|
5981
5950
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, PADDING_HORIZONTAL_AXIS_PATTERN, PADDING_VERTICAL_AXIS_PATTERN);
|
|
5982
5951
|
if (matchedPairs.length === 0) return;
|
|
@@ -6001,7 +5970,6 @@ const noRedundantSizeAxes = defineRule({
|
|
|
6001
5970
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
6002
5971
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
6003
5972
|
if (!classNameLiteral) return;
|
|
6004
|
-
if (!classNameLiteral.includes("w-") || !classNameLiteral.includes("h-")) return;
|
|
6005
5973
|
if (hasResponsivePrefix(classNameLiteral, "w") || hasResponsivePrefix(classNameLiteral, "h")) return;
|
|
6006
5974
|
const matchedPairs = collectAxisShorthandPairs(classNameLiteral, SIZE_WIDTH_AXIS_PATTERN, SIZE_HEIGHT_AXIS_PATTERN);
|
|
6007
5975
|
if (matchedPairs.length === 0) return;
|
|
@@ -6026,7 +5994,6 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
6026
5994
|
if (!isNodeOfType(jsxAttribute.name, "JSXIdentifier") || jsxAttribute.name.name !== "className") return;
|
|
6027
5995
|
const classNameLiteral = getClassNameLiteral(jsxAttribute);
|
|
6028
5996
|
if (!classNameLiteral) return;
|
|
6029
|
-
if (!classNameLiteral.includes("space-")) return;
|
|
6030
5997
|
const tokens = tokenizeClassName(classNameLiteral);
|
|
6031
5998
|
let hasFlexOrGridLayout = false;
|
|
6032
5999
|
for (const token of tokens) {
|
|
@@ -6218,10 +6185,7 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6218
6185
|
const actualMinor = Number(match[2]);
|
|
6219
6186
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6220
6187
|
};
|
|
6221
|
-
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6222
6188
|
const containsJsx$1 = (root) => {
|
|
6223
|
-
const cached = containsJsxCache.get(root);
|
|
6224
|
-
if (cached !== void 0) return cached;
|
|
6225
6189
|
let found = false;
|
|
6226
6190
|
const visit = (node) => {
|
|
6227
6191
|
if (found) return;
|
|
@@ -6244,7 +6208,6 @@ const containsJsx$1 = (root) => {
|
|
|
6244
6208
|
}
|
|
6245
6209
|
};
|
|
6246
6210
|
visit(root);
|
|
6247
|
-
containsJsxCache.set(root, found);
|
|
6248
6211
|
return found;
|
|
6249
6212
|
};
|
|
6250
6213
|
const getStaticMemberName = (node) => {
|
|
@@ -6336,6 +6299,27 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6336
6299
|
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;
|
|
6337
6300
|
return false;
|
|
6338
6301
|
};
|
|
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
|
+
};
|
|
6339
6323
|
const memberExpressionPath = (node) => {
|
|
6340
6324
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6341
6325
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6343,16 +6327,15 @@ const memberExpressionPath = (node) => {
|
|
|
6343
6327
|
const propertyName = getStaticMemberName(node);
|
|
6344
6328
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6345
6329
|
};
|
|
6346
|
-
const
|
|
6347
|
-
|
|
6348
|
-
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6349
|
-
if (cached) return cached;
|
|
6350
|
-
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6351
|
-
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6330
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
6331
|
+
let found = false;
|
|
6352
6332
|
const visit = (node) => {
|
|
6333
|
+
if (found) return;
|
|
6353
6334
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6354
|
-
if (
|
|
6355
|
-
|
|
6335
|
+
if (memberExpressionPath(node.left.object).includes(propertyName)) {
|
|
6336
|
+
found = true;
|
|
6337
|
+
return;
|
|
6338
|
+
}
|
|
6356
6339
|
}
|
|
6357
6340
|
const record = node;
|
|
6358
6341
|
for (const key of Object.keys(record)) {
|
|
@@ -6361,18 +6344,12 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
|
6361
6344
|
if (Array.isArray(child)) {
|
|
6362
6345
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6363
6346
|
} else if (isAstNode(child)) visit(child);
|
|
6347
|
+
if (found) return;
|
|
6364
6348
|
}
|
|
6365
6349
|
};
|
|
6366
6350
|
visit(programRoot);
|
|
6367
|
-
|
|
6368
|
-
identifierTargets,
|
|
6369
|
-
memberPathSegments
|
|
6370
|
-
};
|
|
6371
|
-
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6372
|
-
return index;
|
|
6351
|
+
return found;
|
|
6373
6352
|
};
|
|
6374
|
-
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6375
|
-
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6376
6353
|
const displayName = defineRule({
|
|
6377
6354
|
id: "display-name",
|
|
6378
6355
|
title: "Component missing display name",
|
|
@@ -8637,7 +8614,7 @@ const DEFAULT_HEADING_TAGS = [
|
|
|
8637
8614
|
const resolveSettings$40 = (settings) => {
|
|
8638
8615
|
const reactDoctor = settings?.["react-doctor"];
|
|
8639
8616
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.headingHasContent ?? {} : {};
|
|
8640
|
-
return { headingTags:
|
|
8617
|
+
return { headingTags: [...DEFAULT_HEADING_TAGS, ...ruleSettings.components ?? []] };
|
|
8641
8618
|
};
|
|
8642
8619
|
const headingHasContent = defineRule({
|
|
8643
8620
|
id: "heading-has-content",
|
|
@@ -8650,7 +8627,7 @@ const headingHasContent = defineRule({
|
|
|
8650
8627
|
const settings = resolveSettings$40(context.settings);
|
|
8651
8628
|
return { JSXOpeningElement(node) {
|
|
8652
8629
|
const elementType = getElementType(node, context.settings);
|
|
8653
|
-
if (!settings.headingTags.
|
|
8630
|
+
if (!settings.headingTags.includes(elementType)) return;
|
|
8654
8631
|
const parent = node.parent;
|
|
8655
8632
|
if (parent && isNodeOfType(parent, "JSXElement")) {
|
|
8656
8633
|
if (objectHasAccessibleChild(parent, context.settings)) return;
|
|
@@ -9289,10 +9266,10 @@ const imgRedundantAlt = defineRule({
|
|
|
9289
9266
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
9290
9267
|
const settings = resolveSettings$37(context.settings);
|
|
9291
9268
|
return { JSXOpeningElement(node) {
|
|
9269
|
+
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9292
9270
|
const tag = getElementType(node, context.settings);
|
|
9293
9271
|
if (!settings.components.includes(tag)) return;
|
|
9294
9272
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
9295
|
-
if (isGeneratedImageRenderContext(context, node)) return;
|
|
9296
9273
|
const altAttribute = hasJsxPropIgnoreCase(node.attributes, "alt");
|
|
9297
9274
|
if (!altAttribute) return;
|
|
9298
9275
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
@@ -9347,7 +9324,6 @@ const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|c
|
|
|
9347
9324
|
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
9348
9325
|
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
9349
9326
|
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
9350
|
-
const CRYPTO_SURFACE_TRIGGER_PATTERN = /createHash|md5|cipher|encrypt|decrypt|crypto|signature|Math\.random/i;
|
|
9351
9327
|
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
9352
9328
|
for (const callMatch of content.matchAll(pattern)) {
|
|
9353
9329
|
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
@@ -9377,7 +9353,6 @@ const insecureCryptoRisk = defineRule({
|
|
|
9377
9353
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9378
9354
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9379
9355
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9380
|
-
if (!CRYPTO_SURFACE_TRIGGER_PATTERN.test(file.content)) return [];
|
|
9381
9356
|
const content = getScannableContent(file);
|
|
9382
9357
|
let matchIndex = findMatchIndexNearContext(content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9383
9358
|
if (matchIndex < 0) matchIndex = content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
@@ -9556,7 +9531,6 @@ const KEYBOARD_EVENT_HANDLERS = [
|
|
|
9556
9531
|
"onKeyUp"
|
|
9557
9532
|
];
|
|
9558
9533
|
const ALL_EVENT_HANDLERS = [...MOUSE_EVENT_HANDLERS, ...KEYBOARD_EVENT_HANDLERS];
|
|
9559
|
-
const ALL_EVENT_HANDLERS_LOWER = new Set(ALL_EVENT_HANDLERS.map((handlerName) => handlerName.toLowerCase()));
|
|
9560
9534
|
//#endregion
|
|
9561
9535
|
//#region src/plugin/utils/is-disabled-element.ts
|
|
9562
9536
|
const isDisabledElement = (openingElement) => {
|
|
@@ -9611,25 +9585,15 @@ const interactiveSupportsFocus = defineRule({
|
|
|
9611
9585
|
const settings = resolveSettings$36(context.settings);
|
|
9612
9586
|
const tabbableSet = new Set(settings.tabbable);
|
|
9613
9587
|
return { JSXOpeningElement(node) {
|
|
9614
|
-
if (node.attributes.length === 0) return;
|
|
9615
9588
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
9616
9589
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
9617
9590
|
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
9618
|
-
if (!role) return;
|
|
9619
|
-
let hasInteractiveHandler = false;
|
|
9620
|
-
for (const attribute of node.attributes) {
|
|
9621
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
9622
|
-
const attributeName = getJsxAttributeName(attribute.name);
|
|
9623
|
-
if (attributeName && ALL_EVENT_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
9624
|
-
hasInteractiveHandler = true;
|
|
9625
|
-
break;
|
|
9626
|
-
}
|
|
9627
|
-
}
|
|
9628
|
-
if (!hasInteractiveHandler) return;
|
|
9629
9591
|
const elementType = getElementType(node, context.settings);
|
|
9630
9592
|
if (!HTML_TAGS.has(elementType)) return;
|
|
9631
|
-
|
|
9593
|
+
const hasInteractiveHandler = ALL_EVENT_HANDLERS.some((handler) => Boolean(hasJsxPropIgnoreCase(node.attributes, handler)));
|
|
9632
9594
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
9595
|
+
if (!hasInteractiveHandler || isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
9596
|
+
if (!role) return;
|
|
9633
9597
|
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
9634
9598
|
const message = tabbableSet.has(role) ? buildTabbableMessage(role) : buildFocusableMessage(role);
|
|
9635
9599
|
context.report({
|
|
@@ -10268,6 +10232,8 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10268
10232
|
walkAst(loopBody, (child) => {
|
|
10269
10233
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10270
10234
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10235
|
+
});
|
|
10236
|
+
walkAst(loopBody, (child) => {
|
|
10271
10237
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10272
10238
|
if (child.computed) return;
|
|
10273
10239
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10727,6 +10693,7 @@ const jsHoistIntl = defineRule({
|
|
|
10727
10693
|
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",
|
|
10728
10694
|
create: (context) => ({ NewExpression(node) {
|
|
10729
10695
|
if (!isIntlNewExpression(node)) return;
|
|
10696
|
+
if (isInsideCacheMemo(node)) return;
|
|
10730
10697
|
let cursor = node.parent ?? null;
|
|
10731
10698
|
let inFunctionBody = false;
|
|
10732
10699
|
while (cursor) {
|
|
@@ -10743,7 +10710,6 @@ const jsHoistIntl = defineRule({
|
|
|
10743
10710
|
cursor = cursor.parent ?? null;
|
|
10744
10711
|
}
|
|
10745
10712
|
if (!inFunctionBody) return;
|
|
10746
|
-
if (isInsideCacheMemo(node)) return;
|
|
10747
10713
|
const className = isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : "Intl";
|
|
10748
10714
|
context.report({
|
|
10749
10715
|
node,
|
|
@@ -10818,11 +10784,7 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10818
10784
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10819
10785
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10820
10786
|
};
|
|
10821
|
-
const
|
|
10822
|
-
const getLoopBoundNames = (loop) => {
|
|
10823
|
-
const cached = loopBoundNamesCache.get(loop);
|
|
10824
|
-
if (cached) return cached;
|
|
10825
|
-
const names = /* @__PURE__ */ new Set();
|
|
10787
|
+
const collectLoopBoundNames = (loop, names) => {
|
|
10826
10788
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10827
10789
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10828
10790
|
});
|
|
@@ -10839,8 +10801,6 @@ const getLoopBoundNames = (loop) => {
|
|
|
10839
10801
|
if (targetRoot) names.add(targetRoot);
|
|
10840
10802
|
}
|
|
10841
10803
|
});
|
|
10842
|
-
loopBoundNamesCache.set(loop, names);
|
|
10843
|
-
return names;
|
|
10844
10804
|
};
|
|
10845
10805
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10846
10806
|
let cursor = receiver;
|
|
@@ -10866,7 +10826,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10866
10826
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10867
10827
|
let ancestor = node.parent;
|
|
10868
10828
|
while (ancestor) {
|
|
10869
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10829
|
+
if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
|
|
10870
10830
|
ancestor = ancestor.parent;
|
|
10871
10831
|
}
|
|
10872
10832
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -14474,14 +14434,14 @@ const keyLifecycleRisk = defineRule({
|
|
|
14474
14434
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
14475
14435
|
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`.";
|
|
14476
14436
|
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.";
|
|
14477
|
-
const DEFAULT_CONTROL_COMPONENTS =
|
|
14437
|
+
const DEFAULT_CONTROL_COMPONENTS = [
|
|
14478
14438
|
"input",
|
|
14479
14439
|
"meter",
|
|
14480
14440
|
"output",
|
|
14481
14441
|
"progress",
|
|
14482
14442
|
"select",
|
|
14483
14443
|
"textarea"
|
|
14484
|
-
]
|
|
14444
|
+
];
|
|
14485
14445
|
const DEFAULT_LABEL_ATTRIBUTES = [
|
|
14486
14446
|
"alt",
|
|
14487
14447
|
"aria-label",
|
|
@@ -14493,8 +14453,8 @@ const resolveSettings$24 = (settings) => {
|
|
|
14493
14453
|
const jsxA11y = settings?.["jsx-a11y"];
|
|
14494
14454
|
const forAttributes = (typeof jsxA11y === "object" && jsxA11y !== null ? jsxA11y : {}).attributes?.for ?? ["htmlFor"];
|
|
14495
14455
|
return {
|
|
14496
|
-
labelComponents:
|
|
14497
|
-
labelAttributes: new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []]),
|
|
14456
|
+
labelComponents: ["label", ...ruleSettings.labelComponents ?? []].sort(),
|
|
14457
|
+
labelAttributes: [...new Set([...DEFAULT_LABEL_ATTRIBUTES, ...ruleSettings.labelAttributes ?? []])].sort(),
|
|
14498
14458
|
controlComponents: ruleSettings.controlComponents ?? [],
|
|
14499
14459
|
assert: ruleSettings.assert ?? "either",
|
|
14500
14460
|
depth: Math.min(ruleSettings.depth ?? 5, 25),
|
|
@@ -14502,7 +14462,7 @@ const resolveSettings$24 = (settings) => {
|
|
|
14502
14462
|
};
|
|
14503
14463
|
};
|
|
14504
14464
|
const isControlComponent = (tagName, controlComponents) => {
|
|
14505
|
-
if (DEFAULT_CONTROL_COMPONENTS.
|
|
14465
|
+
if (DEFAULT_CONTROL_COMPONENTS.includes(tagName)) return true;
|
|
14506
14466
|
return controlComponents.some((pattern) => compileGlob(pattern).test(tagName));
|
|
14507
14467
|
};
|
|
14508
14468
|
const searchForNestedControl = (child, currentDepth, searchContext) => {
|
|
@@ -14528,7 +14488,7 @@ const searchForAccessibleLabel = (child, currentDepth, searchContext) => {
|
|
|
14528
14488
|
const attributeName = attribute.name;
|
|
14529
14489
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14530
14490
|
const propName = getJsxAttributeName(attributeName);
|
|
14531
|
-
if (!propName || !searchContext.labelAttributes.
|
|
14491
|
+
if (!propName || !searchContext.labelAttributes.includes(propName)) continue;
|
|
14532
14492
|
const attributeValue = attribute.value;
|
|
14533
14493
|
if (!attributeValue) continue;
|
|
14534
14494
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
@@ -14550,7 +14510,7 @@ const hasAccessibleLabel = (element, searchContext) => {
|
|
|
14550
14510
|
const attributeName = attribute.name;
|
|
14551
14511
|
if (!isNodeOfType(attributeName, "JSXIdentifier")) continue;
|
|
14552
14512
|
const propName = getJsxAttributeName(attributeName);
|
|
14553
|
-
if (propName && searchContext.labelAttributes.
|
|
14513
|
+
if (propName && searchContext.labelAttributes.includes(propName)) return true;
|
|
14554
14514
|
}
|
|
14555
14515
|
for (const child of element.children) if (searchForAccessibleLabel(child, 1, searchContext)) return true;
|
|
14556
14516
|
return false;
|
|
@@ -14573,7 +14533,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
14573
14533
|
if (isTestlikeFile) return;
|
|
14574
14534
|
const opening = node.openingElement;
|
|
14575
14535
|
const tagName = getElementType(opening, context.settings);
|
|
14576
|
-
if (!settings.labelComponents.
|
|
14536
|
+
if (!settings.labelComponents.includes(tagName)) return;
|
|
14577
14537
|
const hasHtmlFor = settings.forAttributes.some((attributeName) => Boolean(hasJsxPropIgnoreCase(opening.attributes, attributeName)));
|
|
14578
14538
|
const searchContext = {
|
|
14579
14539
|
depth: settings.depth,
|
|
@@ -15162,9 +15122,9 @@ const resolveSettings$23 = (settings) => {
|
|
|
15162
15122
|
const reactDoctor = settings?.["react-doctor"];
|
|
15163
15123
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.mediaHasCaption ?? {} : {};
|
|
15164
15124
|
return {
|
|
15165
|
-
audio:
|
|
15166
|
-
video:
|
|
15167
|
-
track:
|
|
15125
|
+
audio: [...DEFAULT_AUDIO, ...ruleSettings.audio ?? []],
|
|
15126
|
+
video: [...DEFAULT_VIDEO, ...ruleSettings.video ?? []],
|
|
15127
|
+
track: [...DEFAULT_TRACK, ...ruleSettings.track ?? []]
|
|
15168
15128
|
};
|
|
15169
15129
|
};
|
|
15170
15130
|
const evaluateMuted = (attribute) => {
|
|
@@ -15193,7 +15153,7 @@ const childMayRenderTrack = (child, trackTags, settings) => {
|
|
|
15193
15153
|
let rendersCaptionTrack = false;
|
|
15194
15154
|
walkAst(expression, (inner) => {
|
|
15195
15155
|
if (rendersCaptionTrack) return false;
|
|
15196
|
-
if (isNodeOfType(inner, "JSXElement") && trackTags.
|
|
15156
|
+
if (isNodeOfType(inner, "JSXElement") && trackTags.includes(getElementType(inner.openingElement, settings)) && trackKindMightBeCaptions(inner.openingElement)) {
|
|
15197
15157
|
rendersCaptionTrack = true;
|
|
15198
15158
|
return false;
|
|
15199
15159
|
}
|
|
@@ -15211,7 +15171,7 @@ const mediaHasCaption = defineRule({
|
|
|
15211
15171
|
const settings = resolveSettings$23(context.settings);
|
|
15212
15172
|
return { JSXOpeningElement(node) {
|
|
15213
15173
|
const tag = getElementType(node, context.settings);
|
|
15214
|
-
if (!(settings.audio.
|
|
15174
|
+
if (!(settings.audio.includes(tag) || settings.video.includes(tag))) return;
|
|
15215
15175
|
if (evaluateMuted(hasJsxPropIgnoreCase(node.attributes, "muted")) === true) return;
|
|
15216
15176
|
const parent = node.parent;
|
|
15217
15177
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
@@ -15226,7 +15186,7 @@ const mediaHasCaption = defineRule({
|
|
|
15226
15186
|
if (!isNodeOfType(child, "JSXElement")) return false;
|
|
15227
15187
|
const opening = child.openingElement;
|
|
15228
15188
|
const childTag = getElementType(opening, context.settings);
|
|
15229
|
-
if (!settings.track.
|
|
15189
|
+
if (!settings.track.includes(childTag)) return false;
|
|
15230
15190
|
const kindAttribute = hasJsxPropIgnoreCase(opening.attributes, "kind");
|
|
15231
15191
|
if (!kindAttribute) return false;
|
|
15232
15192
|
let kindValue = kindAttribute.value;
|
|
@@ -15537,30 +15497,16 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15537
15497
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15538
15498
|
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;
|
|
15539
15499
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15540
|
-
const
|
|
15500
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
15541
15501
|
const strippedSource = stripJsComments(sourceText);
|
|
15542
|
-
|
|
15543
|
-
|
|
15544
|
-
for (const match of strippedSource.matchAll(
|
|
15545
|
-
|
|
15546
|
-
const specifiersText = match[1] ?? "";
|
|
15547
|
-
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15548
|
-
}
|
|
15549
|
-
return exportedNames;
|
|
15502
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
15503
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
15504
|
+
for (const match of strippedSource.matchAll(LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1)) if (parseExportSpecifiers(match[1] ?? "", false).map((specifier) => specifier.exportedName).includes(exportedName)) return true;
|
|
15505
|
+
return false;
|
|
15550
15506
|
};
|
|
15551
|
-
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15552
15507
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15553
15508
|
try {
|
|
15554
|
-
|
|
15555
|
-
const cached = exportNamesCache.get(filePath);
|
|
15556
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15557
|
-
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15558
|
-
exportNamesCache.set(filePath, {
|
|
15559
|
-
mtimeMs: fileStat.mtimeMs,
|
|
15560
|
-
size: fileStat.size,
|
|
15561
|
-
exportedNames
|
|
15562
|
-
});
|
|
15563
|
-
return exportedNames.has(exportedName);
|
|
15509
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
15564
15510
|
} catch {
|
|
15565
15511
|
return false;
|
|
15566
15512
|
}
|
|
@@ -15705,12 +15651,8 @@ const containsFetchCall = (node, options) => {
|
|
|
15705
15651
|
const effectInvokedFunctions = options?.stopAtFunctionBoundary ? collectEffectInvokedFunctions(node) : null;
|
|
15706
15652
|
let didFindFetchCall = false;
|
|
15707
15653
|
walkAst(node, (child) => {
|
|
15708
|
-
if (didFindFetchCall) return false;
|
|
15709
15654
|
if (effectInvokedFunctions && child !== node && isFunctionLike$1(child) && !effectInvokedFunctions.has(child)) return false;
|
|
15710
|
-
if (isFetchCall$1(child))
|
|
15711
|
-
didFindFetchCall = true;
|
|
15712
|
-
return false;
|
|
15713
|
-
}
|
|
15655
|
+
if (isFetchCall$1(child)) didFindFetchCall = true;
|
|
15714
15656
|
});
|
|
15715
15657
|
return didFindFetchCall;
|
|
15716
15658
|
};
|
|
@@ -16400,18 +16342,16 @@ const nextjsNoSideEffectInGetHandler = defineRule({
|
|
|
16400
16342
|
recommendation: "GET requests can be prefetched and are open to CSRF. Move the side effect to a POST handler.",
|
|
16401
16343
|
create: (context) => {
|
|
16402
16344
|
let resolveBinding = () => null;
|
|
16403
|
-
let isRouteHandlerFile = false;
|
|
16404
|
-
let mutatingSegment = null;
|
|
16405
16345
|
return {
|
|
16406
16346
|
Program(node) {
|
|
16407
16347
|
resolveBinding = buildProgramBindingLookup(node);
|
|
16408
|
-
const filename = normalizeFilename(context.filename ?? "");
|
|
16409
|
-
isRouteHandlerFile = ROUTE_HANDLER_FILE_PATTERN.test(filename) && !CRON_ROUTE_PATTERN.test(filename);
|
|
16410
|
-
mutatingSegment = isRouteHandlerFile ? extractMutatingRouteSegment(filename) : null;
|
|
16411
16348
|
},
|
|
16412
16349
|
ExportNamedDeclaration(node) {
|
|
16413
|
-
|
|
16350
|
+
const filename = normalizeFilename(context.filename ?? "");
|
|
16351
|
+
if (!ROUTE_HANDLER_FILE_PATTERN.test(filename)) return;
|
|
16352
|
+
if (CRON_ROUTE_PATTERN.test(filename)) return;
|
|
16414
16353
|
if (!isExportedGetHandler(node)) return;
|
|
16354
|
+
const mutatingSegment = extractMutatingRouteSegment(filename);
|
|
16415
16355
|
const handlerBodies = resolveGetHandlerBodies(node, resolveBinding);
|
|
16416
16356
|
for (const handlerBody of handlerBodies) {
|
|
16417
16357
|
const bodiesToScan = [handlerBody, ...collectCalledSameFileHelperBodies(handlerBody, resolveBinding)];
|
|
@@ -17443,15 +17383,8 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17443
17383
|
programToAnalysis.set(programNode, analysis);
|
|
17444
17384
|
return analysis;
|
|
17445
17385
|
};
|
|
17446
|
-
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17447
17386
|
const getScopeForNode = (node, manager) => {
|
|
17448
17387
|
if (!node.range) return null;
|
|
17449
|
-
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17450
|
-
if (!scopeByNode) {
|
|
17451
|
-
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17452
|
-
scopeByNodeCache.set(manager, scopeByNode);
|
|
17453
|
-
}
|
|
17454
|
-
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17455
17388
|
let bestScope = null;
|
|
17456
17389
|
let bestSize = Infinity;
|
|
17457
17390
|
for (const scope of manager.scopes) {
|
|
@@ -17464,7 +17397,6 @@ const getScopeForNode = (node, manager) => {
|
|
|
17464
17397
|
bestScope = scope;
|
|
17465
17398
|
}
|
|
17466
17399
|
}
|
|
17467
|
-
scopeByNode.set(node, bestScope);
|
|
17468
17400
|
return bestScope;
|
|
17469
17401
|
};
|
|
17470
17402
|
//#endregion
|
|
@@ -17499,20 +17431,11 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17499
17431
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17500
17432
|
}
|
|
17501
17433
|
};
|
|
17502
|
-
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17503
17434
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17504
|
-
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17505
|
-
if (!upstreamByRef) {
|
|
17506
|
-
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17507
|
-
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17508
|
-
}
|
|
17509
|
-
const cached = upstreamByRef.get(ref);
|
|
17510
|
-
if (cached) return cached;
|
|
17511
17435
|
const refs = [];
|
|
17512
17436
|
ascend(analysis, ref, (upRef) => {
|
|
17513
17437
|
refs.push(upRef);
|
|
17514
17438
|
});
|
|
17515
|
-
upstreamByRef.set(ref, refs);
|
|
17516
17439
|
return refs;
|
|
17517
17440
|
};
|
|
17518
17441
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17522,24 +17445,11 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17522
17445
|
});
|
|
17523
17446
|
return nodes;
|
|
17524
17447
|
};
|
|
17525
|
-
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17526
17448
|
const getRef = (analysis, identifier) => {
|
|
17527
|
-
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17528
|
-
if (!refByIdentifier) {
|
|
17529
|
-
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17530
|
-
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17531
|
-
}
|
|
17532
|
-
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17533
|
-
let resolvedReference = null;
|
|
17534
17449
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17535
|
-
if (scope)
|
|
17536
|
-
|
|
17537
|
-
|
|
17538
|
-
break;
|
|
17539
|
-
}
|
|
17540
|
-
}
|
|
17541
|
-
refByIdentifier.set(identifier, resolvedReference);
|
|
17542
|
-
return resolvedReference;
|
|
17450
|
+
if (!scope) return null;
|
|
17451
|
+
for (const reference of scope.references) if (reference.identifier === identifier) return reference;
|
|
17452
|
+
return null;
|
|
17543
17453
|
};
|
|
17544
17454
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17545
17455
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17614,6 +17524,22 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17614
17524
|
};
|
|
17615
17525
|
//#endregion
|
|
17616
17526
|
//#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
|
+
};
|
|
17617
17543
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17618
17544
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17619
17545
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17644,7 +17570,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17644
17570
|
const isWrappedSeparately = () => {
|
|
17645
17571
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17646
17572
|
const bindingName = node.id.name;
|
|
17647
|
-
const containingScope =
|
|
17573
|
+
const containingScope = getOuterScopeContaining(analysis, node);
|
|
17648
17574
|
if (!containingScope) return false;
|
|
17649
17575
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17650
17576
|
if (!variable) return false;
|
|
@@ -19509,8 +19435,8 @@ const CONTEXT_MODULES = [
|
|
|
19509
19435
|
];
|
|
19510
19436
|
const isCreateContextCallee = (callee) => {
|
|
19511
19437
|
if (isNodeOfType(callee, "Identifier")) {
|
|
19512
|
-
const
|
|
19513
|
-
return
|
|
19438
|
+
for (const moduleName of CONTEXT_MODULES) if (getImportedNameFromModule(callee, callee.name, moduleName) === "createContext") return true;
|
|
19439
|
+
return false;
|
|
19514
19440
|
}
|
|
19515
19441
|
if (isNodeOfType(callee, "MemberExpression") && !callee.computed) {
|
|
19516
19442
|
const namespaceIdentifier = callee.object;
|
|
@@ -19520,8 +19446,8 @@ const isCreateContextCallee = (callee) => {
|
|
|
19520
19446
|
if (propertyIdentifier.name !== "createContext") return false;
|
|
19521
19447
|
const namespaceName = namespaceIdentifier.name;
|
|
19522
19448
|
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
19523
|
-
const
|
|
19524
|
-
return
|
|
19449
|
+
for (const moduleName of CONTEXT_MODULES) if (isImportedFromModule(namespaceIdentifier, namespaceName, moduleName)) return true;
|
|
19450
|
+
return false;
|
|
19525
19451
|
}
|
|
19526
19452
|
return false;
|
|
19527
19453
|
};
|
|
@@ -19923,44 +19849,38 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19923
19849
|
};
|
|
19924
19850
|
const parseColorToRgb = (value) => {
|
|
19925
19851
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
|
|
19948
|
-
|
|
19949
|
-
|
|
19950
|
-
|
|
19951
|
-
|
|
19952
|
-
|
|
19953
|
-
|
|
19954
|
-
|
|
19955
|
-
|
|
19956
|
-
|
|
19957
|
-
|
|
19958
|
-
};
|
|
19959
|
-
}
|
|
19960
|
-
if (trimmed.includes("hsl")) {
|
|
19961
|
-
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19962
|
-
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19963
|
-
}
|
|
19852
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
19853
|
+
if (hex8Match) return {
|
|
19854
|
+
red: parseInt(hex8Match[1], 16),
|
|
19855
|
+
green: parseInt(hex8Match[2], 16),
|
|
19856
|
+
blue: parseInt(hex8Match[3], 16)
|
|
19857
|
+
};
|
|
19858
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
19859
|
+
if (hex6Match) return {
|
|
19860
|
+
red: parseInt(hex6Match[1], 16),
|
|
19861
|
+
green: parseInt(hex6Match[2], 16),
|
|
19862
|
+
blue: parseInt(hex6Match[3], 16)
|
|
19863
|
+
};
|
|
19864
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
19865
|
+
if (hex4Match) return {
|
|
19866
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
19867
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
19868
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
19869
|
+
};
|
|
19870
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
19871
|
+
if (hex3Match) return {
|
|
19872
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
19873
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
19874
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
19875
|
+
};
|
|
19876
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
19877
|
+
if (rgbMatch) return {
|
|
19878
|
+
red: parseInt(rgbMatch[1], 10),
|
|
19879
|
+
green: parseInt(rgbMatch[2], 10),
|
|
19880
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
19881
|
+
};
|
|
19882
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19883
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19964
19884
|
return null;
|
|
19965
19885
|
};
|
|
19966
19886
|
//#endregion
|
|
@@ -19988,18 +19908,9 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19988
19908
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19989
19909
|
return null;
|
|
19990
19910
|
};
|
|
19991
|
-
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
19992
|
-
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
19993
|
-
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
19994
|
-
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19995
19911
|
const parseShadowLayerBlur = (layer) => {
|
|
19996
|
-
const
|
|
19997
|
-
|
|
19998
|
-
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
19999
|
-
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
20000
|
-
tokenIndex += 1;
|
|
20001
|
-
}
|
|
20002
|
-
return 0;
|
|
19912
|
+
const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
|
|
19913
|
+
return numericTokens.length >= 3 ? numericTokens[2] : 0;
|
|
20003
19914
|
};
|
|
20004
19915
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
20005
19916
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -20119,10 +20030,7 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
20119
20030
|
};
|
|
20120
20031
|
//#endregion
|
|
20121
20032
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20122
|
-
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20123
20033
|
const collectComponentPropNames = (componentFunction) => {
|
|
20124
|
-
const cached = componentPropNamesCache.get(componentFunction);
|
|
20125
|
-
if (cached) return cached;
|
|
20126
20034
|
const propNames = /* @__PURE__ */ new Set();
|
|
20127
20035
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
20128
20036
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -20136,23 +20044,27 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
20136
20044
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
20137
20045
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
20138
20046
|
});
|
|
20139
|
-
componentPropNamesCache.set(componentFunction, propNames);
|
|
20140
20047
|
return propNames;
|
|
20141
20048
|
};
|
|
20142
|
-
const
|
|
20143
|
-
const getOwnScopeBoundNames = (functionNode) => {
|
|
20144
|
-
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20145
|
-
if (cached) return cached;
|
|
20049
|
+
const declaresBindingNamed = (functionNode, bindingName) => {
|
|
20146
20050
|
const boundNames = /* @__PURE__ */ new Set();
|
|
20147
20051
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
20052
|
+
if (boundNames.has(bindingName)) return true;
|
|
20053
|
+
let declaresName = false;
|
|
20148
20054
|
walkAst(functionNode, (child) => {
|
|
20055
|
+
if (declaresName) return false;
|
|
20149
20056
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
20150
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
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
|
+
}
|
|
20151
20065
|
});
|
|
20152
|
-
|
|
20153
|
-
return boundNames;
|
|
20066
|
+
return declaresName;
|
|
20154
20067
|
};
|
|
20155
|
-
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20156
20068
|
const isPropertyNamePosition = (identifier) => {
|
|
20157
20069
|
const parent = identifier.parent;
|
|
20158
20070
|
if (!parent) return false;
|
|
@@ -20575,28 +20487,36 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20575
20487
|
}
|
|
20576
20488
|
return hasNestedFunction;
|
|
20577
20489
|
};
|
|
20578
|
-
const
|
|
20579
|
-
|
|
20580
|
-
|
|
20581
|
-
|
|
20582
|
-
|
|
20583
|
-
|
|
20584
|
-
|
|
20490
|
+
const isReseededDraftBuffer = (useStateCall, isPropName) => {
|
|
20491
|
+
const setterName = getStateSetterName(useStateCall);
|
|
20492
|
+
if (!setterName) return false;
|
|
20493
|
+
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20494
|
+
if (!componentFunction) return false;
|
|
20495
|
+
let isReseeded = false;
|
|
20496
|
+
walkAst(componentFunction, (child) => {
|
|
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;
|
|
20585
20504
|
};
|
|
20586
|
-
const
|
|
20505
|
+
const isAdjustedDuringRender = (useStateCall, isPropName) => {
|
|
20587
20506
|
const setterName = getStateSetterName(useStateCall);
|
|
20588
20507
|
if (!setterName) return false;
|
|
20589
20508
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20590
20509
|
if (!componentFunction) return false;
|
|
20591
|
-
let
|
|
20510
|
+
let isAdjusted = false;
|
|
20592
20511
|
walkAst(componentFunction, (child) => {
|
|
20593
|
-
if (
|
|
20594
|
-
if (
|
|
20595
|
-
|
|
20512
|
+
if (isAdjusted) return false;
|
|
20513
|
+
if (child !== componentFunction && isFunctionLike$1(child)) return false;
|
|
20514
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
|
|
20515
|
+
isAdjusted = true;
|
|
20596
20516
|
return false;
|
|
20597
20517
|
}
|
|
20598
20518
|
});
|
|
20599
|
-
return
|
|
20519
|
+
return isAdjusted;
|
|
20600
20520
|
};
|
|
20601
20521
|
const noDerivedUseState = defineRule({
|
|
20602
20522
|
id: "no-derived-useState",
|
|
@@ -20613,7 +20533,8 @@ const noDerivedUseState = defineRule({
|
|
|
20613
20533
|
const initializer = node.arguments[0];
|
|
20614
20534
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20615
20535
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20616
|
-
if (
|
|
20536
|
+
if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
|
|
20537
|
+
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20617
20538
|
context.report({
|
|
20618
20539
|
node,
|
|
20619
20540
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20624,7 +20545,8 @@ const noDerivedUseState = defineRule({
|
|
|
20624
20545
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20625
20546
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20626
20547
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20627
|
-
if (
|
|
20548
|
+
if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
|
|
20549
|
+
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20628
20550
|
context.report({
|
|
20629
20551
|
node,
|
|
20630
20552
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22323,20 +22245,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22323
22245
|
return null;
|
|
22324
22246
|
};
|
|
22325
22247
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22326
|
-
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22327
|
-
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22328
|
-
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22329
|
-
walkAst(componentBody, (child) => {
|
|
22330
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22331
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22332
|
-
const setterName = child.callee.name;
|
|
22333
|
-
if (!setterNames.has(setterName)) return;
|
|
22334
|
-
settersWithAnyCall.add(setterName);
|
|
22335
|
-
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22336
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22337
|
-
});
|
|
22338
22248
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22339
|
-
for (const binding of useStateBindings)
|
|
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
|
+
}
|
|
22340
22262
|
return handlerOnlyWriteStateNames;
|
|
22341
22263
|
};
|
|
22342
22264
|
const noEventTriggerState = defineRule({
|
|
@@ -22676,10 +22598,6 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22676
22598
|
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)-)/;
|
|
22677
22599
|
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)-)/;
|
|
22678
22600
|
const splitVariantScope = (token) => {
|
|
22679
|
-
if (!token.includes(":")) return {
|
|
22680
|
-
scope: "",
|
|
22681
|
-
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22682
|
-
};
|
|
22683
22601
|
const segments = token.split(":");
|
|
22684
22602
|
const rawUtility = segments[segments.length - 1];
|
|
22685
22603
|
return {
|
|
@@ -22703,7 +22621,6 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22703
22621
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22704
22622
|
for (const token of classStr.split(/\s+/)) {
|
|
22705
22623
|
if (!token) continue;
|
|
22706
|
-
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22707
22624
|
const { scope, utility } = splitVariantScope(token);
|
|
22708
22625
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22709
22626
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23433,8 +23350,6 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23433
23350
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23434
23351
|
});
|
|
23435
23352
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23436
|
-
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23437
|
-
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23438
23353
|
const noLongTransitionDuration = defineRule({
|
|
23439
23354
|
id: "no-long-transition-duration",
|
|
23440
23355
|
title: "Transition duration too long",
|
|
@@ -23458,10 +23373,11 @@ const noLongTransitionDuration = defineRule({
|
|
|
23458
23373
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23459
23374
|
let longestDurationPropertyMs = 0;
|
|
23460
23375
|
for (const segment of value.split(",")) {
|
|
23461
|
-
const
|
|
23462
|
-
|
|
23463
|
-
const
|
|
23464
|
-
longestDurationPropertyMs = Math.max(longestDurationPropertyMs,
|
|
23376
|
+
const trimmedSegment = segment.trim();
|
|
23377
|
+
const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
|
|
23378
|
+
const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
|
|
23379
|
+
if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
|
|
23380
|
+
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23465
23381
|
}
|
|
23466
23382
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23467
23383
|
}
|
|
@@ -23469,7 +23385,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23469
23385
|
let longestDurationMs = 0;
|
|
23470
23386
|
for (const segment of value.split(",")) {
|
|
23471
23387
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23472
|
-
const firstTimeMatch = segment.match(
|
|
23388
|
+
const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
|
|
23473
23389
|
if (!firstTimeMatch) continue;
|
|
23474
23390
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23475
23391
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -24641,14 +24557,14 @@ const collectRoleBranches = (expression, out) => {
|
|
|
24641
24557
|
out.hasOpaqueBranch = true;
|
|
24642
24558
|
};
|
|
24643
24559
|
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.`;
|
|
24644
|
-
const
|
|
24560
|
+
const INTERACTIVE_HANDLERS = [
|
|
24645
24561
|
"onClick",
|
|
24646
24562
|
"onMouseDown",
|
|
24647
24563
|
"onMouseUp",
|
|
24648
24564
|
"onKeyDown",
|
|
24649
24565
|
"onKeyPress",
|
|
24650
24566
|
"onKeyUp"
|
|
24651
|
-
]
|
|
24567
|
+
];
|
|
24652
24568
|
const noNoninteractiveElementInteractions = defineRule({
|
|
24653
24569
|
id: "no-noninteractive-element-interactions",
|
|
24654
24570
|
title: "Handler on non-interactive element",
|
|
@@ -24663,16 +24579,7 @@ const noNoninteractiveElementInteractions = defineRule({
|
|
|
24663
24579
|
const tag = getElementType(node, context.settings);
|
|
24664
24580
|
if (!NON_INTERACTIVE_ELEMENTS.has(tag)) return;
|
|
24665
24581
|
if (tag === "label") return;
|
|
24666
|
-
|
|
24667
|
-
for (const attribute of node.attributes) {
|
|
24668
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24669
|
-
const attributeName = getJsxAttributeName(attribute.name);
|
|
24670
|
-
if (attributeName && INTERACTIVE_HANDLERS_LOWER.has(attributeName.toLowerCase())) {
|
|
24671
|
-
hasHandler = true;
|
|
24672
|
-
break;
|
|
24673
|
-
}
|
|
24674
|
-
}
|
|
24675
|
-
if (!hasHandler) return;
|
|
24582
|
+
if (!INTERACTIVE_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
24676
24583
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
24677
24584
|
const roleAttr = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
24678
24585
|
if (roleAttr) {
|
|
@@ -25985,22 +25892,16 @@ const ELEMENT_ROLE_PAIRS = [
|
|
|
25985
25892
|
["tr", "row"],
|
|
25986
25893
|
["ul", "list"]
|
|
25987
25894
|
];
|
|
25988
|
-
const
|
|
25989
|
-
const
|
|
25990
|
-
const
|
|
25991
|
-
|
|
25992
|
-
|
|
25993
|
-
|
|
25994
|
-
|
|
25995
|
-
|
|
25996
|
-
|
|
25997
|
-
}
|
|
25998
|
-
return lookup;
|
|
25895
|
+
const getElementImplicitRoles = (tag) => {
|
|
25896
|
+
const out = [];
|
|
25897
|
+
for (const [element, role] of ELEMENT_ROLE_PAIRS) if (element === tag && !out.includes(role)) out.push(role);
|
|
25898
|
+
return out;
|
|
25899
|
+
};
|
|
25900
|
+
const getTagsForRole = (role) => {
|
|
25901
|
+
const out = [];
|
|
25902
|
+
for (const [element, r] of ELEMENT_ROLE_PAIRS) if (r === role && !out.includes(element)) out.push(element);
|
|
25903
|
+
return out;
|
|
25999
25904
|
};
|
|
26000
|
-
const IMPLICIT_ROLES_BY_TAG = buildLookup(ELEMENT_ROLE_PAIRS, 0);
|
|
26001
|
-
const TAGS_BY_ROLE = buildLookup(ELEMENT_ROLE_PAIRS, 1);
|
|
26002
|
-
const getElementImplicitRoles = (tag) => IMPLICIT_ROLES_BY_TAG.get(tag) ?? EMPTY_ROLE_LIST;
|
|
26003
|
-
const getTagsForRole = (role) => TAGS_BY_ROLE.get(role) ?? EMPTY_ROLE_LIST;
|
|
26004
25905
|
//#endregion
|
|
26005
25906
|
//#region src/plugin/utils/get-implicit-role.ts
|
|
26006
25907
|
const getImplicitRole = (node, elementType) => {
|
|
@@ -26242,14 +26143,14 @@ const noRenderInRender = defineRule({
|
|
|
26242
26143
|
const expression = node.expression;
|
|
26243
26144
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
26244
26145
|
let calleeName = null;
|
|
26245
|
-
if (isNodeOfType(expression.callee, "Identifier")) calleeName = expression.callee.name;
|
|
26246
|
-
else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) calleeName = expression.callee.property.name;
|
|
26247
|
-
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26248
26146
|
if (isNodeOfType(expression.callee, "Identifier")) {
|
|
26249
26147
|
if (tracesToPropOrParameter(context.scopes.symbolFor(expression.callee), context.scopes)) return;
|
|
26250
|
-
|
|
26148
|
+
calleeName = expression.callee.name;
|
|
26149
|
+
} else if (isNodeOfType(expression.callee, "MemberExpression") && isNodeOfType(expression.callee.property, "Identifier")) {
|
|
26251
26150
|
if (rootsInProps(expression.callee.object, context.scopes)) return;
|
|
26151
|
+
calleeName = expression.callee.property.name;
|
|
26252
26152
|
}
|
|
26153
|
+
if (!calleeName || !RENDER_FUNCTION_PATTERN.test(calleeName)) return;
|
|
26253
26154
|
context.report({
|
|
26254
26155
|
node: expression,
|
|
26255
26156
|
message: `Your users lose state because "${calleeName}()" builds UI from an inline call that React remounts, so pull it into its own component instead.`
|
|
@@ -26500,7 +26401,6 @@ const TANSTACK_ROUTE_PROPERTY_ORDER = [
|
|
|
26500
26401
|
"headers",
|
|
26501
26402
|
"remountDeps"
|
|
26502
26403
|
];
|
|
26503
|
-
const TANSTACK_ROUTE_PROPERTY_INDEX = new Map(TANSTACK_ROUTE_PROPERTY_ORDER.map((propertyName, orderIndex) => [propertyName, orderIndex]));
|
|
26504
26404
|
const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
|
|
26505
26405
|
"createFileRoute",
|
|
26506
26406
|
"createRoute",
|
|
@@ -26516,7 +26416,6 @@ const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
|
|
|
26516
26416
|
"server",
|
|
26517
26417
|
"handler"
|
|
26518
26418
|
];
|
|
26519
|
-
const TANSTACK_MIDDLEWARE_METHOD_INDEX = new Map(TANSTACK_MIDDLEWARE_METHOD_ORDER.map((methodName, orderIndex) => [methodName, orderIndex]));
|
|
26520
26419
|
const TANSTACK_REDIRECT_FUNCTIONS = new Set(["redirect", "notFound"]);
|
|
26521
26420
|
const TANSTACK_SERVER_FN_FILE_PATTERN = /\.functions(\.[jt]sx?)?$/;
|
|
26522
26421
|
const TANSTACK_QUERY_HOOKS = new Set([
|
|
@@ -27221,21 +27120,14 @@ const noStaticElementInteractions = defineRule({
|
|
|
27221
27120
|
category: "Accessibility",
|
|
27222
27121
|
create: (context) => {
|
|
27223
27122
|
const settings = resolveSettings$12(context.settings);
|
|
27224
|
-
const handlersLower = new Set(settings.handlers.map((handlerName) => handlerName.toLowerCase()));
|
|
27225
27123
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
27226
27124
|
return { JSXOpeningElement(node) {
|
|
27227
27125
|
if (isTestlikeFile) return;
|
|
27228
27126
|
let hasNonBlockerHandler = false;
|
|
27229
27127
|
let hasAnyHandler = false;
|
|
27230
|
-
|
|
27231
|
-
|
|
27232
|
-
if (!
|
|
27233
|
-
const attributeName = getJsxAttributeName(attribute.name);
|
|
27234
|
-
if (!attributeName) continue;
|
|
27235
|
-
const handlerNameLower = attributeName.toLowerCase();
|
|
27236
|
-
if (!handlersLower.has(handlerNameLower)) continue;
|
|
27237
|
-
if (seenHandlerNames?.has(handlerNameLower)) continue;
|
|
27238
|
-
(seenHandlerNames ??= /* @__PURE__ */ new Set()).add(handlerNameLower);
|
|
27128
|
+
for (const handler of settings.handlers) {
|
|
27129
|
+
const attribute = hasJsxPropIgnoreCase(node.attributes, handler);
|
|
27130
|
+
if (!attribute) continue;
|
|
27239
27131
|
if (isNullValue(attribute)) continue;
|
|
27240
27132
|
hasAnyHandler = true;
|
|
27241
27133
|
if (!isPureEventBlockerHandler(attribute)) {
|
|
@@ -28416,13 +28308,13 @@ const DOM_ATTRIBUTES_TO_CAMEL = new Map([
|
|
|
28416
28308
|
["xml:lang", "xmlLang"],
|
|
28417
28309
|
["xml:space", "xmlSpace"]
|
|
28418
28310
|
]);
|
|
28419
|
-
const
|
|
28311
|
+
const DOM_PROPERTIES_IGNORE_CASE = [
|
|
28420
28312
|
"charset",
|
|
28421
28313
|
"allowFullScreen",
|
|
28422
28314
|
"webkitAllowFullScreen",
|
|
28423
28315
|
"mozAllowFullScreen",
|
|
28424
28316
|
"webkitDirectory"
|
|
28425
|
-
]
|
|
28317
|
+
];
|
|
28426
28318
|
//#endregion
|
|
28427
28319
|
//#region src/plugin/constants/dom-property-tags.ts
|
|
28428
28320
|
const DOM_PROPERTY_TO_ALLOWED_TAGS = new Map([
|
|
@@ -28626,7 +28518,10 @@ const matchesHtmlTagConventions = (tagName) => {
|
|
|
28626
28518
|
if (!(firstCharacter >= 97 && firstCharacter <= 122)) return false;
|
|
28627
28519
|
return !tagName.includes("-");
|
|
28628
28520
|
};
|
|
28629
|
-
const normalizeAttributeCase = (name) =>
|
|
28521
|
+
const normalizeAttributeCase = (name) => {
|
|
28522
|
+
for (const ignoreCaseName of DOM_PROPERTIES_IGNORE_CASE) if (ignoreCaseName.toLowerCase() === name.toLowerCase()) return ignoreCaseName;
|
|
28523
|
+
return name;
|
|
28524
|
+
};
|
|
28630
28525
|
const hasUppercaseChar = (input) => /[A-Z]/.test(input);
|
|
28631
28526
|
const INVALID_PROP_ON_TAG = (propName, allowedTags) => `React ignores \`${propName}\` here because it only works on these tags: ${allowedTags}.`;
|
|
28632
28527
|
const DATA_LOWERCASE_REQUIRED = () => `React drops this \`data-*\` prop because of its capital letters.`;
|
|
@@ -28970,26 +28865,32 @@ const isFirstArgumentOfHocCall = (node) => {
|
|
|
28970
28865
|
if (!isHocCallee$1(parent)) return false;
|
|
28971
28866
|
return parent.arguments[0] === node;
|
|
28972
28867
|
};
|
|
28973
|
-
const MAP_LIKE_METHOD_NAMES = new Set([
|
|
28974
|
-
"map",
|
|
28975
|
-
"forEach",
|
|
28976
|
-
"filter",
|
|
28977
|
-
"flatMap",
|
|
28978
|
-
"reduce",
|
|
28979
|
-
"reduceRight"
|
|
28980
|
-
]);
|
|
28981
28868
|
const isReturnOfMapCallback = (node) => {
|
|
28982
28869
|
const parent = node.parent;
|
|
28983
28870
|
if (!parent) return false;
|
|
28984
28871
|
if (isNodeOfType(parent, "CallExpression")) {
|
|
28985
28872
|
const callee = parent.callee;
|
|
28986
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
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);
|
|
28987
28881
|
}
|
|
28988
28882
|
if (isNodeOfType(parent, "ArrowFunctionExpression") || isNodeOfType(parent, "FunctionExpression")) {
|
|
28989
28883
|
const callbackParent = parent.parent;
|
|
28990
28884
|
if (callbackParent && isNodeOfType(callbackParent, "CallExpression")) {
|
|
28991
28885
|
const callee = callbackParent.callee;
|
|
28992
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return
|
|
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);
|
|
28993
28894
|
}
|
|
28994
28895
|
}
|
|
28995
28896
|
return false;
|
|
@@ -29025,10 +28926,10 @@ const isRenderFlowingReadReference = (identifier) => {
|
|
|
29025
28926
|
valueNode = parent;
|
|
29026
28927
|
parent = parent.parent;
|
|
29027
28928
|
continue;
|
|
29028
|
-
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") &&
|
|
28929
|
+
case "VariableDeclarator": return parent.init === valueNode && isNodeOfType(parent.id, "Identifier") && isUppercaseName(parent.id.name);
|
|
29029
28930
|
case "AssignmentExpression": {
|
|
29030
28931
|
const assignmentTarget = parent.left;
|
|
29031
|
-
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") &&
|
|
28932
|
+
return parent.right === valueNode && isNodeOfType(assignmentTarget, "Identifier") && isUppercaseName(assignmentTarget.name);
|
|
29032
28933
|
}
|
|
29033
28934
|
case "Property":
|
|
29034
28935
|
if (parent.value !== valueNode) return false;
|
|
@@ -29121,14 +29022,14 @@ const noUnstableNestedComponents = defineRule({
|
|
|
29121
29022
|
};
|
|
29122
29023
|
return {
|
|
29123
29024
|
JSXOpeningElement(node) {
|
|
29124
|
-
if (isNodeOfType(node.name, "JSXIdentifier") &&
|
|
29025
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) {
|
|
29125
29026
|
recordInstantiation(node.name, node.name.name);
|
|
29126
29027
|
return;
|
|
29127
29028
|
}
|
|
29128
29029
|
if (isNodeOfType(node.name, "JSXMemberExpression")) recordMemberChainInstantiation(node.name);
|
|
29129
29030
|
},
|
|
29130
29031
|
Identifier(node) {
|
|
29131
|
-
if (!
|
|
29032
|
+
if (!isUppercaseName(node.name)) return;
|
|
29132
29033
|
if (!isRenderFlowingReadReference(node)) return;
|
|
29133
29034
|
recordInstantiation(node, node.name);
|
|
29134
29035
|
},
|
|
@@ -30938,13 +30839,13 @@ const preferStableEmptyFallback = defineRule({
|
|
|
30938
30839
|
memoRegistry = buildSameFileMemoRegistry(node);
|
|
30939
30840
|
},
|
|
30940
30841
|
JSXAttribute(node) {
|
|
30842
|
+
if (!isInsideFunctionScope(node)) return;
|
|
30843
|
+
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30941
30844
|
if (!node.value) return;
|
|
30942
30845
|
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
30943
30846
|
const innerExpression = node.value.expression;
|
|
30944
30847
|
if (!innerExpression) return;
|
|
30945
30848
|
if (innerExpression.type === "JSXEmptyExpression") return;
|
|
30946
|
-
if (!isInsideFunctionScope(node)) return;
|
|
30947
|
-
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
30948
30849
|
const parentJsxOpening = node.parent;
|
|
30949
30850
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
30950
30851
|
if (memoStatusForJsxOpeningName(memoRegistry, openingName) !== "memoised") return;
|
|
@@ -32316,11 +32217,41 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32316
32217
|
});
|
|
32317
32218
|
return found;
|
|
32318
32219
|
};
|
|
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
|
+
};
|
|
32319
32232
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32320
32233
|
"then",
|
|
32321
32234
|
"catch",
|
|
32322
32235
|
"finally"
|
|
32323
32236
|
]);
|
|
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
|
+
};
|
|
32324
32255
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32325
32256
|
"useApolloClient",
|
|
32326
32257
|
"useMutation",
|
|
@@ -32333,36 +32264,20 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32333
32264
|
"fetch",
|
|
32334
32265
|
"axios"
|
|
32335
32266
|
]);
|
|
32336
|
-
const
|
|
32267
|
+
const referencesAsyncDataApi = (body) => {
|
|
32268
|
+
if (!body) return false;
|
|
32337
32269
|
let found = false;
|
|
32338
|
-
walkAst(
|
|
32339
|
-
if (found) return
|
|
32270
|
+
walkAst(body, (child) => {
|
|
32271
|
+
if (found) return;
|
|
32340
32272
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32341
32273
|
const callee = child.callee;
|
|
32342
32274
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32343
32275
|
found = true;
|
|
32344
|
-
return
|
|
32345
|
-
}
|
|
32346
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32347
|
-
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32348
|
-
found = true;
|
|
32349
|
-
return false;
|
|
32350
|
-
}
|
|
32351
|
-
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32352
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32353
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32354
|
-
found = true;
|
|
32355
|
-
return false;
|
|
32356
|
-
}
|
|
32357
|
-
}
|
|
32276
|
+
return;
|
|
32358
32277
|
}
|
|
32359
|
-
|
|
32360
|
-
}
|
|
32361
|
-
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32362
|
-
const functionBody = child.body;
|
|
32363
|
-
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32278
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32364
32279
|
found = true;
|
|
32365
|
-
return
|
|
32280
|
+
return;
|
|
32366
32281
|
}
|
|
32367
32282
|
}
|
|
32368
32283
|
});
|
|
@@ -32387,7 +32302,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32387
32302
|
const secondBinding = node.id.elements[1];
|
|
32388
32303
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32389
32304
|
const fnBody = enclosingFunctionBody(node);
|
|
32390
|
-
if (fnBody &&
|
|
32305
|
+
if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
|
|
32391
32306
|
context.report({
|
|
32392
32307
|
node: node.init,
|
|
32393
32308
|
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`
|
|
@@ -33181,17 +33096,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33181
33096
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33182
33097
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33183
33098
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33184
|
-
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33185
|
-
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33186
|
-
walkAst(componentBody, (child) => {
|
|
33187
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33188
|
-
});
|
|
33189
33099
|
for (const binding of bindings) {
|
|
33190
33100
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33191
33101
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33192
33102
|
const setterSuffix = binding.setterName.slice(3);
|
|
33193
33103
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33194
|
-
|
|
33104
|
+
let setterCalled = false;
|
|
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;
|
|
33195
33110
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33196
33111
|
context.report({
|
|
33197
33112
|
node: binding.declarator,
|
|
@@ -33523,7 +33438,6 @@ const RECYCLABLE_LIST_PACKAGES = {
|
|
|
33523
33438
|
FlashList: ["@shopify/flash-list"],
|
|
33524
33439
|
LegendList: ["@legendapp/list"]
|
|
33525
33440
|
};
|
|
33526
|
-
const RECYCLABLE_LIST_PACKAGE_SOURCES = Object.values(RECYCLABLE_LIST_PACKAGES).flat();
|
|
33527
33441
|
const REACT_NATIVE_LIST_COMPONENTS = new Set([...REACT_NATIVE_BUILTIN_LIST_COMPONENTS, ...Object.keys(RECYCLABLE_LIST_PACKAGES)]);
|
|
33528
33442
|
const RENDER_ITEM_PROP_NAMES = new Set([
|
|
33529
33443
|
"renderItem",
|
|
@@ -33766,42 +33680,33 @@ const rnListMissingEstimatedItemSize = defineRule({
|
|
|
33766
33680
|
requires: ["react-native"],
|
|
33767
33681
|
severity: "warn",
|
|
33768
33682
|
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.",
|
|
33769
|
-
create: (context) => {
|
|
33770
|
-
|
|
33771
|
-
return
|
|
33772
|
-
|
|
33773
|
-
|
|
33774
|
-
|
|
33775
|
-
|
|
33776
|
-
|
|
33777
|
-
|
|
33778
|
-
|
|
33779
|
-
|
|
33780
|
-
|
|
33781
|
-
|
|
33782
|
-
|
|
33783
|
-
|
|
33784
|
-
|
|
33785
|
-
|
|
33786
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
33787
|
-
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
33788
|
-
const attributeName = attribute.name.name;
|
|
33789
|
-
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
33790
|
-
if (attributeName === "data") {
|
|
33791
|
-
hasDataProp = true;
|
|
33792
|
-
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
33793
|
-
}
|
|
33794
|
-
}
|
|
33795
|
-
if (hasSizingHint) return;
|
|
33796
|
-
if (dataIsEmptyLiteral) return;
|
|
33797
|
-
if (!hasDataProp) return;
|
|
33798
|
-
context.report({
|
|
33799
|
-
node,
|
|
33800
|
-
message: `Your users see blank cells flash on fast scroll when <${localElementName}> has no \`estimatedItemSize\`.`
|
|
33801
|
-
});
|
|
33683
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
33684
|
+
const localElementName = resolveJsxElementName(node);
|
|
33685
|
+
if (!localElementName) return;
|
|
33686
|
+
const canonicalRecyclerName = resolveImportedRecyclerName(node, localElementName);
|
|
33687
|
+
if (canonicalRecyclerName === null) return;
|
|
33688
|
+
if (canonicalRecyclerName === "FlashList" && isFlashListV2OrNewer(context)) return;
|
|
33689
|
+
let hasSizingHint = false;
|
|
33690
|
+
let dataIsEmptyLiteral = false;
|
|
33691
|
+
let hasDataProp = false;
|
|
33692
|
+
for (const attribute of node.attributes ?? []) {
|
|
33693
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
33694
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
33695
|
+
const attributeName = attribute.name.name;
|
|
33696
|
+
if (SIZING_HINT_ATTRIBUTE_NAMES.has(attributeName)) hasSizingHint = true;
|
|
33697
|
+
if (attributeName === "data") {
|
|
33698
|
+
hasDataProp = true;
|
|
33699
|
+
if (isEmptyArrayLiteral(attribute)) dataIsEmptyLiteral = true;
|
|
33802
33700
|
}
|
|
33803
|
-
}
|
|
33804
|
-
|
|
33701
|
+
}
|
|
33702
|
+
if (hasSizingHint) return;
|
|
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
|
+
} })
|
|
33805
33710
|
});
|
|
33806
33711
|
//#endregion
|
|
33807
33712
|
//#region src/plugin/rules/react-native/rn-list-recyclable-without-types.ts
|
|
@@ -33812,34 +33717,25 @@ const rnListRecyclableWithoutTypes = defineRule({
|
|
|
33812
33717
|
requires: ["react-native"],
|
|
33813
33718
|
severity: "warn",
|
|
33814
33719
|
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.",
|
|
33815
|
-
create: (context) => {
|
|
33816
|
-
|
|
33817
|
-
return
|
|
33818
|
-
|
|
33819
|
-
|
|
33820
|
-
|
|
33821
|
-
|
|
33822
|
-
|
|
33823
|
-
|
|
33824
|
-
|
|
33825
|
-
|
|
33826
|
-
|
|
33827
|
-
|
|
33828
|
-
|
|
33829
|
-
|
|
33830
|
-
|
|
33831
|
-
|
|
33832
|
-
|
|
33833
|
-
|
|
33834
|
-
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
33835
|
-
}
|
|
33836
|
-
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
33837
|
-
node,
|
|
33838
|
-
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
33839
|
-
});
|
|
33840
|
-
}
|
|
33841
|
-
};
|
|
33842
|
-
}
|
|
33720
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
33721
|
+
const elementName = resolveJsxElementName(node);
|
|
33722
|
+
if (!elementName) return;
|
|
33723
|
+
if (resolveImportedRecyclerName(node, elementName, { allowNamespaceMemberAccess: true }) === null) return;
|
|
33724
|
+
let hasRecycleItemsEnabled = false;
|
|
33725
|
+
let hasGetItemType = false;
|
|
33726
|
+
for (const attr of node.attributes ?? []) {
|
|
33727
|
+
if (!isNodeOfType(attr, "JSXAttribute")) continue;
|
|
33728
|
+
if (!isNodeOfType(attr.name, "JSXIdentifier")) continue;
|
|
33729
|
+
if (attr.name.name === "recycleItems") if (!attr.value) hasRecycleItemsEnabled = true;
|
|
33730
|
+
else if (isNodeOfType(attr.value, "JSXExpressionContainer") && isNodeOfType(attr.value.expression, "Literal")) hasRecycleItemsEnabled = attr.value.expression.value === true;
|
|
33731
|
+
else hasRecycleItemsEnabled = true;
|
|
33732
|
+
if (attr.name.name === "getItemType") hasGetItemType = true;
|
|
33733
|
+
}
|
|
33734
|
+
if (hasRecycleItemsEnabled && !hasGetItemType) context.report({
|
|
33735
|
+
node,
|
|
33736
|
+
message: `Your users see rows of different shapes reuse the wrong cells when <${elementName} recycleItems> has no \`getItemType\`.`
|
|
33737
|
+
});
|
|
33738
|
+
} })
|
|
33843
33739
|
});
|
|
33844
33740
|
//#endregion
|
|
33845
33741
|
//#region src/plugin/rules/react-native/rn-no-deep-imports.ts
|
|
@@ -34823,10 +34719,9 @@ const resolveTextBoundaryName = (openingElement) => {
|
|
|
34823
34719
|
if (isNodeOfType(openingElement.name, "JSXNamespacedName")) return openingElement.name.namespace.name;
|
|
34824
34720
|
return resolveJsxElementName(openingElement);
|
|
34825
34721
|
};
|
|
34826
|
-
const TEXT_COMPONENT_KEYWORDS = [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS];
|
|
34827
34722
|
const isTextHandlingComponent = (elementName) => {
|
|
34828
34723
|
if (REACT_NATIVE_TEXT_COMPONENTS.has(elementName)) return true;
|
|
34829
|
-
return
|
|
34724
|
+
return [...REACT_NATIVE_TEXT_COMPONENT_KEYWORDS].some((keyword) => elementName.includes(keyword));
|
|
34830
34725
|
};
|
|
34831
34726
|
const isTransparentTextWrapper = (elementName) => elementName !== null && REACT_NATIVE_TEXT_TRANSPARENT_COMPONENTS.has(elementName);
|
|
34832
34727
|
const isInsideTextHandlingComponent = (node) => {
|
|
@@ -34870,7 +34765,6 @@ const rnNoRawText = defineRule({
|
|
|
34870
34765
|
return {
|
|
34871
34766
|
Program(programNode) {
|
|
34872
34767
|
isDomComponentFile = hasDirective(programNode, "use dom");
|
|
34873
|
-
if (!containsJsxElement(programNode)) return;
|
|
34874
34768
|
const childrenForwarding = collectTextWrapperComponents(programNode, isTextHandlingComponent, isNonTextHostName);
|
|
34875
34769
|
autoDetectedTextWrappers = childrenForwarding.textWrappers;
|
|
34876
34770
|
autoDetectedNonTextWrappers = childrenForwarding.nonTextWrappers;
|
|
@@ -38825,7 +38719,14 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38825
38719
|
recommendation: "Only use `aria-*` attributes that the element's role supports.",
|
|
38826
38720
|
category: "Accessibility",
|
|
38827
38721
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
38828
|
-
|
|
38722
|
+
const elementType = getElementType(node, context.settings);
|
|
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;
|
|
38829
38730
|
for (const attribute of node.attributes) {
|
|
38830
38731
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
38831
38732
|
const attributeNode = attribute;
|
|
@@ -38835,21 +38736,6 @@ const roleSupportsAriaProps = defineRule({
|
|
|
38835
38736
|
const propName = propRawName.toLowerCase();
|
|
38836
38737
|
if (!propName.startsWith("aria-")) continue;
|
|
38837
38738
|
if (!ARIA_PROPERTIES.has(propName)) continue;
|
|
38838
|
-
(ariaAttributes ??= []).push({
|
|
38839
|
-
attribute,
|
|
38840
|
-
propName
|
|
38841
|
-
});
|
|
38842
|
-
}
|
|
38843
|
-
if (!ariaAttributes) return;
|
|
38844
|
-
const elementType = getElementType(node, context.settings);
|
|
38845
|
-
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
38846
|
-
const role = roleAttribute ? getJsxPropStringValue(roleAttribute) : getImplicitRole(node, elementType);
|
|
38847
|
-
if (!role) return;
|
|
38848
|
-
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
38849
|
-
const isImplicit = !roleAttribute;
|
|
38850
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
38851
|
-
if (!supported) return;
|
|
38852
|
-
for (const { attribute, propName } of ariaAttributes) {
|
|
38853
38739
|
if (supported.has(propName)) continue;
|
|
38854
38740
|
context.report({
|
|
38855
38741
|
node: attribute,
|
|
@@ -39687,7 +39573,6 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
39687
39573
|
cachedFunctionNames.add(node.id.name);
|
|
39688
39574
|
},
|
|
39689
39575
|
CallExpression(node) {
|
|
39690
|
-
if (cachedFunctionNames.size === 0) return;
|
|
39691
39576
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
39692
39577
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
39693
39578
|
const firstArg = node.arguments?.[0];
|
|
@@ -40614,16 +40499,7 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40614
40499
|
//#endregion
|
|
40615
40500
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40616
40501
|
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;
|
|
40617
|
-
const
|
|
40618
|
-
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40619
|
-
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40620
|
-
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40621
|
-
const tableName = match[1];
|
|
40622
|
-
if (tableName === void 0) continue;
|
|
40623
|
-
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40624
|
-
}
|
|
40625
|
-
return lastEnableIndexByTable;
|
|
40626
|
-
};
|
|
40502
|
+
const enableRlsForTablePattern = (tableName) => new RegExp(`alter\\s+table\\s+(?:if\\s+exists\\s+)?(?:only\\s+)?(?:public\\s*\\.\\s*)?["\`]?${escapeRegExp(tableName)}["\`]?\\s+(?:force\\s+)?enable\\s+row\\s+level\\s+security`, "i");
|
|
40627
40503
|
const supabaseTableMissingRls = defineRule({
|
|
40628
40504
|
id: "supabase-table-missing-rls",
|
|
40629
40505
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40634,13 +40510,11 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40634
40510
|
const content = sanitizeSqlForScan(file.content);
|
|
40635
40511
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40636
40512
|
const findings = [];
|
|
40637
|
-
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40638
40513
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40639
40514
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40640
40515
|
const tableName = match[1];
|
|
40641
40516
|
if (tableName === void 0) continue;
|
|
40642
|
-
|
|
40643
|
-
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40517
|
+
if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
|
|
40644
40518
|
const location = getLocationAtIndex(content, match.index);
|
|
40645
40519
|
findings.push({
|
|
40646
40520
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -41330,10 +41204,10 @@ const tanstackStartRoutePropertyOrder = defineRule({
|
|
|
41330
41204
|
const propertyName = getPropertyKeyName(property);
|
|
41331
41205
|
if (propertyName !== null) orderedPropertyNames.push(propertyName);
|
|
41332
41206
|
}
|
|
41333
|
-
const sensitiveProperties = orderedPropertyNames.filter((propertyName) =>
|
|
41207
|
+
const sensitiveProperties = orderedPropertyNames.filter((propertyName) => TANSTACK_ROUTE_PROPERTY_ORDER.includes(propertyName));
|
|
41334
41208
|
let lastIndex = -1;
|
|
41335
41209
|
for (const propertyName of sensitiveProperties) {
|
|
41336
|
-
const currentIndex =
|
|
41210
|
+
const currentIndex = TANSTACK_ROUTE_PROPERTY_ORDER.indexOf(propertyName);
|
|
41337
41211
|
if (currentIndex < lastIndex) {
|
|
41338
41212
|
const expectedBefore = TANSTACK_ROUTE_PROPERTY_ORDER[lastIndex];
|
|
41339
41213
|
context.report({
|
|
@@ -41370,10 +41244,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
|
|
|
41370
41244
|
} else return;
|
|
41371
41245
|
const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
|
|
41372
41246
|
if (methodNames[methodNames.length - 1] !== ownMethodName) return;
|
|
41373
|
-
const orderSensitiveMethods = methodNames.filter((name) =>
|
|
41247
|
+
const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
|
|
41374
41248
|
let lastIndex = -1;
|
|
41375
41249
|
for (const methodName of orderSensitiveMethods) {
|
|
41376
|
-
const currentIndex =
|
|
41250
|
+
const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
|
|
41377
41251
|
if (currentIndex < lastIndex) {
|
|
41378
41252
|
const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
|
|
41379
41253
|
context.report({
|
|
@@ -41657,21 +41531,13 @@ const webhookSignatureRisk = defineRule({
|
|
|
41657
41531
|
//#endregion
|
|
41658
41532
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
41659
41533
|
const ZOD_MODULE = "zod";
|
|
41660
|
-
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
41661
41534
|
const getStaticPropertyName = (member) => {
|
|
41662
41535
|
const property = member.property;
|
|
41663
41536
|
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
41664
41537
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41665
41538
|
return null;
|
|
41666
41539
|
};
|
|
41667
|
-
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41668
41540
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41669
|
-
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41670
|
-
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41671
|
-
importInfoCache.set(identifier, importInfo);
|
|
41672
|
-
return importInfo;
|
|
41673
|
-
};
|
|
41674
|
-
const computeImportInfoForIdentifier = (identifier) => {
|
|
41675
41541
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41676
41542
|
if (!specifier) return null;
|
|
41677
41543
|
const declaration = specifier.parent;
|
|
@@ -41808,33 +41674,25 @@ const zodV4NoDeprecatedErrorApis = defineRule({
|
|
|
41808
41674
|
tags: ["migration-hint"],
|
|
41809
41675
|
severity: "warn",
|
|
41810
41676
|
recommendation: "Use the Zod 4 helpers instead: `z.treeifyError()`, `z.flattenError()`, `z.prettifyError()`, or read `error.issues` directly.",
|
|
41811
|
-
create: (context) => {
|
|
41812
|
-
|
|
41813
|
-
|
|
41814
|
-
|
|
41815
|
-
|
|
41816
|
-
|
|
41817
|
-
|
|
41818
|
-
|
|
41819
|
-
|
|
41820
|
-
|
|
41821
|
-
|
|
41822
|
-
|
|
41823
|
-
|
|
41824
|
-
|
|
41825
|
-
|
|
41826
|
-
|
|
41827
|
-
|
|
41828
|
-
|
|
41829
|
-
|
|
41830
|
-
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
41831
|
-
context.report({
|
|
41832
|
-
node,
|
|
41833
|
-
message: ZOD_ERROR_API_MESSAGE
|
|
41834
|
-
});
|
|
41835
|
-
}
|
|
41836
|
-
};
|
|
41837
|
-
}
|
|
41677
|
+
create: (context) => ({
|
|
41678
|
+
CallExpression(node) {
|
|
41679
|
+
if (isZodErrorCreateCall(node) && isReceiverOfDeprecatedZodErrorMember(node)) return;
|
|
41680
|
+
if (!isZodErrorCreateCall(node) && !isDeprecatedZodErrorMemberAccess(node.callee)) return;
|
|
41681
|
+
context.report({
|
|
41682
|
+
node,
|
|
41683
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41684
|
+
});
|
|
41685
|
+
},
|
|
41686
|
+
MemberExpression(node) {
|
|
41687
|
+
const parent = node.parent;
|
|
41688
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41689
|
+
if (!isDeprecatedZodErrorMemberAccess(node)) return;
|
|
41690
|
+
context.report({
|
|
41691
|
+
node,
|
|
41692
|
+
message: ZOD_ERROR_API_MESSAGE
|
|
41693
|
+
});
|
|
41694
|
+
}
|
|
41695
|
+
})
|
|
41838
41696
|
});
|
|
41839
41697
|
//#endregion
|
|
41840
41698
|
//#region src/plugin/rules/zod/zod-v4-no-deprecated-error-customization.ts
|
|
@@ -42026,24 +41884,16 @@ const zodV4NoDeprecatedSchemaApis = defineRule({
|
|
|
42026
41884
|
tags: ["migration-hint"],
|
|
42027
41885
|
severity: "warn",
|
|
42028
41886
|
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()`.",
|
|
42029
|
-
create: (context) => {
|
|
42030
|
-
|
|
42031
|
-
|
|
42032
|
-
|
|
42033
|
-
|
|
42034
|
-
|
|
42035
|
-
CallExpression(node)
|
|
42036
|
-
|
|
42037
|
-
|
|
42038
|
-
|
|
42039
|
-
MemberExpression(node) {
|
|
42040
|
-
if (!fileImportsZod) return;
|
|
42041
|
-
const parent = node.parent;
|
|
42042
|
-
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
42043
|
-
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
42044
|
-
}
|
|
42045
|
-
};
|
|
42046
|
-
}
|
|
41887
|
+
create: (context) => ({
|
|
41888
|
+
CallExpression(node) {
|
|
41889
|
+
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);
|
|
41890
|
+
},
|
|
41891
|
+
MemberExpression(node) {
|
|
41892
|
+
const parent = node.parent;
|
|
41893
|
+
if (parent && isNodeOfType(parent, "CallExpression") && stripParenExpression(parent.callee) === node) return;
|
|
41894
|
+
if (isDroppedEnumAliasAccess(node) || isZodNamespaceImportMemberCreate(node)) reportSchemaMigration(context, node);
|
|
41895
|
+
}
|
|
41896
|
+
})
|
|
42047
41897
|
});
|
|
42048
41898
|
//#endregion
|
|
42049
41899
|
//#region src/plugin/rules/zod/zod-v4-prefer-top-level-string-formats.ts
|
|
@@ -42078,22 +41928,13 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
42078
41928
|
tags: ["migration-hint"],
|
|
42079
41929
|
severity: "warn",
|
|
42080
41930
|
recommendation: "Use the Zod 4 top-level format checks like `z.email()`, `z.uuid()`, or `z.ipv4()` instead of `z.string().<format>()`.",
|
|
42081
|
-
create: (context) => {
|
|
42082
|
-
|
|
42083
|
-
|
|
42084
|
-
|
|
42085
|
-
|
|
42086
|
-
|
|
42087
|
-
|
|
42088
|
-
if (!fileImportsZod) return;
|
|
42089
|
-
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
42090
|
-
context.report({
|
|
42091
|
-
node,
|
|
42092
|
-
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
42093
|
-
});
|
|
42094
|
-
}
|
|
42095
|
-
};
|
|
42096
|
-
}
|
|
41931
|
+
create: (context) => ({ CallExpression(node) {
|
|
41932
|
+
if (!isDirectMethodCallOnZodFactory(node, ZOD_STRING_FACTORY, STRING_FORMAT_METHODS)) return;
|
|
41933
|
+
context.report({
|
|
41934
|
+
node,
|
|
41935
|
+
message: "This `z.string().<format>()` check is deprecated in Zod 4, so it can break during the upgrade."
|
|
41936
|
+
});
|
|
41937
|
+
} })
|
|
42097
41938
|
});
|
|
42098
41939
|
//#endregion
|
|
42099
41940
|
//#region src/plugin/rule-registry.ts
|