oxlint-plugin-react-doctor 0.6.2-dev.d8628d7 → 0.6.2-dev.fa61c20
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 +270 -323
- 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}>`);
|
|
@@ -1682,7 +1667,7 @@ const isNextjsMetadataImageRouteFilename = (rawFilename) => {
|
|
|
1682
1667
|
};
|
|
1683
1668
|
//#endregion
|
|
1684
1669
|
//#region src/plugin/utils/normalize-filename.ts
|
|
1685
|
-
const normalizeFilename = (filename) => filename.
|
|
1670
|
+
const normalizeFilename = (filename) => filename.replaceAll("\\", "/");
|
|
1686
1671
|
//#endregion
|
|
1687
1672
|
//#region src/plugin/utils/is-generated-image-render-context.ts
|
|
1688
1673
|
const IMAGE_RESPONSE_MODULES = ["next/og", "@vercel/og"];
|
|
@@ -3647,9 +3632,8 @@ const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
|
3647
3632
|
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3648
3633
|
//#endregion
|
|
3649
3634
|
//#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
3635
|
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3652
|
-
for (const match of content.matchAll(
|
|
3636
|
+
for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
|
|
3653
3637
|
const value = match[0] ?? "";
|
|
3654
3638
|
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3655
3639
|
}
|
|
@@ -4116,56 +4100,62 @@ const collectPatternIdentifiers = (pattern, target) => {
|
|
|
4116
4100
|
for (const element of pattern.elements ?? []) if (element) collectPatternIdentifiers(element, target);
|
|
4117
4101
|
} else if (isNodeOfType(pattern, "AssignmentPattern") && pattern.left) collectPatternIdentifiers(pattern.left, target);
|
|
4118
4102
|
};
|
|
4103
|
+
const collectAssignedIdentifiers = (block) => {
|
|
4104
|
+
const assigned = /* @__PURE__ */ new Set();
|
|
4105
|
+
walkAst(block, (child) => {
|
|
4106
|
+
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4107
|
+
if (isNodeOfType(child, "AssignmentExpression") && child.left) collectPatternIdentifiers(child.left, assigned);
|
|
4108
|
+
});
|
|
4109
|
+
return assigned;
|
|
4110
|
+
};
|
|
4111
|
+
const collectAwaitedArgIdentifiers = (block) => {
|
|
4112
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4113
|
+
walkAst(block, (child) => {
|
|
4114
|
+
if (isInlineFunctionExpression(child) || isNodeOfType(child, "FunctionDeclaration")) return false;
|
|
4115
|
+
if (!isNodeOfType(child, "AwaitExpression") || !child.argument) return;
|
|
4116
|
+
collectReferenceIdentifierNames(child.argument, referenced);
|
|
4117
|
+
});
|
|
4118
|
+
return referenced;
|
|
4119
|
+
};
|
|
4119
4120
|
const ARRAY_MUTATION_METHOD_NAMES = new Set([
|
|
4120
4121
|
"push",
|
|
4121
4122
|
"unshift",
|
|
4122
4123
|
"splice"
|
|
4123
4124
|
]);
|
|
4124
|
-
const
|
|
4125
|
-
const
|
|
4125
|
+
const collectMutatedArrayNames = (block) => {
|
|
4126
|
+
const mutated = /* @__PURE__ */ new Set();
|
|
4126
4127
|
walkAst(block, (child) => {
|
|
4127
4128
|
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
|
-
});
|
|
4129
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
4130
|
+
const callee = child.callee;
|
|
4131
|
+
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
4132
|
});
|
|
4133
|
+
return mutated;
|
|
4134
|
+
};
|
|
4135
|
+
const addDerivedBindings = (block, names) => {
|
|
4137
4136
|
let didGrow = true;
|
|
4138
4137
|
while (didGrow) {
|
|
4139
4138
|
didGrow = false;
|
|
4140
|
-
|
|
4141
|
-
if (
|
|
4142
|
-
|
|
4143
|
-
|
|
4139
|
+
walkAst(block, (child) => {
|
|
4140
|
+
if (child !== block && isFunctionLike$1(child)) return false;
|
|
4141
|
+
if (!isNodeOfType(child, "VariableDeclarator") || !child.init) return;
|
|
4142
|
+
if (!isNodeOfType(child.id, "Identifier") || names.has(child.id.name)) return;
|
|
4143
|
+
const initReferences = /* @__PURE__ */ new Set();
|
|
4144
|
+
collectReferenceIdentifierNames(child.init, initReferences);
|
|
4145
|
+
for (const referenced of initReferences) if (names.has(referenced)) {
|
|
4146
|
+
names.add(child.id.name);
|
|
4144
4147
|
didGrow = true;
|
|
4145
4148
|
break;
|
|
4146
4149
|
}
|
|
4147
|
-
}
|
|
4150
|
+
});
|
|
4148
4151
|
}
|
|
4149
4152
|
};
|
|
4150
4153
|
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
|
-
});
|
|
4154
|
+
const carried = collectAssignedIdentifiers(block);
|
|
4155
|
+
for (const name of collectMutatedArrayNames(block)) carried.add(name);
|
|
4167
4156
|
if (carried.size === 0) return false;
|
|
4168
4157
|
addDerivedBindings(block, carried);
|
|
4158
|
+
const awaitedReferences = collectAwaitedArgIdentifiers(block);
|
|
4169
4159
|
for (const name of carried) if (awaitedReferences.has(name)) return true;
|
|
4170
4160
|
return false;
|
|
4171
4161
|
};
|
|
@@ -5718,20 +5708,12 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
5718
5708
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
5719
5709
|
};
|
|
5720
5710
|
const isInertParseTarget = (target, fileContent) => {
|
|
5721
|
-
const fileHasCreateElement = fileContent.includes("createElement");
|
|
5722
|
-
const fileHasIsolatedDocument = fileContent.includes("createHTMLDocument");
|
|
5723
|
-
if (!fileHasCreateElement && !fileHasIsolatedDocument) return false;
|
|
5724
5711
|
const escapedTarget = escapeRegExp(target);
|
|
5725
5712
|
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
5726
5713
|
if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
|
|
5727
|
-
if (
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
}
|
|
5731
|
-
if (fileHasIsolatedDocument) {
|
|
5732
|
-
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5733
|
-
}
|
|
5734
|
-
if (!fileHasCreateElement) return false;
|
|
5714
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
5715
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
5716
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
5735
5717
|
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
5736
5718
|
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
5737
5719
|
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
@@ -5824,9 +5806,10 @@ const dangerousHtmlSink = defineRule({
|
|
|
5824
5806
|
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
5825
5807
|
if (valueIdentifier !== void 0) {
|
|
5826
5808
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
5827
|
-
|
|
5828
|
-
|
|
5829
|
-
|
|
5809
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
5810
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
5811
|
+
const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
|
|
5812
|
+
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
5830
5813
|
}
|
|
5831
5814
|
}
|
|
5832
5815
|
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
@@ -6217,10 +6200,7 @@ const isReactVersionAtLeast$1 = (version, major, minor) => {
|
|
|
6217
6200
|
const actualMinor = Number(match[2]);
|
|
6218
6201
|
return actualMajor > major || actualMajor === major && actualMinor >= minor;
|
|
6219
6202
|
};
|
|
6220
|
-
const containsJsxCache = /* @__PURE__ */ new WeakMap();
|
|
6221
6203
|
const containsJsx$1 = (root) => {
|
|
6222
|
-
const cached = containsJsxCache.get(root);
|
|
6223
|
-
if (cached !== void 0) return cached;
|
|
6224
6204
|
let found = false;
|
|
6225
6205
|
const visit = (node) => {
|
|
6226
6206
|
if (found) return;
|
|
@@ -6243,7 +6223,6 @@ const containsJsx$1 = (root) => {
|
|
|
6243
6223
|
}
|
|
6244
6224
|
};
|
|
6245
6225
|
visit(root);
|
|
6246
|
-
containsJsxCache.set(root, found);
|
|
6247
6226
|
return found;
|
|
6248
6227
|
};
|
|
6249
6228
|
const getStaticMemberName = (node) => {
|
|
@@ -6335,6 +6314,27 @@ const hasDisplayNameMember = (classNode) => {
|
|
|
6335
6314
|
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;
|
|
6336
6315
|
return false;
|
|
6337
6316
|
};
|
|
6317
|
+
const hasDisplayNameAssignment = (className, programRoot) => {
|
|
6318
|
+
let found = false;
|
|
6319
|
+
const visit = (node) => {
|
|
6320
|
+
if (found) return;
|
|
6321
|
+
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && isNodeOfType(node.left.object, "Identifier") && node.left.object.name === className && getStaticMemberName(node.left) === "displayName") {
|
|
6322
|
+
found = true;
|
|
6323
|
+
return;
|
|
6324
|
+
}
|
|
6325
|
+
const record = node;
|
|
6326
|
+
for (const key of Object.keys(record)) {
|
|
6327
|
+
if (key === "parent") continue;
|
|
6328
|
+
const child = record[key];
|
|
6329
|
+
if (Array.isArray(child)) {
|
|
6330
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6331
|
+
} else if (isAstNode(child)) visit(child);
|
|
6332
|
+
if (found) return;
|
|
6333
|
+
}
|
|
6334
|
+
};
|
|
6335
|
+
visit(programRoot);
|
|
6336
|
+
return found;
|
|
6337
|
+
};
|
|
6338
6338
|
const memberExpressionPath = (node) => {
|
|
6339
6339
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
6340
6340
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
@@ -6342,16 +6342,15 @@ const memberExpressionPath = (node) => {
|
|
|
6342
6342
|
const propertyName = getStaticMemberName(node);
|
|
6343
6343
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
6344
6344
|
};
|
|
6345
|
-
const
|
|
6346
|
-
|
|
6347
|
-
const cached = displayNameAssignmentIndexCache.get(programRoot);
|
|
6348
|
-
if (cached) return cached;
|
|
6349
|
-
const identifierTargets = /* @__PURE__ */ new Set();
|
|
6350
|
-
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
6345
|
+
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => {
|
|
6346
|
+
let found = false;
|
|
6351
6347
|
const visit = (node) => {
|
|
6348
|
+
if (found) return;
|
|
6352
6349
|
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName(node.left) === "displayName") {
|
|
6353
|
-
if (
|
|
6354
|
-
|
|
6350
|
+
if (memberExpressionPath(node.left.object).includes(propertyName)) {
|
|
6351
|
+
found = true;
|
|
6352
|
+
return;
|
|
6353
|
+
}
|
|
6355
6354
|
}
|
|
6356
6355
|
const record = node;
|
|
6357
6356
|
for (const key of Object.keys(record)) {
|
|
@@ -6360,18 +6359,12 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
|
6360
6359
|
if (Array.isArray(child)) {
|
|
6361
6360
|
for (const item of child) if (isAstNode(item)) visit(item);
|
|
6362
6361
|
} else if (isAstNode(child)) visit(child);
|
|
6362
|
+
if (found) return;
|
|
6363
6363
|
}
|
|
6364
6364
|
};
|
|
6365
6365
|
visit(programRoot);
|
|
6366
|
-
|
|
6367
|
-
identifierTargets,
|
|
6368
|
-
memberPathSegments
|
|
6369
|
-
};
|
|
6370
|
-
displayNameAssignmentIndexCache.set(programRoot, index);
|
|
6371
|
-
return index;
|
|
6366
|
+
return found;
|
|
6372
6367
|
};
|
|
6373
|
-
const hasDisplayNameAssignment = (className, programRoot) => getDisplayNameAssignmentIndex(programRoot).identifierTargets.has(className);
|
|
6374
|
-
const hasDisplayNameAssignmentForProperty = (propertyName, programRoot) => getDisplayNameAssignmentIndex(programRoot).memberPathSegments.has(propertyName);
|
|
6375
6368
|
const displayName = defineRule({
|
|
6376
6369
|
id: "display-name",
|
|
6377
6370
|
title: "Component missing display name",
|
|
@@ -10257,6 +10250,8 @@ const jsCachePropertyAccess = defineRule({
|
|
|
10257
10250
|
walkAst(loopBody, (child) => {
|
|
10258
10251
|
if (isNodeOfType(child, "AssignmentExpression")) recordWriteTarget(child.left);
|
|
10259
10252
|
if (isNodeOfType(child, "UpdateExpression")) recordWriteTarget(child.argument);
|
|
10253
|
+
});
|
|
10254
|
+
walkAst(loopBody, (child) => {
|
|
10260
10255
|
if (!isNodeOfType(child, "MemberExpression")) return;
|
|
10261
10256
|
if (child.computed) return;
|
|
10262
10257
|
if (isNodeOfType(child.parent, "MemberExpression") && child.parent.object === child) return;
|
|
@@ -10807,11 +10802,7 @@ const isSingleFieldEqualityPredicate = (node) => {
|
|
|
10807
10802
|
if (!isNodeOfType(predicate, "BinaryExpression") || predicate.operator !== "===" && predicate.operator !== "==") return false;
|
|
10808
10803
|
return referencesParameter(predicate.left, firstParameter.name) || referencesParameter(predicate.right, firstParameter.name);
|
|
10809
10804
|
};
|
|
10810
|
-
const
|
|
10811
|
-
const getLoopBoundNames = (loop) => {
|
|
10812
|
-
const cached = loopBoundNamesCache.get(loop);
|
|
10813
|
-
if (cached) return cached;
|
|
10814
|
-
const names = /* @__PURE__ */ new Set();
|
|
10805
|
+
const collectLoopBoundNames = (loop, names) => {
|
|
10815
10806
|
if ((isNodeOfType(loop, "ForOfStatement") || isNodeOfType(loop, "ForInStatement")) && loop.left) walkAst(loop.left, (child) => {
|
|
10816
10807
|
if (isNodeOfType(child, "Identifier")) names.add(child.name);
|
|
10817
10808
|
});
|
|
@@ -10828,8 +10819,6 @@ const getLoopBoundNames = (loop) => {
|
|
|
10828
10819
|
if (targetRoot) names.add(targetRoot);
|
|
10829
10820
|
}
|
|
10830
10821
|
});
|
|
10831
|
-
loopBoundNamesCache.set(loop, names);
|
|
10832
|
-
return names;
|
|
10833
10822
|
};
|
|
10834
10823
|
const hasLoopBoundComputedIndex = (receiver, loopBoundNames) => {
|
|
10835
10824
|
let cursor = receiver;
|
|
@@ -10855,7 +10844,7 @@ const isLoopVariantReceiver = (node) => {
|
|
|
10855
10844
|
const loopBoundNames = /* @__PURE__ */ new Set();
|
|
10856
10845
|
let ancestor = node.parent;
|
|
10857
10846
|
while (ancestor) {
|
|
10858
|
-
if (LOOP_TYPES.includes(ancestor.type))
|
|
10847
|
+
if (LOOP_TYPES.includes(ancestor.type)) collectLoopBoundNames(ancestor, loopBoundNames);
|
|
10859
10848
|
ancestor = ancestor.parent;
|
|
10860
10849
|
}
|
|
10861
10850
|
if (loopBoundNames.has(receiverRoot)) return true;
|
|
@@ -15526,30 +15515,16 @@ const stripJsComments = (sourceText) => sourceText.replace(BLOCK_COMMENT_PATTERN
|
|
|
15526
15515
|
const DEFAULT_EXPORT_DECLARATION_PATTERN = /^\s*export\s+default\b/m;
|
|
15527
15516
|
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;
|
|
15528
15517
|
const LOCAL_EXPORT_SPECIFIER_DECLARATION_PATTERN$1 = /^\s*export\s+(?:type\s+)?\{([\s\S]*?)\}(?:\s+from\s+["'][^"']+["'])?\s*;?\s*(?:(?:\/\/[^\n]*)?\s*)/gm;
|
|
15529
|
-
const
|
|
15518
|
+
const doesSourceTextExportName = (sourceText, exportedName) => {
|
|
15530
15519
|
const strippedSource = stripJsComments(sourceText);
|
|
15531
|
-
|
|
15532
|
-
|
|
15533
|
-
for (const match of strippedSource.matchAll(
|
|
15534
|
-
|
|
15535
|
-
const specifiersText = match[1] ?? "";
|
|
15536
|
-
for (const specifier of parseExportSpecifiers(specifiersText, false)) exportedNames.add(specifier.exportedName);
|
|
15537
|
-
}
|
|
15538
|
-
return exportedNames;
|
|
15520
|
+
if (exportedName === "default" && DEFAULT_EXPORT_DECLARATION_PATTERN.test(strippedSource)) return true;
|
|
15521
|
+
for (const match of strippedSource.matchAll(NAMED_EXPORT_DECLARATION_PATTERN)) if (match[1] === exportedName) return true;
|
|
15522
|
+
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;
|
|
15523
|
+
return false;
|
|
15539
15524
|
};
|
|
15540
|
-
const exportNamesCache = /* @__PURE__ */ new Map();
|
|
15541
15525
|
const doesModuleExportName = (filePath, exportedName) => {
|
|
15542
15526
|
try {
|
|
15543
|
-
|
|
15544
|
-
const cached = exportNamesCache.get(filePath);
|
|
15545
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.exportedNames.has(exportedName);
|
|
15546
|
-
const exportedNames = collectSourceTextExportNames(fs.readFileSync(filePath, "utf8"));
|
|
15547
|
-
exportNamesCache.set(filePath, {
|
|
15548
|
-
mtimeMs: fileStat.mtimeMs,
|
|
15549
|
-
size: fileStat.size,
|
|
15550
|
-
exportedNames
|
|
15551
|
-
});
|
|
15552
|
-
return exportedNames.has(exportedName);
|
|
15527
|
+
return doesSourceTextExportName(fs.readFileSync(filePath, "utf8"), exportedName);
|
|
15553
15528
|
} catch {
|
|
15554
15529
|
return false;
|
|
15555
15530
|
}
|
|
@@ -17432,15 +17407,8 @@ const getProgramAnalysis = (anyNode) => {
|
|
|
17432
17407
|
programToAnalysis.set(programNode, analysis);
|
|
17433
17408
|
return analysis;
|
|
17434
17409
|
};
|
|
17435
|
-
const scopeByNodeCache = /* @__PURE__ */ new WeakMap();
|
|
17436
17410
|
const getScopeForNode = (node, manager) => {
|
|
17437
17411
|
if (!node.range) return null;
|
|
17438
|
-
let scopeByNode = scopeByNodeCache.get(manager);
|
|
17439
|
-
if (!scopeByNode) {
|
|
17440
|
-
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
17441
|
-
scopeByNodeCache.set(manager, scopeByNode);
|
|
17442
|
-
}
|
|
17443
|
-
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
17444
17412
|
let bestScope = null;
|
|
17445
17413
|
let bestSize = Infinity;
|
|
17446
17414
|
for (const scope of manager.scopes) {
|
|
@@ -17453,7 +17421,6 @@ const getScopeForNode = (node, manager) => {
|
|
|
17453
17421
|
bestScope = scope;
|
|
17454
17422
|
}
|
|
17455
17423
|
}
|
|
17456
|
-
scopeByNode.set(node, bestScope);
|
|
17457
17424
|
return bestScope;
|
|
17458
17425
|
};
|
|
17459
17426
|
//#endregion
|
|
@@ -17488,20 +17455,11 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
17488
17455
|
} else if (isAstNode(child)) descend(child, visit, visited);
|
|
17489
17456
|
}
|
|
17490
17457
|
};
|
|
17491
|
-
const upstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17492
17458
|
const getUpstreamRefs = (analysis, ref) => {
|
|
17493
|
-
let upstreamByRef = upstreamRefsCache.get(analysis);
|
|
17494
|
-
if (!upstreamByRef) {
|
|
17495
|
-
upstreamByRef = /* @__PURE__ */ new WeakMap();
|
|
17496
|
-
upstreamRefsCache.set(analysis, upstreamByRef);
|
|
17497
|
-
}
|
|
17498
|
-
const cached = upstreamByRef.get(ref);
|
|
17499
|
-
if (cached) return cached;
|
|
17500
17459
|
const refs = [];
|
|
17501
17460
|
ascend(analysis, ref, (upRef) => {
|
|
17502
17461
|
refs.push(upRef);
|
|
17503
17462
|
});
|
|
17504
|
-
upstreamByRef.set(ref, refs);
|
|
17505
17463
|
return refs;
|
|
17506
17464
|
};
|
|
17507
17465
|
const findDownstreamNodes = (topNode, type) => {
|
|
@@ -17511,24 +17469,11 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
17511
17469
|
});
|
|
17512
17470
|
return nodes;
|
|
17513
17471
|
};
|
|
17514
|
-
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
17515
17472
|
const getRef = (analysis, identifier) => {
|
|
17516
|
-
let refByIdentifier = refByIdentifierCache.get(analysis);
|
|
17517
|
-
if (!refByIdentifier) {
|
|
17518
|
-
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
17519
|
-
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
17520
|
-
}
|
|
17521
|
-
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
17522
|
-
let resolvedReference = null;
|
|
17523
17473
|
const scope = getScopeForNode(identifier, analysis.scopeManager);
|
|
17524
|
-
if (scope)
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
break;
|
|
17528
|
-
}
|
|
17529
|
-
}
|
|
17530
|
-
refByIdentifier.set(identifier, resolvedReference);
|
|
17531
|
-
return resolvedReference;
|
|
17474
|
+
if (!scope) return null;
|
|
17475
|
+
for (const reference of scope.references) if (reference.identifier === identifier) return reference;
|
|
17476
|
+
return null;
|
|
17532
17477
|
};
|
|
17533
17478
|
const downstreamRefsCache = /* @__PURE__ */ new WeakMap();
|
|
17534
17479
|
const getDownstreamRefs = (analysis, node) => {
|
|
@@ -17603,6 +17548,22 @@ const isEventualCallTo = (analysis, ref, predicate) => {
|
|
|
17603
17548
|
};
|
|
17604
17549
|
//#endregion
|
|
17605
17550
|
//#region src/plugin/rules/state-and-effects/utils/effect/react.ts
|
|
17551
|
+
const getOuterScopeContaining = (analysis, node) => {
|
|
17552
|
+
if (!node.range) return null;
|
|
17553
|
+
let best = null;
|
|
17554
|
+
let bestSize = Infinity;
|
|
17555
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
17556
|
+
const block = scope.block;
|
|
17557
|
+
if (!block?.range) continue;
|
|
17558
|
+
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
17559
|
+
const size = block.range[1] - block.range[0];
|
|
17560
|
+
if (size <= bestSize) {
|
|
17561
|
+
bestSize = size;
|
|
17562
|
+
best = scope;
|
|
17563
|
+
}
|
|
17564
|
+
}
|
|
17565
|
+
return best;
|
|
17566
|
+
};
|
|
17606
17567
|
const KNOWN_PURE_HOC_NAMES = new Set(["memo", "forwardRef"]);
|
|
17607
17568
|
const startsWithUppercase = (name) => Boolean(name && name.length > 0 && name[0] >= "A" && name[0] <= "Z");
|
|
17608
17569
|
const isReactFunctionalComponent = (node) => {
|
|
@@ -17633,7 +17594,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
17633
17594
|
const isWrappedSeparately = () => {
|
|
17634
17595
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
17635
17596
|
const bindingName = node.id.name;
|
|
17636
|
-
const containingScope =
|
|
17597
|
+
const containingScope = getOuterScopeContaining(analysis, node);
|
|
17637
17598
|
if (!containingScope) return false;
|
|
17638
17599
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
17639
17600
|
if (!variable) return false;
|
|
@@ -19912,44 +19873,38 @@ const convertHslToRgb = (hueDegrees, saturationPercent, lightnessPercent) => {
|
|
|
19912
19873
|
};
|
|
19913
19874
|
const parseColorToRgb = (value) => {
|
|
19914
19875
|
const trimmed = value.trim().toLowerCase().replace(/_/g, " ");
|
|
19915
|
-
|
|
19916
|
-
|
|
19917
|
-
|
|
19918
|
-
|
|
19919
|
-
|
|
19920
|
-
|
|
19921
|
-
|
|
19922
|
-
|
|
19923
|
-
|
|
19924
|
-
|
|
19925
|
-
|
|
19926
|
-
|
|
19927
|
-
|
|
19928
|
-
|
|
19929
|
-
|
|
19930
|
-
|
|
19931
|
-
|
|
19932
|
-
|
|
19933
|
-
|
|
19934
|
-
|
|
19935
|
-
|
|
19936
|
-
|
|
19937
|
-
|
|
19938
|
-
|
|
19939
|
-
|
|
19940
|
-
|
|
19941
|
-
|
|
19942
|
-
|
|
19943
|
-
|
|
19944
|
-
|
|
19945
|
-
|
|
19946
|
-
|
|
19947
|
-
};
|
|
19948
|
-
}
|
|
19949
|
-
if (trimmed.includes("hsl")) {
|
|
19950
|
-
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19951
|
-
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19952
|
-
}
|
|
19876
|
+
const hex8Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})[0-9a-f]{2}$/);
|
|
19877
|
+
if (hex8Match) return {
|
|
19878
|
+
red: parseInt(hex8Match[1], 16),
|
|
19879
|
+
green: parseInt(hex8Match[2], 16),
|
|
19880
|
+
blue: parseInt(hex8Match[3], 16)
|
|
19881
|
+
};
|
|
19882
|
+
const hex6Match = trimmed.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/);
|
|
19883
|
+
if (hex6Match) return {
|
|
19884
|
+
red: parseInt(hex6Match[1], 16),
|
|
19885
|
+
green: parseInt(hex6Match[2], 16),
|
|
19886
|
+
blue: parseInt(hex6Match[3], 16)
|
|
19887
|
+
};
|
|
19888
|
+
const hex4Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])[0-9a-f]$/);
|
|
19889
|
+
if (hex4Match) return {
|
|
19890
|
+
red: parseInt(hex4Match[1] + hex4Match[1], 16),
|
|
19891
|
+
green: parseInt(hex4Match[2] + hex4Match[2], 16),
|
|
19892
|
+
blue: parseInt(hex4Match[3] + hex4Match[3], 16)
|
|
19893
|
+
};
|
|
19894
|
+
const hex3Match = trimmed.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
19895
|
+
if (hex3Match) return {
|
|
19896
|
+
red: parseInt(hex3Match[1] + hex3Match[1], 16),
|
|
19897
|
+
green: parseInt(hex3Match[2] + hex3Match[2], 16),
|
|
19898
|
+
blue: parseInt(hex3Match[3] + hex3Match[3], 16)
|
|
19899
|
+
};
|
|
19900
|
+
const rgbMatch = trimmed.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
|
|
19901
|
+
if (rgbMatch) return {
|
|
19902
|
+
red: parseInt(rgbMatch[1], 10),
|
|
19903
|
+
green: parseInt(rgbMatch[2], 10),
|
|
19904
|
+
blue: parseInt(rgbMatch[3], 10)
|
|
19905
|
+
};
|
|
19906
|
+
const hslMatch = trimmed.match(/hsla?\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%/);
|
|
19907
|
+
if (hslMatch) return convertHslToRgb(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
|
|
19953
19908
|
return null;
|
|
19954
19909
|
};
|
|
19955
19910
|
//#endregion
|
|
@@ -19977,18 +19932,9 @@ const extractColorFromShadowLayer = (layer) => {
|
|
|
19977
19932
|
if (hexMatch) return parseColorToRgb(`#${hexMatch[1]}`);
|
|
19978
19933
|
return null;
|
|
19979
19934
|
};
|
|
19980
|
-
const RGB_FUNCTION_PATTERN = /rgba?\([^)]*\)/g;
|
|
19981
|
-
const HEX_COLOR_PATTERN = /#[0-9a-f]{3,8}\b/gi;
|
|
19982
|
-
const NUMERIC_TOKEN_PATTERN = /(\d+(?:\.\d+)?)(px)?/g;
|
|
19983
|
-
const SHADOW_BLUR_TOKEN_INDEX = 2;
|
|
19984
19935
|
const parseShadowLayerBlur = (layer) => {
|
|
19985
|
-
const
|
|
19986
|
-
|
|
19987
|
-
for (const match of withoutColors.matchAll(NUMERIC_TOKEN_PATTERN)) {
|
|
19988
|
-
if (tokenIndex === SHADOW_BLUR_TOKEN_INDEX) return parseFloat(match[1]);
|
|
19989
|
-
tokenIndex += 1;
|
|
19990
|
-
}
|
|
19991
|
-
return 0;
|
|
19936
|
+
const numericTokens = [...layer.replace(/rgba?\([^)]*\)/g, "").replace(/#[0-9a-f]{3,8}\b/gi, "").matchAll(/(\d+(?:\.\d+)?)(px)?/g)].map((match) => parseFloat(match[1]));
|
|
19937
|
+
return numericTokens.length >= 3 ? numericTokens[2] : 0;
|
|
19992
19938
|
};
|
|
19993
19939
|
const hasColoredGlowShadow = (shadowValue) => {
|
|
19994
19940
|
for (const layer of splitShadowLayers(shadowValue)) {
|
|
@@ -20108,10 +20054,7 @@ const isIntrinsicJsxAttribute = (node) => {
|
|
|
20108
20054
|
};
|
|
20109
20055
|
//#endregion
|
|
20110
20056
|
//#region src/plugin/rules/state-and-effects/utils/is-controlled-prop-mirror.ts
|
|
20111
|
-
const componentPropNamesCache = /* @__PURE__ */ new WeakMap();
|
|
20112
20057
|
const collectComponentPropNames = (componentFunction) => {
|
|
20113
|
-
const cached = componentPropNamesCache.get(componentFunction);
|
|
20114
|
-
if (cached) return cached;
|
|
20115
20058
|
const propNames = /* @__PURE__ */ new Set();
|
|
20116
20059
|
if (!isFunctionLike$1(componentFunction)) return propNames;
|
|
20117
20060
|
const propsObjectParamNames = /* @__PURE__ */ new Set();
|
|
@@ -20125,23 +20068,27 @@ const collectComponentPropNames = (componentFunction) => {
|
|
|
20125
20068
|
if (child !== componentBody && isFunctionLike$1(child)) return false;
|
|
20126
20069
|
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "ObjectPattern") && isNodeOfType(child.init, "Identifier") && propsObjectParamNames.has(child.init.name)) collectPatternNames(child.id, propNames);
|
|
20127
20070
|
});
|
|
20128
|
-
componentPropNamesCache.set(componentFunction, propNames);
|
|
20129
20071
|
return propNames;
|
|
20130
20072
|
};
|
|
20131
|
-
const
|
|
20132
|
-
const getOwnScopeBoundNames = (functionNode) => {
|
|
20133
|
-
const cached = ownScopeBoundNamesCache.get(functionNode);
|
|
20134
|
-
if (cached) return cached;
|
|
20073
|
+
const declaresBindingNamed = (functionNode, bindingName) => {
|
|
20135
20074
|
const boundNames = /* @__PURE__ */ new Set();
|
|
20136
20075
|
if (isFunctionLike$1(functionNode)) for (const param of functionNode.params ?? []) collectPatternNames(param, boundNames);
|
|
20076
|
+
if (boundNames.has(bindingName)) return true;
|
|
20077
|
+
let declaresName = false;
|
|
20137
20078
|
walkAst(functionNode, (child) => {
|
|
20079
|
+
if (declaresName) return false;
|
|
20138
20080
|
if (child !== functionNode && isFunctionLike$1(child)) return false;
|
|
20139
|
-
if (isNodeOfType(child, "VariableDeclarator"))
|
|
20081
|
+
if (isNodeOfType(child, "VariableDeclarator")) {
|
|
20082
|
+
const declaratorNames = /* @__PURE__ */ new Set();
|
|
20083
|
+
collectPatternNames(child.id, declaratorNames);
|
|
20084
|
+
if (declaratorNames.has(bindingName)) {
|
|
20085
|
+
declaresName = true;
|
|
20086
|
+
return false;
|
|
20087
|
+
}
|
|
20088
|
+
}
|
|
20140
20089
|
});
|
|
20141
|
-
|
|
20142
|
-
return boundNames;
|
|
20090
|
+
return declaresName;
|
|
20143
20091
|
};
|
|
20144
|
-
const declaresBindingNamed = (functionNode, bindingName) => getOwnScopeBoundNames(functionNode).has(bindingName);
|
|
20145
20092
|
const isPropertyNamePosition = (identifier) => {
|
|
20146
20093
|
const parent = identifier.parent;
|
|
20147
20094
|
if (!parent) return false;
|
|
@@ -20564,28 +20511,36 @@ const isHandlerShapedReseed = (setterCall, componentFunction) => {
|
|
|
20564
20511
|
}
|
|
20565
20512
|
return hasNestedFunction;
|
|
20566
20513
|
};
|
|
20567
|
-
const
|
|
20568
|
-
|
|
20569
|
-
|
|
20570
|
-
|
|
20571
|
-
|
|
20572
|
-
|
|
20573
|
-
|
|
20514
|
+
const isReseededDraftBuffer = (useStateCall, isPropName) => {
|
|
20515
|
+
const setterName = getStateSetterName(useStateCall);
|
|
20516
|
+
if (!setterName) return false;
|
|
20517
|
+
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20518
|
+
if (!componentFunction) return false;
|
|
20519
|
+
let isReseeded = false;
|
|
20520
|
+
walkAst(componentFunction, (child) => {
|
|
20521
|
+
if (isReseeded) return false;
|
|
20522
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName) && isHandlerShapedReseed(child, componentFunction)) {
|
|
20523
|
+
isReseeded = true;
|
|
20524
|
+
return false;
|
|
20525
|
+
}
|
|
20526
|
+
});
|
|
20527
|
+
return isReseeded;
|
|
20574
20528
|
};
|
|
20575
|
-
const
|
|
20529
|
+
const isAdjustedDuringRender = (useStateCall, isPropName) => {
|
|
20576
20530
|
const setterName = getStateSetterName(useStateCall);
|
|
20577
20531
|
if (!setterName) return false;
|
|
20578
20532
|
const componentFunction = findEnclosingFunction(useStateCall);
|
|
20579
20533
|
if (!componentFunction) return false;
|
|
20580
|
-
let
|
|
20534
|
+
let isAdjusted = false;
|
|
20581
20535
|
walkAst(componentFunction, (child) => {
|
|
20582
|
-
if (
|
|
20583
|
-
if (
|
|
20584
|
-
|
|
20536
|
+
if (isAdjusted) return false;
|
|
20537
|
+
if (child !== componentFunction && isFunctionLike$1(child)) return false;
|
|
20538
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === setterName && isPropDerivedArgument(child.arguments?.[0], isPropName)) {
|
|
20539
|
+
isAdjusted = true;
|
|
20585
20540
|
return false;
|
|
20586
20541
|
}
|
|
20587
20542
|
});
|
|
20588
|
-
return
|
|
20543
|
+
return isAdjusted;
|
|
20589
20544
|
};
|
|
20590
20545
|
const noDerivedUseState = defineRule({
|
|
20591
20546
|
id: "no-derived-useState",
|
|
@@ -20602,7 +20557,8 @@ const noDerivedUseState = defineRule({
|
|
|
20602
20557
|
const initializer = node.arguments[0];
|
|
20603
20558
|
if (isNodeOfType(initializer, "Identifier") && propStackTracker.isPropName(initializer.name)) {
|
|
20604
20559
|
if (isInitialOnlyPropName(initializer.name)) return;
|
|
20605
|
-
if (
|
|
20560
|
+
if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
|
|
20561
|
+
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20606
20562
|
context.report({
|
|
20607
20563
|
node,
|
|
20608
20564
|
message: `Your users see a stale value when prop "${initializer.name}" changes because useState copies it once.`
|
|
@@ -20613,7 +20569,8 @@ const noDerivedUseState = defineRule({
|
|
|
20613
20569
|
const rootIdentifierName = getRootIdentifierName(initializer);
|
|
20614
20570
|
if (rootIdentifierName && propStackTracker.isPropName(rootIdentifierName)) {
|
|
20615
20571
|
if (isNodeOfType(initializer.property, "Identifier") && isInitialOnlyPropName(initializer.property.name)) return;
|
|
20616
|
-
if (
|
|
20572
|
+
if (isReseededDraftBuffer(node, propStackTracker.isPropName)) return;
|
|
20573
|
+
if (isAdjustedDuringRender(node, propStackTracker.isPropName)) return;
|
|
20617
20574
|
context.report({
|
|
20618
20575
|
node,
|
|
20619
20576
|
message: `Your users see a stale value when prop "${rootIdentifierName}" changes because useState copies it once.`
|
|
@@ -22312,20 +22269,20 @@ const getTriggerGuardRootName = (testNode) => {
|
|
|
22312
22269
|
return null;
|
|
22313
22270
|
};
|
|
22314
22271
|
const collectHandlerOnlyWriteStateNames = (componentBody, useStateBindings, handlerBindingNames) => {
|
|
22315
|
-
const setterNames = new Set(useStateBindings.map((binding) => binding.setterName));
|
|
22316
|
-
const settersWithAnyCall = /* @__PURE__ */ new Set();
|
|
22317
|
-
const settersWithNonHandlerCall = /* @__PURE__ */ new Set();
|
|
22318
|
-
walkAst(componentBody, (child) => {
|
|
22319
|
-
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22320
|
-
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22321
|
-
const setterName = child.callee.name;
|
|
22322
|
-
if (!setterNames.has(setterName)) return;
|
|
22323
|
-
settersWithAnyCall.add(setterName);
|
|
22324
|
-
if (settersWithNonHandlerCall.has(setterName)) return;
|
|
22325
|
-
if (!isInsideEventHandler(child, handlerBindingNames)) settersWithNonHandlerCall.add(setterName);
|
|
22326
|
-
});
|
|
22327
22272
|
const handlerOnlyWriteStateNames = /* @__PURE__ */ new Set();
|
|
22328
|
-
for (const binding of useStateBindings)
|
|
22273
|
+
for (const binding of useStateBindings) {
|
|
22274
|
+
let didFindAnySetterCall = false;
|
|
22275
|
+
let areAllSetterCallsInHandlers = true;
|
|
22276
|
+
walkAst(componentBody, (child) => {
|
|
22277
|
+
if (!areAllSetterCallsInHandlers) return false;
|
|
22278
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
22279
|
+
if (!isNodeOfType(child.callee, "Identifier")) return;
|
|
22280
|
+
if (child.callee.name !== binding.setterName) return;
|
|
22281
|
+
didFindAnySetterCall = true;
|
|
22282
|
+
if (!isInsideEventHandler(child, handlerBindingNames)) areAllSetterCallsInHandlers = false;
|
|
22283
|
+
});
|
|
22284
|
+
if (didFindAnySetterCall && areAllSetterCallsInHandlers) handlerOnlyWriteStateNames.add(binding.valueName);
|
|
22285
|
+
}
|
|
22329
22286
|
return handlerOnlyWriteStateNames;
|
|
22330
22287
|
};
|
|
22331
22288
|
const noEventTriggerState = defineRule({
|
|
@@ -22665,10 +22622,6 @@ const COLORED_BG_PATTERN = /^bg-(?:red|orange|amber|yellow|lime|green|emerald|te
|
|
|
22665
22622
|
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)-)/;
|
|
22666
22623
|
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)-)/;
|
|
22667
22624
|
const splitVariantScope = (token) => {
|
|
22668
|
-
if (!token.includes(":")) return {
|
|
22669
|
-
scope: "",
|
|
22670
|
-
utility: token.startsWith("!") ? token.slice(1) : token
|
|
22671
|
-
};
|
|
22672
22625
|
const segments = token.split(":");
|
|
22673
22626
|
const rawUtility = segments[segments.length - 1];
|
|
22674
22627
|
return {
|
|
@@ -22692,7 +22645,6 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
22692
22645
|
const bgColorScopes = /* @__PURE__ */ new Set();
|
|
22693
22646
|
for (const token of classStr.split(/\s+/)) {
|
|
22694
22647
|
if (!token) continue;
|
|
22695
|
-
if (!token.includes("text-") && !token.includes("bg-")) continue;
|
|
22696
22648
|
const { scope, utility } = splitVariantScope(token);
|
|
22697
22649
|
if (TEXT_COLOR_PATTERN.test(utility)) textColorScopes.add(scope);
|
|
22698
22650
|
if (BG_COLOR_PATTERN.test(utility)) bgColorScopes.add(scope);
|
|
@@ -23422,8 +23374,6 @@ const hasInfiniteIterationCount = (properties) => properties.some((property) =>
|
|
|
23422
23374
|
return isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier") && property.value.name === "Infinity";
|
|
23423
23375
|
});
|
|
23424
23376
|
const isInfiniteAnimationSegment = (segment) => segment.trim().split(/\s+/).includes("infinite");
|
|
23425
|
-
const DURATION_SEGMENT_PATTERN = /^([\d.]+)(m?s)$/;
|
|
23426
|
-
const FIRST_TIME_TOKEN_PATTERN = /(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/;
|
|
23427
23377
|
const noLongTransitionDuration = defineRule({
|
|
23428
23378
|
id: "no-long-transition-duration",
|
|
23429
23379
|
title: "Transition duration too long",
|
|
@@ -23447,10 +23397,11 @@ const noLongTransitionDuration = defineRule({
|
|
|
23447
23397
|
if (key === "transitionDuration" || key === "animationDuration") {
|
|
23448
23398
|
let longestDurationPropertyMs = 0;
|
|
23449
23399
|
for (const segment of value.split(",")) {
|
|
23450
|
-
const
|
|
23451
|
-
|
|
23452
|
-
const
|
|
23453
|
-
longestDurationPropertyMs = Math.max(longestDurationPropertyMs,
|
|
23400
|
+
const trimmedSegment = segment.trim();
|
|
23401
|
+
const msMatch = trimmedSegment.match(/^([\d.]+)ms$/);
|
|
23402
|
+
const secondsMatch = trimmedSegment.match(/^([\d.]+)s$/);
|
|
23403
|
+
if (msMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(msMatch[1]));
|
|
23404
|
+
else if (secondsMatch) longestDurationPropertyMs = Math.max(longestDurationPropertyMs, parseFloat(secondsMatch[1]) * 1e3);
|
|
23454
23405
|
}
|
|
23455
23406
|
if (longestDurationPropertyMs > 0) durationMs = longestDurationPropertyMs;
|
|
23456
23407
|
}
|
|
@@ -23458,7 +23409,7 @@ const noLongTransitionDuration = defineRule({
|
|
|
23458
23409
|
let longestDurationMs = 0;
|
|
23459
23410
|
for (const segment of value.split(",")) {
|
|
23460
23411
|
if (key === "animation" && isInfiniteAnimationSegment(segment)) continue;
|
|
23461
|
-
const firstTimeMatch = segment.match(
|
|
23412
|
+
const firstTimeMatch = segment.match(/(?<![a-zA-Z\d])([\d.]+)(m?s)(?![a-zA-Z\d-])/);
|
|
23462
23413
|
if (!firstTimeMatch) continue;
|
|
23463
23414
|
const segmentDurationMs = firstTimeMatch[2] === "ms" ? parseFloat(firstTimeMatch[1]) : parseFloat(firstTimeMatch[1]) * 1e3;
|
|
23464
23415
|
longestDurationMs = Math.max(longestDurationMs, segmentDurationMs);
|
|
@@ -32284,11 +32235,41 @@ const callsIdentifier = (root, identifierName) => {
|
|
|
32284
32235
|
});
|
|
32285
32236
|
return found;
|
|
32286
32237
|
};
|
|
32238
|
+
const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
32239
|
+
if (!componentBody) return false;
|
|
32240
|
+
let found = false;
|
|
32241
|
+
walkAst(componentBody, (child) => {
|
|
32242
|
+
if (found) return;
|
|
32243
|
+
if (!isFunctionLike$1(child)) return;
|
|
32244
|
+
const functionBody = child.body;
|
|
32245
|
+
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
32246
|
+
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
32247
|
+
});
|
|
32248
|
+
return found;
|
|
32249
|
+
};
|
|
32287
32250
|
const PROMISE_CHAIN_METHOD_NAMES = new Set([
|
|
32288
32251
|
"then",
|
|
32289
32252
|
"catch",
|
|
32290
32253
|
"finally"
|
|
32291
32254
|
]);
|
|
32255
|
+
const setterIsCalledInPromiseChain = (componentBody, setterName) => {
|
|
32256
|
+
if (!componentBody) return false;
|
|
32257
|
+
let found = false;
|
|
32258
|
+
walkAst(componentBody, (child) => {
|
|
32259
|
+
if (found) return;
|
|
32260
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32261
|
+
const callee = child.callee;
|
|
32262
|
+
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || !PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) return;
|
|
32263
|
+
for (const argument of child.arguments ?? []) {
|
|
32264
|
+
if (!isFunctionLike$1(argument)) continue;
|
|
32265
|
+
if (callsIdentifier(argument.body, setterName)) {
|
|
32266
|
+
found = true;
|
|
32267
|
+
return;
|
|
32268
|
+
}
|
|
32269
|
+
}
|
|
32270
|
+
});
|
|
32271
|
+
return found;
|
|
32272
|
+
};
|
|
32292
32273
|
const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
32293
32274
|
"useApolloClient",
|
|
32294
32275
|
"useMutation",
|
|
@@ -32301,36 +32282,20 @@ const ASYNC_DATA_CALLEE_NAMES = new Set([
|
|
|
32301
32282
|
"fetch",
|
|
32302
32283
|
"axios"
|
|
32303
32284
|
]);
|
|
32304
|
-
const
|
|
32285
|
+
const referencesAsyncDataApi = (body) => {
|
|
32286
|
+
if (!body) return false;
|
|
32305
32287
|
let found = false;
|
|
32306
|
-
walkAst(
|
|
32307
|
-
if (found) return
|
|
32288
|
+
walkAst(body, (child) => {
|
|
32289
|
+
if (found) return;
|
|
32308
32290
|
if (isNodeOfType(child, "CallExpression")) {
|
|
32309
32291
|
const callee = child.callee;
|
|
32310
32292
|
if (isNodeOfType(callee, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.name)) {
|
|
32311
32293
|
found = true;
|
|
32312
|
-
return
|
|
32313
|
-
}
|
|
32314
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) {
|
|
32315
|
-
if (ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32316
|
-
found = true;
|
|
32317
|
-
return false;
|
|
32318
|
-
}
|
|
32319
|
-
if (setterName !== null && PROMISE_CHAIN_METHOD_NAMES.has(callee.property.name)) for (const argument of child.arguments ?? []) {
|
|
32320
|
-
if (!isFunctionLike$1(argument)) continue;
|
|
32321
|
-
if (callsIdentifier(argument.body, setterName)) {
|
|
32322
|
-
found = true;
|
|
32323
|
-
return false;
|
|
32324
|
-
}
|
|
32325
|
-
}
|
|
32294
|
+
return;
|
|
32326
32295
|
}
|
|
32327
|
-
|
|
32328
|
-
}
|
|
32329
|
-
if (setterName !== null && isFunctionLike$1(child)) {
|
|
32330
|
-
const functionBody = child.body;
|
|
32331
|
-
if ((Boolean(child.async) || hasOwnAwait(functionBody)) && callsIdentifier(functionBody, setterName)) {
|
|
32296
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && ASYNC_DATA_CALLEE_NAMES.has(callee.property.name)) {
|
|
32332
32297
|
found = true;
|
|
32333
|
-
return
|
|
32298
|
+
return;
|
|
32334
32299
|
}
|
|
32335
32300
|
}
|
|
32336
32301
|
});
|
|
@@ -32355,7 +32320,7 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
32355
32320
|
const secondBinding = node.id.elements[1];
|
|
32356
32321
|
const setterName = isNodeOfType(secondBinding, "Identifier") ? secondBinding.name : null;
|
|
32357
32322
|
const fnBody = enclosingFunctionBody(node);
|
|
32358
|
-
if (fnBody &&
|
|
32323
|
+
if (fnBody && (setterName && setterIsCalledInAsyncContext(fnBody, setterName) || setterName && setterIsCalledInPromiseChain(fnBody, setterName) || referencesAsyncDataApi(fnBody))) return;
|
|
32359
32324
|
context.report({
|
|
32360
32325
|
node: node.init,
|
|
32361
32326
|
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`
|
|
@@ -33149,17 +33114,17 @@ const rerenderStateOnlyInHandlers = defineRule({
|
|
|
33149
33114
|
const effectTriggerNames = /* @__PURE__ */ new Set();
|
|
33150
33115
|
for (const dependencyName of collectDependencyArrayNames(componentBody)) if (!(stateValueNames.has(dependencyName) && effectCallbackReadNames.has(dependencyName))) effectTriggerNames.add(dependencyName);
|
|
33151
33116
|
for (const reachableName of expandTransitiveDependencies(effectTriggerNames, dependencyGraph)) renderReachableNames.add(reachableName);
|
|
33152
|
-
const setterNames = new Set(bindings.map((binding) => binding.setterName));
|
|
33153
|
-
const calledSetterNames = /* @__PURE__ */ new Set();
|
|
33154
|
-
walkAst(componentBody, (child) => {
|
|
33155
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && setterNames.has(child.callee.name)) calledSetterNames.add(child.callee.name);
|
|
33156
|
-
});
|
|
33157
33117
|
for (const binding of bindings) {
|
|
33158
33118
|
if (renderReachableNames.has(binding.valueName)) continue;
|
|
33159
33119
|
if (binding.valueName === "_" || binding.valueName.startsWith("_")) continue;
|
|
33160
33120
|
const setterSuffix = binding.setterName.slice(3);
|
|
33161
33121
|
if (/^(TriggerRender|ForceUpdate|Rerender|ForceRender|Tick|Bump|BumpVersion|InvalidateRender|Refresh|Repaint)$/i.test(setterSuffix)) continue;
|
|
33162
|
-
|
|
33122
|
+
let setterCalled = false;
|
|
33123
|
+
walkAst(componentBody, (child) => {
|
|
33124
|
+
if (setterCalled) return;
|
|
33125
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && child.callee.name === binding.setterName) setterCalled = true;
|
|
33126
|
+
});
|
|
33127
|
+
if (!setterCalled) continue;
|
|
33163
33128
|
if (isSetterCalledDuringRender(componentBody, binding.setterName)) continue;
|
|
33164
33129
|
context.report({
|
|
33165
33130
|
node: binding.declarator,
|
|
@@ -40581,16 +40546,7 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
40581
40546
|
//#endregion
|
|
40582
40547
|
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
40583
40548
|
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;
|
|
40584
|
-
const
|
|
40585
|
-
const collectLastEnableRlsIndexByTable = (content) => {
|
|
40586
|
-
const lastEnableIndexByTable = /* @__PURE__ */ new Map();
|
|
40587
|
-
for (const match of content.matchAll(ENABLE_RLS_PATTERN)) {
|
|
40588
|
-
const tableName = match[1];
|
|
40589
|
-
if (tableName === void 0) continue;
|
|
40590
|
-
lastEnableIndexByTable.set(tableName.toLowerCase(), match.index);
|
|
40591
|
-
}
|
|
40592
|
-
return lastEnableIndexByTable;
|
|
40593
|
-
};
|
|
40549
|
+
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");
|
|
40594
40550
|
const supabaseTableMissingRls = defineRule({
|
|
40595
40551
|
id: "supabase-table-missing-rls",
|
|
40596
40552
|
title: "Supabase table created without Row Level Security",
|
|
@@ -40601,13 +40557,11 @@ const supabaseTableMissingRls = defineRule({
|
|
|
40601
40557
|
const content = sanitizeSqlForScan(file.content);
|
|
40602
40558
|
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
40603
40559
|
const findings = [];
|
|
40604
|
-
const lastEnableIndexByTable = collectLastEnableRlsIndexByTable(content);
|
|
40605
40560
|
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
40606
40561
|
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
40607
40562
|
const tableName = match[1];
|
|
40608
40563
|
if (tableName === void 0) continue;
|
|
40609
|
-
|
|
40610
|
-
if (lastEnableIndex !== void 0 && lastEnableIndex >= match.index) continue;
|
|
40564
|
+
if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
|
|
40611
40565
|
const location = getLocationAtIndex(content, match.index);
|
|
40612
40566
|
findings.push({
|
|
40613
40567
|
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
@@ -41631,14 +41585,7 @@ const getStaticPropertyName = (member) => {
|
|
|
41631
41585
|
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
41632
41586
|
return null;
|
|
41633
41587
|
};
|
|
41634
|
-
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
41635
41588
|
const getImportInfoForIdentifier = (identifier) => {
|
|
41636
|
-
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
41637
|
-
const importInfo = computeImportInfoForIdentifier(identifier);
|
|
41638
|
-
importInfoCache.set(identifier, importInfo);
|
|
41639
|
-
return importInfo;
|
|
41640
|
-
};
|
|
41641
|
-
const computeImportInfoForIdentifier = (identifier) => {
|
|
41642
41589
|
const specifier = findVariableInitializer(identifier, identifier.name)?.initializer;
|
|
41643
41590
|
if (!specifier) return null;
|
|
41644
41591
|
const declaration = specifier.parent;
|