oxlint-plugin-react-doctor 0.5.5 → 0.5.6-dev.8908f98
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.d.ts +1293 -0
- package/dist/index.js +993 -144
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -323,9 +323,18 @@ const BROWSER_ARTIFACT_PATH_PATTERNS = [
|
|
|
323
323
|
const AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN = /\b(?:exec|execSync|spawn|child_process|eval|new Function|vm\.run|readFile|writeFile|fs\.read|fs\.write|fetch|axios|http\.request|sandbox|runCode|executeCode)\b/;
|
|
324
324
|
//#endregion
|
|
325
325
|
//#region src/plugin/rules/security-scan/utils/is-browser-artifact-path.ts
|
|
326
|
-
const
|
|
326
|
+
const SERVER_BUILD_ROOT_SEGMENTS = new Set([".next", ".output"]);
|
|
327
|
+
const isNonShippedBuildArtifactPath = (relativePath) => {
|
|
328
|
+
const segments = relativePath.split("/");
|
|
329
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
330
|
+
if (!SERVER_BUILD_ROOT_SEGMENTS.has(segments[index])) continue;
|
|
331
|
+
if (segments[index] === ".next" && segments[index + 1] === "dev") return true;
|
|
332
|
+
if (segments[index + 1] === "server" && index + 2 < segments.length) return true;
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
};
|
|
327
336
|
const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
328
|
-
if (
|
|
337
|
+
if (isNonShippedBuildArtifactPath(relativePath)) return false;
|
|
329
338
|
if (isGeneratedBundle) return true;
|
|
330
339
|
if (relativePath.endsWith(".map")) return true;
|
|
331
340
|
return BROWSER_ARTIFACT_PATH_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
@@ -1108,6 +1117,11 @@ const getImportedNameFromModule = (contextNode, localIdentifierName, moduleSourc
|
|
|
1108
1117
|
if (info.source !== moduleSource) return null;
|
|
1109
1118
|
return info.imported;
|
|
1110
1119
|
};
|
|
1120
|
+
const getImportSourceForName = (contextNode, localIdentifierName) => {
|
|
1121
|
+
const lookup = getImportLookup(contextNode);
|
|
1122
|
+
if (!lookup) return null;
|
|
1123
|
+
return lookup.get(localIdentifierName)?.source ?? null;
|
|
1124
|
+
};
|
|
1111
1125
|
//#endregion
|
|
1112
1126
|
//#region src/plugin/utils/find-variable-initializer.ts
|
|
1113
1127
|
const FUNCTION_LIKE_TYPES$1 = new Set([
|
|
@@ -1847,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1847
1861
|
});
|
|
1848
1862
|
//#endregion
|
|
1849
1863
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1850
|
-
const MESSAGE$
|
|
1864
|
+
const MESSAGE$57 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1851
1865
|
const anchorHasContent = defineRule({
|
|
1852
1866
|
id: "anchor-has-content",
|
|
1853
1867
|
title: "Anchor has no content",
|
|
@@ -1863,7 +1877,7 @@ const anchorHasContent = defineRule({
|
|
|
1863
1877
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1864
1878
|
context.report({
|
|
1865
1879
|
node: opening.name,
|
|
1866
|
-
message: MESSAGE$
|
|
1880
|
+
message: MESSAGE$57
|
|
1867
1881
|
});
|
|
1868
1882
|
} })
|
|
1869
1883
|
});
|
|
@@ -2257,7 +2271,7 @@ const parseJsxValue = (value) => {
|
|
|
2257
2271
|
};
|
|
2258
2272
|
//#endregion
|
|
2259
2273
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2260
|
-
const MESSAGE$
|
|
2274
|
+
const MESSAGE$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2261
2275
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2262
2276
|
id: "aria-activedescendant-has-tabindex",
|
|
2263
2277
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2275,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2275
2289
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2276
2290
|
context.report({
|
|
2277
2291
|
node: node.name,
|
|
2278
|
-
message: MESSAGE$
|
|
2292
|
+
message: MESSAGE$56
|
|
2279
2293
|
});
|
|
2280
2294
|
return;
|
|
2281
2295
|
}
|
|
2282
2296
|
if (isInteractiveElement(tag, node)) return;
|
|
2283
2297
|
context.report({
|
|
2284
2298
|
node: node.name,
|
|
2285
|
-
message: MESSAGE$
|
|
2299
|
+
message: MESSAGE$56
|
|
2286
2300
|
});
|
|
2287
2301
|
} })
|
|
2288
2302
|
});
|
|
@@ -4187,6 +4201,58 @@ const asyncParallel = defineRule({
|
|
|
4187
4201
|
}
|
|
4188
4202
|
});
|
|
4189
4203
|
//#endregion
|
|
4204
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4205
|
+
const MESSAGE$55 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4206
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4207
|
+
const STORAGE_GLOBALS = new Set([
|
|
4208
|
+
"window",
|
|
4209
|
+
"globalThis",
|
|
4210
|
+
"self"
|
|
4211
|
+
]);
|
|
4212
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4213
|
+
const isWebStorageObject = (node) => {
|
|
4214
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4215
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4216
|
+
return false;
|
|
4217
|
+
};
|
|
4218
|
+
const staticMemberName = (member) => {
|
|
4219
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4220
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4221
|
+
return null;
|
|
4222
|
+
};
|
|
4223
|
+
const authTokenInWebStorage = defineRule({
|
|
4224
|
+
id: "auth-token-in-web-storage",
|
|
4225
|
+
title: "Auth token in web storage",
|
|
4226
|
+
severity: "warn",
|
|
4227
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4228
|
+
create: (context) => ({
|
|
4229
|
+
CallExpression(node) {
|
|
4230
|
+
const callee = node.callee;
|
|
4231
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4232
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4233
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4234
|
+
const keyArgument = node.arguments?.[0];
|
|
4235
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4236
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4237
|
+
context.report({
|
|
4238
|
+
node,
|
|
4239
|
+
message: MESSAGE$55
|
|
4240
|
+
});
|
|
4241
|
+
},
|
|
4242
|
+
AssignmentExpression(node) {
|
|
4243
|
+
const target = node.left;
|
|
4244
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4245
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4246
|
+
const propertyName = staticMemberName(target);
|
|
4247
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4248
|
+
context.report({
|
|
4249
|
+
node: target,
|
|
4250
|
+
message: MESSAGE$55
|
|
4251
|
+
});
|
|
4252
|
+
}
|
|
4253
|
+
})
|
|
4254
|
+
});
|
|
4255
|
+
//#endregion
|
|
4190
4256
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4191
4257
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4192
4258
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4558,7 +4624,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4558
4624
|
//#endregion
|
|
4559
4625
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4560
4626
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4561
|
-
const MESSAGE$
|
|
4627
|
+
const MESSAGE$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4562
4628
|
const KEY_HANDLERS = [
|
|
4563
4629
|
"onKeyUp",
|
|
4564
4630
|
"onKeyDown",
|
|
@@ -4590,7 +4656,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4590
4656
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4591
4657
|
context.report({
|
|
4592
4658
|
node: node.name,
|
|
4593
|
-
message: MESSAGE$
|
|
4659
|
+
message: MESSAGE$54
|
|
4594
4660
|
});
|
|
4595
4661
|
} };
|
|
4596
4662
|
}
|
|
@@ -4705,7 +4771,7 @@ const isReactComponentName = (name) => {
|
|
|
4705
4771
|
};
|
|
4706
4772
|
//#endregion
|
|
4707
4773
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4708
|
-
const MESSAGE$
|
|
4774
|
+
const MESSAGE$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4709
4775
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4710
4776
|
const DEFAULT_LABELLING_PROPS = [
|
|
4711
4777
|
"alt",
|
|
@@ -4866,7 +4932,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4866
4932
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4867
4933
|
context.report({
|
|
4868
4934
|
node: opening,
|
|
4869
|
-
message: MESSAGE$
|
|
4935
|
+
message: MESSAGE$53
|
|
4870
4936
|
});
|
|
4871
4937
|
} };
|
|
4872
4938
|
}
|
|
@@ -5292,6 +5358,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5292
5358
|
} })
|
|
5293
5359
|
});
|
|
5294
5360
|
//#endregion
|
|
5361
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5362
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5363
|
+
//#endregion
|
|
5364
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5365
|
+
const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5366
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5367
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5368
|
+
"aria-label",
|
|
5369
|
+
"aria-labelledby",
|
|
5370
|
+
"title"
|
|
5371
|
+
];
|
|
5372
|
+
const dialogHasAccessibleName = defineRule({
|
|
5373
|
+
id: "dialog-has-accessible-name",
|
|
5374
|
+
title: "Dialog without accessible name",
|
|
5375
|
+
severity: "warn",
|
|
5376
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5377
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5378
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5379
|
+
const tagName = node.name.name;
|
|
5380
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5381
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5382
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5383
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5384
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5385
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5386
|
+
context.report({
|
|
5387
|
+
node: node.name,
|
|
5388
|
+
message: MESSAGE$52
|
|
5389
|
+
});
|
|
5390
|
+
} })
|
|
5391
|
+
});
|
|
5392
|
+
//#endregion
|
|
5295
5393
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5296
5394
|
const PRAGMA$2 = "React";
|
|
5297
5395
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5326,7 +5424,7 @@ const isEs6Component = (node) => {
|
|
|
5326
5424
|
};
|
|
5327
5425
|
//#endregion
|
|
5328
5426
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5329
|
-
const MESSAGE$
|
|
5427
|
+
const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5330
5428
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5331
5429
|
"observer",
|
|
5332
5430
|
"lazy",
|
|
@@ -5529,7 +5627,7 @@ const displayName = defineRule({
|
|
|
5529
5627
|
const reportAt = (node) => {
|
|
5530
5628
|
context.report({
|
|
5531
5629
|
node,
|
|
5532
|
-
message: MESSAGE$
|
|
5630
|
+
message: MESSAGE$51
|
|
5533
5631
|
});
|
|
5534
5632
|
};
|
|
5535
5633
|
return {
|
|
@@ -7677,7 +7775,7 @@ const forbidElements = defineRule({
|
|
|
7677
7775
|
});
|
|
7678
7776
|
//#endregion
|
|
7679
7777
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7680
|
-
const MESSAGE$
|
|
7778
|
+
const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7681
7779
|
const forwardRefUsesRef = defineRule({
|
|
7682
7780
|
id: "forward-ref-uses-ref",
|
|
7683
7781
|
title: "forwardRef without ref parameter",
|
|
@@ -7697,7 +7795,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7697
7795
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7698
7796
|
context.report({
|
|
7699
7797
|
node: inner,
|
|
7700
|
-
message: MESSAGE$
|
|
7798
|
+
message: MESSAGE$50
|
|
7701
7799
|
});
|
|
7702
7800
|
} })
|
|
7703
7801
|
});
|
|
@@ -7734,7 +7832,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7734
7832
|
});
|
|
7735
7833
|
//#endregion
|
|
7736
7834
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7737
|
-
const MESSAGE$
|
|
7835
|
+
const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7738
7836
|
const DEFAULT_HEADING_TAGS = [
|
|
7739
7837
|
"h1",
|
|
7740
7838
|
"h2",
|
|
@@ -7767,7 +7865,7 @@ const headingHasContent = defineRule({
|
|
|
7767
7865
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7768
7866
|
context.report({
|
|
7769
7867
|
node,
|
|
7770
|
-
message: MESSAGE$
|
|
7868
|
+
message: MESSAGE$49
|
|
7771
7869
|
});
|
|
7772
7870
|
} };
|
|
7773
7871
|
}
|
|
@@ -7905,7 +8003,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7905
8003
|
});
|
|
7906
8004
|
//#endregion
|
|
7907
8005
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7908
|
-
const MESSAGE$
|
|
8006
|
+
const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7909
8007
|
const resolveSettings$38 = (settings) => {
|
|
7910
8008
|
const reactDoctor = settings?.["react-doctor"];
|
|
7911
8009
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7953,7 +8051,7 @@ const htmlHasLang = defineRule({
|
|
|
7953
8051
|
if (!lang) {
|
|
7954
8052
|
context.report({
|
|
7955
8053
|
node: node.name,
|
|
7956
|
-
message: MESSAGE$
|
|
8054
|
+
message: MESSAGE$48
|
|
7957
8055
|
});
|
|
7958
8056
|
return;
|
|
7959
8057
|
}
|
|
@@ -7961,13 +8059,13 @@ const htmlHasLang = defineRule({
|
|
|
7961
8059
|
if (verdict === "missing" || verdict === "empty") {
|
|
7962
8060
|
context.report({
|
|
7963
8061
|
node: lang,
|
|
7964
|
-
message: MESSAGE$
|
|
8062
|
+
message: MESSAGE$48
|
|
7965
8063
|
});
|
|
7966
8064
|
return;
|
|
7967
8065
|
}
|
|
7968
8066
|
if (hasSpread && !lang) context.report({
|
|
7969
8067
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8068
|
+
message: MESSAGE$48
|
|
7971
8069
|
});
|
|
7972
8070
|
} };
|
|
7973
8071
|
}
|
|
@@ -8181,7 +8279,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8181
8279
|
});
|
|
8182
8280
|
//#endregion
|
|
8183
8281
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8184
|
-
const MESSAGE$
|
|
8282
|
+
const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8185
8283
|
const evaluateTitleValue = (value) => {
|
|
8186
8284
|
if (!value) return "missing";
|
|
8187
8285
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8221,14 +8319,14 @@ const iframeHasTitle = defineRule({
|
|
|
8221
8319
|
if (!titleAttr) {
|
|
8222
8320
|
if (hasSpread || tag === "iframe") context.report({
|
|
8223
8321
|
node: node.name,
|
|
8224
|
-
message: MESSAGE$
|
|
8322
|
+
message: MESSAGE$47
|
|
8225
8323
|
});
|
|
8226
8324
|
return;
|
|
8227
8325
|
}
|
|
8228
8326
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8229
8327
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8230
8328
|
node: titleAttr,
|
|
8231
|
-
message: MESSAGE$
|
|
8329
|
+
message: MESSAGE$47
|
|
8232
8330
|
});
|
|
8233
8331
|
} })
|
|
8234
8332
|
});
|
|
@@ -8332,7 +8430,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8332
8430
|
});
|
|
8333
8431
|
//#endregion
|
|
8334
8432
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8335
|
-
const MESSAGE$
|
|
8433
|
+
const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8336
8434
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8337
8435
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8338
8436
|
"image",
|
|
@@ -8397,7 +8495,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8397
8495
|
if (!altAttribute) return;
|
|
8398
8496
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8399
8497
|
node: altAttribute,
|
|
8400
|
-
message: MESSAGE$
|
|
8498
|
+
message: MESSAGE$46
|
|
8401
8499
|
});
|
|
8402
8500
|
} };
|
|
8403
8501
|
}
|
|
@@ -8495,6 +8593,136 @@ const insecureCryptoRisk = defineRule({
|
|
|
8495
8593
|
}
|
|
8496
8594
|
});
|
|
8497
8595
|
//#endregion
|
|
8596
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
8597
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
8598
|
+
const open = content[openIndex];
|
|
8599
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
8600
|
+
if (close === "") return -1;
|
|
8601
|
+
let depth = 0;
|
|
8602
|
+
let stringDelimiter = null;
|
|
8603
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
8604
|
+
const character = content[index];
|
|
8605
|
+
if (stringDelimiter !== null) {
|
|
8606
|
+
if (character === "\\") index += 1;
|
|
8607
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8608
|
+
continue;
|
|
8609
|
+
}
|
|
8610
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8611
|
+
else if (character === open) depth += 1;
|
|
8612
|
+
else if (character === close) {
|
|
8613
|
+
depth -= 1;
|
|
8614
|
+
if (depth === 0) return index;
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
return -1;
|
|
8618
|
+
};
|
|
8619
|
+
//#endregion
|
|
8620
|
+
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
8621
|
+
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
8622
|
+
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
8623
|
+
const AUTH_COOKIE_SET_CALL_PATTERN = new RegExp(`(?:\\.cookies\\.set|cookies\\(\\s*\\)\\.set|\\.cookie)\\s*\\(\\s*${AUTH_COOKIE_NAME_LITERAL}`, "gi");
|
|
8624
|
+
const HTTP_ONLY_DISABLED_PATTERN = /httpOnly\s*:\s*false\b/i;
|
|
8625
|
+
const STRING_LITERAL_PATTERN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g;
|
|
8626
|
+
const blankStringContents = (text) => {
|
|
8627
|
+
const characters = text.split("");
|
|
8628
|
+
let index = 0;
|
|
8629
|
+
let stringDelimiter = null;
|
|
8630
|
+
while (index < text.length) {
|
|
8631
|
+
const character = text[index];
|
|
8632
|
+
if (stringDelimiter !== null) {
|
|
8633
|
+
if (character === "\\") {
|
|
8634
|
+
index += 2;
|
|
8635
|
+
continue;
|
|
8636
|
+
}
|
|
8637
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
8638
|
+
else if (character !== "\n") characters[index] = " ";
|
|
8639
|
+
index += 1;
|
|
8640
|
+
continue;
|
|
8641
|
+
}
|
|
8642
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8643
|
+
index += 1;
|
|
8644
|
+
}
|
|
8645
|
+
return characters.join("");
|
|
8646
|
+
};
|
|
8647
|
+
const COOKIE_CONFIG_OPENER_PATTERN = /cookie\s*:\s*\{/gi;
|
|
8648
|
+
const CLIENT_AUTH_COOKIE_WRITE_PATTERN = new RegExp(`document\\.cookie\\s*=\\s*[\`"'][^\`"'=;]*?${AUTH_COOKIE_NAME_TOKEN}[^\`"'=;]*=`, "gi");
|
|
8649
|
+
const countTopLevelArguments = (argumentsSource) => {
|
|
8650
|
+
if (argumentsSource.trim().length === 0) return 0;
|
|
8651
|
+
let depth = 0;
|
|
8652
|
+
let stringDelimiter = null;
|
|
8653
|
+
let count = 1;
|
|
8654
|
+
for (let index = 0; index < argumentsSource.length; index += 1) {
|
|
8655
|
+
const character = argumentsSource[index];
|
|
8656
|
+
if (stringDelimiter !== null) {
|
|
8657
|
+
if (character === "\\") index += 1;
|
|
8658
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
8659
|
+
continue;
|
|
8660
|
+
}
|
|
8661
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
8662
|
+
else if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
8663
|
+
else if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
8664
|
+
else if (character === "," && depth === 0) count += 1;
|
|
8665
|
+
}
|
|
8666
|
+
return count;
|
|
8667
|
+
};
|
|
8668
|
+
const addMatchFindings = (content, pattern, message, isInsecure, findings) => {
|
|
8669
|
+
pattern.lastIndex = 0;
|
|
8670
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
8671
|
+
if (!isInsecure(match.index, match[0])) continue;
|
|
8672
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8673
|
+
findings.push({
|
|
8674
|
+
message,
|
|
8675
|
+
line: location.line,
|
|
8676
|
+
column: location.column
|
|
8677
|
+
});
|
|
8678
|
+
}
|
|
8679
|
+
};
|
|
8680
|
+
const insecureSessionCookie = defineRule({
|
|
8681
|
+
id: "insecure-session-cookie",
|
|
8682
|
+
title: "Auth cookie missing HttpOnly protection",
|
|
8683
|
+
severity: "warn",
|
|
8684
|
+
recommendation: "Set auth/session cookies server-side with `httpOnly: true`, `secure: true`, and `sameSite`. Cookies set via `document.cookie` or with `httpOnly: false` are readable by any XSS payload and can be stolen.",
|
|
8685
|
+
scan: (file) => {
|
|
8686
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
8687
|
+
const content = getScannableContent(file);
|
|
8688
|
+
if (!/cookie/i.test(content)) return [];
|
|
8689
|
+
const findings = [];
|
|
8690
|
+
const message = "An auth/session cookie is exposed to JavaScript (set via document.cookie, with httpOnly: false, or without cookie options), letting an XSS payload steal it.";
|
|
8691
|
+
AUTH_COOKIE_SET_CALL_PATTERN.lastIndex = 0;
|
|
8692
|
+
for (let match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content); match !== null; match = AUTH_COOKIE_SET_CALL_PATTERN.exec(content)) {
|
|
8693
|
+
const openParenIndex = match.index + match[0].lastIndexOf("(");
|
|
8694
|
+
const closeParenIndex = findMatchingBracket(content, openParenIndex);
|
|
8695
|
+
if (closeParenIndex < 0) continue;
|
|
8696
|
+
const argumentsSource = content.slice(openParenIndex + 1, closeParenIndex);
|
|
8697
|
+
const hasNoOptions = countTopLevelArguments(argumentsSource) < 3;
|
|
8698
|
+
const argumentsWithoutStrings = argumentsSource.replace(STRING_LITERAL_PATTERN, "");
|
|
8699
|
+
if (!hasNoOptions && !HTTP_ONLY_DISABLED_PATTERN.test(argumentsWithoutStrings)) continue;
|
|
8700
|
+
const location = getLocationAtIndex(content, match.index);
|
|
8701
|
+
findings.push({
|
|
8702
|
+
message,
|
|
8703
|
+
line: location.line,
|
|
8704
|
+
column: location.column
|
|
8705
|
+
});
|
|
8706
|
+
}
|
|
8707
|
+
const blankedContent = blankStringContents(content);
|
|
8708
|
+
COOKIE_CONFIG_OPENER_PATTERN.lastIndex = 0;
|
|
8709
|
+
for (let match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent); match !== null; match = COOKIE_CONFIG_OPENER_PATTERN.exec(blankedContent)) {
|
|
8710
|
+
const braceIndex = match.index + match[0].length - 1;
|
|
8711
|
+
const closeBraceIndex = findMatchingBracket(blankedContent, braceIndex);
|
|
8712
|
+
const block = closeBraceIndex >= 0 ? blankedContent.slice(braceIndex, closeBraceIndex) : blankedContent.slice(braceIndex, braceIndex + 400);
|
|
8713
|
+
if (!HTTP_ONLY_DISABLED_PATTERN.test(block)) continue;
|
|
8714
|
+
const location = getLocationAtIndex(blankedContent, match.index);
|
|
8715
|
+
findings.push({
|
|
8716
|
+
message,
|
|
8717
|
+
line: location.line,
|
|
8718
|
+
column: location.column
|
|
8719
|
+
});
|
|
8720
|
+
}
|
|
8721
|
+
addMatchFindings(content, CLIENT_AUTH_COOKIE_WRITE_PATTERN, message, () => true, findings);
|
|
8722
|
+
return findings;
|
|
8723
|
+
}
|
|
8724
|
+
});
|
|
8725
|
+
//#endregion
|
|
8498
8726
|
//#region src/plugin/constants/event-handlers.ts
|
|
8499
8727
|
const MOUSE_EVENT_HANDLERS = [
|
|
8500
8728
|
"onClick",
|
|
@@ -10624,7 +10852,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10624
10852
|
});
|
|
10625
10853
|
//#endregion
|
|
10626
10854
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10627
|
-
const MESSAGE$
|
|
10855
|
+
const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10628
10856
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10629
10857
|
"code",
|
|
10630
10858
|
"pre",
|
|
@@ -10660,7 +10888,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10660
10888
|
if (isInsideLiteralTextTag(node)) return;
|
|
10661
10889
|
context.report({
|
|
10662
10890
|
node,
|
|
10663
|
-
message: MESSAGE$
|
|
10891
|
+
message: MESSAGE$45
|
|
10664
10892
|
});
|
|
10665
10893
|
} })
|
|
10666
10894
|
});
|
|
@@ -10691,7 +10919,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10691
10919
|
};
|
|
10692
10920
|
//#endregion
|
|
10693
10921
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10694
|
-
const MESSAGE$
|
|
10922
|
+
const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10695
10923
|
const CONTEXT_MODULES$1 = [
|
|
10696
10924
|
"react",
|
|
10697
10925
|
"use-context-selector",
|
|
@@ -10789,7 +11017,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10789
11017
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10790
11018
|
context.report({
|
|
10791
11019
|
node: attribute,
|
|
10792
|
-
message: MESSAGE$
|
|
11020
|
+
message: MESSAGE$44
|
|
10793
11021
|
});
|
|
10794
11022
|
}
|
|
10795
11023
|
}
|
|
@@ -10875,7 +11103,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
10875
11103
|
};
|
|
10876
11104
|
//#endregion
|
|
10877
11105
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
10878
|
-
const MESSAGE$
|
|
11106
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
10879
11107
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
10880
11108
|
"icon",
|
|
10881
11109
|
"Icon",
|
|
@@ -11144,7 +11372,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11144
11372
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11145
11373
|
context.report({
|
|
11146
11374
|
node,
|
|
11147
|
-
message: MESSAGE$
|
|
11375
|
+
message: MESSAGE$43
|
|
11148
11376
|
});
|
|
11149
11377
|
}
|
|
11150
11378
|
};
|
|
@@ -11432,7 +11660,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11432
11660
|
];
|
|
11433
11661
|
//#endregion
|
|
11434
11662
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11435
|
-
const MESSAGE$
|
|
11663
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11436
11664
|
const isDataArrayPropName = (propName) => {
|
|
11437
11665
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11438
11666
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11516,7 +11744,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11516
11744
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11517
11745
|
context.report({
|
|
11518
11746
|
node,
|
|
11519
|
-
message: MESSAGE$
|
|
11747
|
+
message: MESSAGE$42
|
|
11520
11748
|
});
|
|
11521
11749
|
}
|
|
11522
11750
|
};
|
|
@@ -11774,7 +12002,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11774
12002
|
]);
|
|
11775
12003
|
//#endregion
|
|
11776
12004
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11777
|
-
const MESSAGE$
|
|
12005
|
+
const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11778
12006
|
const isAccessorPredicateName = (propName) => {
|
|
11779
12007
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11780
12008
|
if (propName.length <= prefix.length) continue;
|
|
@@ -11980,7 +12208,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
11980
12208
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
11981
12209
|
context.report({
|
|
11982
12210
|
node,
|
|
11983
|
-
message: MESSAGE$
|
|
12211
|
+
message: MESSAGE$41
|
|
11984
12212
|
});
|
|
11985
12213
|
}
|
|
11986
12214
|
};
|
|
@@ -12200,7 +12428,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12200
12428
|
];
|
|
12201
12429
|
//#endregion
|
|
12202
12430
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12203
|
-
const MESSAGE$
|
|
12431
|
+
const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12204
12432
|
const isConfigObjectPropName = (propName) => {
|
|
12205
12433
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12206
12434
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12288,7 +12516,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12288
12516
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12289
12517
|
context.report({
|
|
12290
12518
|
node,
|
|
12291
|
-
message: MESSAGE$
|
|
12519
|
+
message: MESSAGE$40
|
|
12292
12520
|
});
|
|
12293
12521
|
}
|
|
12294
12522
|
};
|
|
@@ -12296,7 +12524,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12296
12524
|
});
|
|
12297
12525
|
//#endregion
|
|
12298
12526
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12299
|
-
const MESSAGE$
|
|
12527
|
+
const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12300
12528
|
const JAVASCRIPT_URL_PATTERN = /j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i;
|
|
12301
12529
|
const resolveSettings$28 = (settings) => {
|
|
12302
12530
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12337,7 +12565,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12337
12565
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12338
12566
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12339
12567
|
node: attribute,
|
|
12340
|
-
message: MESSAGE$
|
|
12568
|
+
message: MESSAGE$39
|
|
12341
12569
|
});
|
|
12342
12570
|
}
|
|
12343
12571
|
} };
|
|
@@ -12652,7 +12880,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12652
12880
|
});
|
|
12653
12881
|
//#endregion
|
|
12654
12882
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12655
|
-
const MESSAGE$
|
|
12883
|
+
const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
|
|
12656
12884
|
const resolveSettings$25 = (settings) => {
|
|
12657
12885
|
const reactDoctor = settings?.["react-doctor"];
|
|
12658
12886
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12693,18 +12921,77 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12693
12921
|
}
|
|
12694
12922
|
context.report({
|
|
12695
12923
|
node: attribute,
|
|
12696
|
-
message: MESSAGE$
|
|
12924
|
+
message: MESSAGE$38
|
|
12697
12925
|
});
|
|
12698
12926
|
}
|
|
12699
12927
|
} };
|
|
12700
12928
|
}
|
|
12701
12929
|
});
|
|
12702
12930
|
//#endregion
|
|
12931
|
+
//#region src/plugin/rules/security-scan/jwt-insecure-verification.ts
|
|
12932
|
+
const NONE_ALGORITHM_PATTERN = /\b(?:alg|algorithms?)\s*:\s*\[?\s*["'`]none["'`]/gi;
|
|
12933
|
+
const isIndexInsideStringLiteral = (content, index) => {
|
|
12934
|
+
let stringDelimiter = null;
|
|
12935
|
+
const templateExpressionDepths = [];
|
|
12936
|
+
for (let cursor = 0; cursor < index; cursor += 1) {
|
|
12937
|
+
const character = content[cursor];
|
|
12938
|
+
if (stringDelimiter === "`") {
|
|
12939
|
+
if (character === "\\") cursor += 1;
|
|
12940
|
+
else if (character === "`") stringDelimiter = null;
|
|
12941
|
+
else if (character === "$" && content[cursor + 1] === "{") {
|
|
12942
|
+
templateExpressionDepths.push(0);
|
|
12943
|
+
stringDelimiter = null;
|
|
12944
|
+
cursor += 1;
|
|
12945
|
+
}
|
|
12946
|
+
continue;
|
|
12947
|
+
}
|
|
12948
|
+
if (stringDelimiter !== null) {
|
|
12949
|
+
if (character === "\\") cursor += 1;
|
|
12950
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12951
|
+
continue;
|
|
12952
|
+
}
|
|
12953
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12954
|
+
else if (templateExpressionDepths.length > 0) {
|
|
12955
|
+
const top = templateExpressionDepths.length - 1;
|
|
12956
|
+
if (character === "{") templateExpressionDepths[top] += 1;
|
|
12957
|
+
else if (character === "}") if (templateExpressionDepths[top] === 0) {
|
|
12958
|
+
templateExpressionDepths.pop();
|
|
12959
|
+
stringDelimiter = "`";
|
|
12960
|
+
} else templateExpressionDepths[top] -= 1;
|
|
12961
|
+
}
|
|
12962
|
+
}
|
|
12963
|
+
return stringDelimiter !== null;
|
|
12964
|
+
};
|
|
12965
|
+
const jwtInsecureVerification = defineRule({
|
|
12966
|
+
id: "jwt-insecure-verification",
|
|
12967
|
+
title: "JWT verified with the 'none' algorithm",
|
|
12968
|
+
severity: "error",
|
|
12969
|
+
recommendation: "Never accept the `none` algorithm; it disables signature verification and lets any forged token through. Pin the real algorithm(s) explicitly (`jwt.verify(token, key, { algorithms: ['RS256'] })`).",
|
|
12970
|
+
scan: (file) => {
|
|
12971
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
12972
|
+
const content = getScannableContent(file);
|
|
12973
|
+
if (!/\bjwt\b|jsonwebtoken|\bjose\b/i.test(content)) return [];
|
|
12974
|
+
const findings = [];
|
|
12975
|
+
NONE_ALGORITHM_PATTERN.lastIndex = 0;
|
|
12976
|
+
for (let noneMatch = NONE_ALGORITHM_PATTERN.exec(content); noneMatch !== null; noneMatch = NONE_ALGORITHM_PATTERN.exec(content)) {
|
|
12977
|
+
if (isIndexInsideStringLiteral(content, noneMatch.index)) continue;
|
|
12978
|
+
const location = getLocationAtIndex(content, noneMatch.index);
|
|
12979
|
+
findings.push({
|
|
12980
|
+
message: "JWT is configured with the 'none' algorithm, which disables signature verification, so any forged token is accepted.",
|
|
12981
|
+
line: location.line,
|
|
12982
|
+
column: location.column
|
|
12983
|
+
});
|
|
12984
|
+
}
|
|
12985
|
+
return findings;
|
|
12986
|
+
}
|
|
12987
|
+
});
|
|
12988
|
+
//#endregion
|
|
12703
12989
|
//#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
|
|
12704
12990
|
const keyLifecycleRisk = defineRule({
|
|
12705
12991
|
id: "key-lifecycle-risk",
|
|
12706
12992
|
title: "Long-lived key material in repository",
|
|
12707
12993
|
severity: "error",
|
|
12994
|
+
committedFilesOnly: true,
|
|
12708
12995
|
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
12709
12996
|
scan: scanByPattern({
|
|
12710
12997
|
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
@@ -12862,7 +13149,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
12862
13149
|
});
|
|
12863
13150
|
//#endregion
|
|
12864
13151
|
//#region src/plugin/rules/a11y/lang.ts
|
|
12865
|
-
const MESSAGE$
|
|
13152
|
+
const MESSAGE$37 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
12866
13153
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
12867
13154
|
"aa",
|
|
12868
13155
|
"ab",
|
|
@@ -13074,7 +13361,7 @@ const lang = defineRule({
|
|
|
13074
13361
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13075
13362
|
context.report({
|
|
13076
13363
|
node: langAttr,
|
|
13077
|
-
message: MESSAGE$
|
|
13364
|
+
message: MESSAGE$37
|
|
13078
13365
|
});
|
|
13079
13366
|
return;
|
|
13080
13367
|
}
|
|
@@ -13083,7 +13370,7 @@ const lang = defineRule({
|
|
|
13083
13370
|
if (value === null) return;
|
|
13084
13371
|
if (!isValidLangTag(value)) context.report({
|
|
13085
13372
|
node: langAttr,
|
|
13086
|
-
message: MESSAGE$
|
|
13373
|
+
message: MESSAGE$37
|
|
13087
13374
|
});
|
|
13088
13375
|
} })
|
|
13089
13376
|
});
|
|
@@ -13127,7 +13414,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13127
13414
|
});
|
|
13128
13415
|
//#endregion
|
|
13129
13416
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13130
|
-
const MESSAGE$
|
|
13417
|
+
const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13131
13418
|
const DEFAULT_AUDIO = ["audio"];
|
|
13132
13419
|
const DEFAULT_VIDEO = ["video"];
|
|
13133
13420
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13168,7 +13455,7 @@ const mediaHasCaption = defineRule({
|
|
|
13168
13455
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13169
13456
|
context.report({
|
|
13170
13457
|
node: node.name,
|
|
13171
|
-
message: MESSAGE$
|
|
13458
|
+
message: MESSAGE$36
|
|
13172
13459
|
});
|
|
13173
13460
|
return;
|
|
13174
13461
|
}
|
|
@@ -13185,7 +13472,7 @@ const mediaHasCaption = defineRule({
|
|
|
13185
13472
|
return kindValue.value.toLowerCase() === "captions";
|
|
13186
13473
|
})) context.report({
|
|
13187
13474
|
node: node.name,
|
|
13188
|
-
message: MESSAGE$
|
|
13475
|
+
message: MESSAGE$36
|
|
13189
13476
|
});
|
|
13190
13477
|
} };
|
|
13191
13478
|
}
|
|
@@ -14986,7 +15273,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
14986
15273
|
});
|
|
14987
15274
|
//#endregion
|
|
14988
15275
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
14989
|
-
const MESSAGE$
|
|
15276
|
+
const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
14990
15277
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
14991
15278
|
const noAccessKey = defineRule({
|
|
14992
15279
|
id: "no-access-key",
|
|
@@ -15003,7 +15290,7 @@ const noAccessKey = defineRule({
|
|
|
15003
15290
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15004
15291
|
context.report({
|
|
15005
15292
|
node: accessKey,
|
|
15006
|
-
message: MESSAGE$
|
|
15293
|
+
message: MESSAGE$35
|
|
15007
15294
|
});
|
|
15008
15295
|
return;
|
|
15009
15296
|
}
|
|
@@ -15013,7 +15300,7 @@ const noAccessKey = defineRule({
|
|
|
15013
15300
|
if (isUndefinedIdentifier(expression)) return;
|
|
15014
15301
|
context.report({
|
|
15015
15302
|
node: accessKey,
|
|
15016
|
-
message: MESSAGE$
|
|
15303
|
+
message: MESSAGE$35
|
|
15017
15304
|
});
|
|
15018
15305
|
}
|
|
15019
15306
|
} })
|
|
@@ -15496,7 +15783,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15496
15783
|
});
|
|
15497
15784
|
//#endregion
|
|
15498
15785
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15499
|
-
const MESSAGE$
|
|
15786
|
+
const MESSAGE$34 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15500
15787
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15501
15788
|
id: "no-aria-hidden-on-focusable",
|
|
15502
15789
|
title: "aria-hidden on focusable element",
|
|
@@ -15523,7 +15810,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15523
15810
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15524
15811
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15525
15812
|
node: ariaHidden,
|
|
15526
|
-
message: MESSAGE$
|
|
15813
|
+
message: MESSAGE$34
|
|
15527
15814
|
});
|
|
15528
15815
|
} })
|
|
15529
15816
|
});
|
|
@@ -15891,7 +16178,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
15891
16178
|
});
|
|
15892
16179
|
//#endregion
|
|
15893
16180
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
15894
|
-
const MESSAGE$
|
|
16181
|
+
const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
|
|
15895
16182
|
const SECOND_INDEX_METHODS = new Set([
|
|
15896
16183
|
"every",
|
|
15897
16184
|
"filter",
|
|
@@ -16095,7 +16382,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16095
16382
|
}
|
|
16096
16383
|
context.report({
|
|
16097
16384
|
node: keyAttribute,
|
|
16098
|
-
message: MESSAGE$
|
|
16385
|
+
message: MESSAGE$33
|
|
16099
16386
|
});
|
|
16100
16387
|
},
|
|
16101
16388
|
CallExpression(node) {
|
|
@@ -16115,15 +16402,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16115
16402
|
if (propName !== "key") continue;
|
|
16116
16403
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16117
16404
|
node: property,
|
|
16118
|
-
message: MESSAGE$
|
|
16405
|
+
message: MESSAGE$33
|
|
16119
16406
|
});
|
|
16120
16407
|
}
|
|
16121
16408
|
}
|
|
16122
16409
|
})
|
|
16123
16410
|
});
|
|
16124
16411
|
//#endregion
|
|
16412
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16413
|
+
const MESSAGE$32 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16414
|
+
const noAsyncEffectCallback = defineRule({
|
|
16415
|
+
id: "no-async-effect-callback",
|
|
16416
|
+
title: "Async effect callback",
|
|
16417
|
+
severity: "warn",
|
|
16418
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16419
|
+
create: (context) => ({ CallExpression(node) {
|
|
16420
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16421
|
+
const callback = getEffectCallback(node);
|
|
16422
|
+
if (!callback) return;
|
|
16423
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16424
|
+
if (!callback.async) return;
|
|
16425
|
+
context.report({
|
|
16426
|
+
node: callback,
|
|
16427
|
+
message: MESSAGE$32
|
|
16428
|
+
});
|
|
16429
|
+
} })
|
|
16430
|
+
});
|
|
16431
|
+
//#endregion
|
|
16125
16432
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16126
|
-
const MESSAGE$
|
|
16433
|
+
const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16127
16434
|
const resolveSettings$21 = (settings) => {
|
|
16128
16435
|
const reactDoctor = settings?.["react-doctor"];
|
|
16129
16436
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16179,7 +16486,7 @@ const noAutofocus = defineRule({
|
|
|
16179
16486
|
}
|
|
16180
16487
|
context.report({
|
|
16181
16488
|
node: autoFocusAttribute,
|
|
16182
|
-
message: MESSAGE$
|
|
16489
|
+
message: MESSAGE$31
|
|
16183
16490
|
});
|
|
16184
16491
|
} };
|
|
16185
16492
|
}
|
|
@@ -16429,6 +16736,109 @@ const noBarrelImport = defineRule({
|
|
|
16429
16736
|
}
|
|
16430
16737
|
});
|
|
16431
16738
|
//#endregion
|
|
16739
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16740
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16741
|
+
"FunctionDeclaration",
|
|
16742
|
+
"FunctionExpression",
|
|
16743
|
+
"ArrowFunctionExpression",
|
|
16744
|
+
"ClassDeclaration",
|
|
16745
|
+
"ClassExpression"
|
|
16746
|
+
]);
|
|
16747
|
+
const isReactImport$1 = (symbol) => {
|
|
16748
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16749
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16750
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16751
|
+
return importDeclaration.source.value === "react";
|
|
16752
|
+
};
|
|
16753
|
+
const getImportedName = (symbol) => {
|
|
16754
|
+
if (symbol.kind !== "import") return null;
|
|
16755
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16756
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16757
|
+
};
|
|
16758
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16759
|
+
if (symbol.kind !== "import") return false;
|
|
16760
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16761
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16762
|
+
};
|
|
16763
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16764
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16765
|
+
const symbol = scopes.symbolFor(callee);
|
|
16766
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16767
|
+
};
|
|
16768
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16769
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16770
|
+
if (callee.computed) return false;
|
|
16771
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16772
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16773
|
+
if (callee.property.name !== "createElement") return false;
|
|
16774
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16775
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16776
|
+
};
|
|
16777
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16778
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16779
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16780
|
+
};
|
|
16781
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16782
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16783
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16784
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16785
|
+
const nodeRecord = node;
|
|
16786
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16787
|
+
if (key === "parent") continue;
|
|
16788
|
+
const child = nodeRecord[key];
|
|
16789
|
+
if (Array.isArray(child)) {
|
|
16790
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16791
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16792
|
+
}
|
|
16793
|
+
return false;
|
|
16794
|
+
};
|
|
16795
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16796
|
+
//#endregion
|
|
16797
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16798
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16799
|
+
//#endregion
|
|
16800
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16801
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16802
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16803
|
+
const declaration = symbol.declarationNode;
|
|
16804
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16805
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16806
|
+
return false;
|
|
16807
|
+
};
|
|
16808
|
+
const noCallComponentAsFunction = defineRule({
|
|
16809
|
+
id: "no-call-component-as-function",
|
|
16810
|
+
title: "Component called as a function",
|
|
16811
|
+
severity: "warn",
|
|
16812
|
+
tags: ["test-noise"],
|
|
16813
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16814
|
+
create: (context) => {
|
|
16815
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16816
|
+
const candidateCalls = [];
|
|
16817
|
+
return {
|
|
16818
|
+
JSXOpeningElement(node) {
|
|
16819
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16820
|
+
},
|
|
16821
|
+
CallExpression(node) {
|
|
16822
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16823
|
+
node,
|
|
16824
|
+
callee: node.callee,
|
|
16825
|
+
name: node.callee.name
|
|
16826
|
+
});
|
|
16827
|
+
},
|
|
16828
|
+
"Program:exit"() {
|
|
16829
|
+
for (const candidate of candidateCalls) {
|
|
16830
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16831
|
+
if (!symbol) continue;
|
|
16832
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16833
|
+
node: candidate.node,
|
|
16834
|
+
message: message(candidate.name)
|
|
16835
|
+
});
|
|
16836
|
+
}
|
|
16837
|
+
}
|
|
16838
|
+
};
|
|
16839
|
+
}
|
|
16840
|
+
});
|
|
16841
|
+
//#endregion
|
|
16432
16842
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16433
16843
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16434
16844
|
//#endregion
|
|
@@ -16580,7 +16990,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16580
16990
|
});
|
|
16581
16991
|
//#endregion
|
|
16582
16992
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16583
|
-
const MESSAGE$
|
|
16993
|
+
const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16584
16994
|
const noChildrenProp = defineRule({
|
|
16585
16995
|
id: "no-children-prop",
|
|
16586
16996
|
title: "Children passed as a prop",
|
|
@@ -16592,7 +17002,7 @@ const noChildrenProp = defineRule({
|
|
|
16592
17002
|
if (node.name.name !== "children") return;
|
|
16593
17003
|
context.report({
|
|
16594
17004
|
node: node.name,
|
|
16595
|
-
message: MESSAGE$
|
|
17005
|
+
message: MESSAGE$30
|
|
16596
17006
|
});
|
|
16597
17007
|
},
|
|
16598
17008
|
CallExpression(node) {
|
|
@@ -16605,7 +17015,7 @@ const noChildrenProp = defineRule({
|
|
|
16605
17015
|
const propertyKey = property.key;
|
|
16606
17016
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16607
17017
|
node: propertyKey,
|
|
16608
|
-
message: MESSAGE$
|
|
17018
|
+
message: MESSAGE$30
|
|
16609
17019
|
});
|
|
16610
17020
|
}
|
|
16611
17021
|
}
|
|
@@ -16613,7 +17023,7 @@ const noChildrenProp = defineRule({
|
|
|
16613
17023
|
});
|
|
16614
17024
|
//#endregion
|
|
16615
17025
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16616
|
-
const MESSAGE$
|
|
17026
|
+
const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16617
17027
|
const noCloneElement = defineRule({
|
|
16618
17028
|
id: "no-clone-element",
|
|
16619
17029
|
title: "cloneElement makes child props fragile",
|
|
@@ -16626,7 +17036,7 @@ const noCloneElement = defineRule({
|
|
|
16626
17036
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16627
17037
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16628
17038
|
node: callee,
|
|
16629
|
-
message: MESSAGE$
|
|
17039
|
+
message: MESSAGE$29
|
|
16630
17040
|
});
|
|
16631
17041
|
return;
|
|
16632
17042
|
}
|
|
@@ -16639,7 +17049,7 @@ const noCloneElement = defineRule({
|
|
|
16639
17049
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16640
17050
|
context.report({
|
|
16641
17051
|
node: callee,
|
|
16642
|
-
message: MESSAGE$
|
|
17052
|
+
message: MESSAGE$29
|
|
16643
17053
|
});
|
|
16644
17054
|
}
|
|
16645
17055
|
} })
|
|
@@ -16688,7 +17098,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16688
17098
|
};
|
|
16689
17099
|
//#endregion
|
|
16690
17100
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16691
|
-
const MESSAGE$
|
|
17101
|
+
const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16692
17102
|
const CONTEXT_MODULES = [
|
|
16693
17103
|
"react",
|
|
16694
17104
|
"use-context-selector",
|
|
@@ -16724,7 +17134,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16724
17134
|
if (!componentOrHookName) return;
|
|
16725
17135
|
context.report({
|
|
16726
17136
|
node,
|
|
16727
|
-
message: `${MESSAGE$
|
|
17137
|
+
message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
|
|
17138
|
+
});
|
|
17139
|
+
} })
|
|
17140
|
+
});
|
|
17141
|
+
//#endregion
|
|
17142
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17143
|
+
const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17144
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17145
|
+
id: "no-create-ref-in-function-component",
|
|
17146
|
+
title: "createRef in function component",
|
|
17147
|
+
severity: "warn",
|
|
17148
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17149
|
+
create: (context) => ({ CallExpression(node) {
|
|
17150
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17151
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17152
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17153
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17154
|
+
}
|
|
17155
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17156
|
+
if (!enclosingFunction) return;
|
|
17157
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17158
|
+
if (!displayName) return;
|
|
17159
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17160
|
+
context.report({
|
|
17161
|
+
node,
|
|
17162
|
+
message: MESSAGE$27
|
|
16728
17163
|
});
|
|
16729
17164
|
} })
|
|
16730
17165
|
});
|
|
@@ -16864,7 +17299,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
16864
17299
|
});
|
|
16865
17300
|
//#endregion
|
|
16866
17301
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
16867
|
-
const MESSAGE$
|
|
17302
|
+
const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
16868
17303
|
const noDanger = defineRule({
|
|
16869
17304
|
id: "no-danger",
|
|
16870
17305
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -16877,7 +17312,7 @@ const noDanger = defineRule({
|
|
|
16877
17312
|
if (!propAttribute) return;
|
|
16878
17313
|
context.report({
|
|
16879
17314
|
node: propAttribute.name,
|
|
16880
|
-
message: MESSAGE$
|
|
17315
|
+
message: MESSAGE$26
|
|
16881
17316
|
});
|
|
16882
17317
|
},
|
|
16883
17318
|
CallExpression(node) {
|
|
@@ -16889,7 +17324,7 @@ const noDanger = defineRule({
|
|
|
16889
17324
|
const propertyKey = property.key;
|
|
16890
17325
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
16891
17326
|
node: propertyKey,
|
|
16892
|
-
message: MESSAGE$
|
|
17327
|
+
message: MESSAGE$26
|
|
16893
17328
|
});
|
|
16894
17329
|
}
|
|
16895
17330
|
}
|
|
@@ -16897,7 +17332,7 @@ const noDanger = defineRule({
|
|
|
16897
17332
|
});
|
|
16898
17333
|
//#endregion
|
|
16899
17334
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
16900
|
-
const MESSAGE$
|
|
17335
|
+
const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
16901
17336
|
const isLineBreak = (child) => {
|
|
16902
17337
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
16903
17338
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -16967,7 +17402,7 @@ const noDangerWithChildren = defineRule({
|
|
|
16967
17402
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
16968
17403
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
16969
17404
|
node: opening,
|
|
16970
|
-
message: MESSAGE$
|
|
17405
|
+
message: MESSAGE$25
|
|
16971
17406
|
});
|
|
16972
17407
|
},
|
|
16973
17408
|
CallExpression(node) {
|
|
@@ -16979,7 +17414,7 @@ const noDangerWithChildren = defineRule({
|
|
|
16979
17414
|
if (!propsShape.hasDangerously) return;
|
|
16980
17415
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
16981
17416
|
node,
|
|
16982
|
-
message: MESSAGE$
|
|
17417
|
+
message: MESSAGE$25
|
|
16983
17418
|
});
|
|
16984
17419
|
}
|
|
16985
17420
|
})
|
|
@@ -17556,7 +17991,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17556
17991
|
//#endregion
|
|
17557
17992
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17558
17993
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17559
|
-
const MESSAGE$
|
|
17994
|
+
const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17560
17995
|
const resolveSettings$20 = (settings) => {
|
|
17561
17996
|
const reactDoctor = settings?.["react-doctor"];
|
|
17562
17997
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17575,7 +18010,7 @@ const noDidMountSetState = defineRule({
|
|
|
17575
18010
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17576
18011
|
context.report({
|
|
17577
18012
|
node: node.callee,
|
|
17578
|
-
message: MESSAGE$
|
|
18013
|
+
message: MESSAGE$24
|
|
17579
18014
|
});
|
|
17580
18015
|
} };
|
|
17581
18016
|
}
|
|
@@ -17583,7 +18018,7 @@ const noDidMountSetState = defineRule({
|
|
|
17583
18018
|
//#endregion
|
|
17584
18019
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17585
18020
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17586
|
-
const MESSAGE$
|
|
18021
|
+
const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17587
18022
|
const resolveSettings$19 = (settings) => {
|
|
17588
18023
|
const reactDoctor = settings?.["react-doctor"];
|
|
17589
18024
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17602,7 +18037,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17602
18037
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17603
18038
|
context.report({
|
|
17604
18039
|
node: node.callee,
|
|
17605
|
-
message: MESSAGE$
|
|
18040
|
+
message: MESSAGE$23
|
|
17606
18041
|
});
|
|
17607
18042
|
} };
|
|
17608
18043
|
}
|
|
@@ -17625,7 +18060,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17625
18060
|
};
|
|
17626
18061
|
//#endregion
|
|
17627
18062
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17628
|
-
const MESSAGE$
|
|
18063
|
+
const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17629
18064
|
const shouldIgnoreMutation = (node) => {
|
|
17630
18065
|
let isConstructor = false;
|
|
17631
18066
|
let isInsideCallExpression = false;
|
|
@@ -17647,7 +18082,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17647
18082
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17648
18083
|
context.report({
|
|
17649
18084
|
node: reportNode,
|
|
17650
|
-
message: MESSAGE$
|
|
18085
|
+
message: MESSAGE$22
|
|
17651
18086
|
});
|
|
17652
18087
|
};
|
|
17653
18088
|
const noDirectMutationState = defineRule({
|
|
@@ -19235,7 +19670,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19235
19670
|
"ReactDOM",
|
|
19236
19671
|
"ReactDom"
|
|
19237
19672
|
]);
|
|
19238
|
-
const MESSAGE$
|
|
19673
|
+
const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19239
19674
|
const noFindDomNode = defineRule({
|
|
19240
19675
|
id: "no-find-dom-node",
|
|
19241
19676
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19246,7 +19681,7 @@ const noFindDomNode = defineRule({
|
|
|
19246
19681
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19247
19682
|
context.report({
|
|
19248
19683
|
node: callee,
|
|
19249
|
-
message: MESSAGE$
|
|
19684
|
+
message: MESSAGE$21
|
|
19250
19685
|
});
|
|
19251
19686
|
return;
|
|
19252
19687
|
}
|
|
@@ -19257,7 +19692,7 @@ const noFindDomNode = defineRule({
|
|
|
19257
19692
|
if (callee.property.name !== "findDOMNode") return;
|
|
19258
19693
|
context.report({
|
|
19259
19694
|
node: callee.property,
|
|
19260
|
-
message: MESSAGE$
|
|
19695
|
+
message: MESSAGE$21
|
|
19261
19696
|
});
|
|
19262
19697
|
}
|
|
19263
19698
|
} })
|
|
@@ -19320,64 +19755,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19320
19755
|
} })
|
|
19321
19756
|
});
|
|
19322
19757
|
//#endregion
|
|
19323
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19324
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19325
|
-
"FunctionDeclaration",
|
|
19326
|
-
"FunctionExpression",
|
|
19327
|
-
"ArrowFunctionExpression",
|
|
19328
|
-
"ClassDeclaration",
|
|
19329
|
-
"ClassExpression"
|
|
19330
|
-
]);
|
|
19331
|
-
const isReactImport$1 = (symbol) => {
|
|
19332
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19333
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19334
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19335
|
-
return importDeclaration.source.value === "react";
|
|
19336
|
-
};
|
|
19337
|
-
const getImportedName = (symbol) => {
|
|
19338
|
-
if (symbol.kind !== "import") return null;
|
|
19339
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19340
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19341
|
-
};
|
|
19342
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19343
|
-
if (symbol.kind !== "import") return false;
|
|
19344
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19345
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19346
|
-
};
|
|
19347
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19348
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19349
|
-
const symbol = scopes.symbolFor(callee);
|
|
19350
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19351
|
-
};
|
|
19352
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19353
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19354
|
-
if (callee.computed) return false;
|
|
19355
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19356
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19357
|
-
if (callee.property.name !== "createElement") return false;
|
|
19358
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19359
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19360
|
-
};
|
|
19361
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19362
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19363
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19364
|
-
};
|
|
19365
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19366
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19367
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19368
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19369
|
-
const nodeRecord = node;
|
|
19370
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19371
|
-
if (key === "parent") continue;
|
|
19372
|
-
const child = nodeRecord[key];
|
|
19373
|
-
if (Array.isArray(child)) {
|
|
19374
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19375
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19376
|
-
}
|
|
19377
|
-
return false;
|
|
19378
|
-
};
|
|
19379
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19380
|
-
//#endregion
|
|
19381
19758
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19382
19759
|
const noGiantComponent = defineRule({
|
|
19383
19760
|
id: "no-giant-component",
|
|
@@ -19556,6 +19933,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19556
19933
|
} })
|
|
19557
19934
|
});
|
|
19558
19935
|
//#endregion
|
|
19936
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
19937
|
+
const MESSAGE$20 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
19938
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
19939
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
19940
|
+
title: "Lazy image with high fetchPriority",
|
|
19941
|
+
severity: "warn",
|
|
19942
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
19943
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
19944
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
19945
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
19946
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
19947
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
19948
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
19949
|
+
context.report({
|
|
19950
|
+
node: node.name,
|
|
19951
|
+
message: MESSAGE$20
|
|
19952
|
+
});
|
|
19953
|
+
} })
|
|
19954
|
+
});
|
|
19955
|
+
//#endregion
|
|
19559
19956
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19560
19957
|
const noInitializeState = defineRule({
|
|
19561
19958
|
id: "no-initialize-state",
|
|
@@ -19785,6 +20182,29 @@ const noIsMounted = defineRule({
|
|
|
19785
20182
|
} })
|
|
19786
20183
|
});
|
|
19787
20184
|
//#endregion
|
|
20185
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20186
|
+
const MESSAGE$19 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20187
|
+
const isJsonMethodCall = (node, method) => {
|
|
20188
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20189
|
+
const callee = node.callee;
|
|
20190
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20191
|
+
};
|
|
20192
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20193
|
+
id: "no-json-parse-stringify-clone",
|
|
20194
|
+
title: "JSON parse/stringify deep clone",
|
|
20195
|
+
severity: "warn",
|
|
20196
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20197
|
+
create: (context) => ({ CallExpression(node) {
|
|
20198
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20199
|
+
const firstArgument = node.arguments?.[0];
|
|
20200
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20201
|
+
context.report({
|
|
20202
|
+
node,
|
|
20203
|
+
message: MESSAGE$19
|
|
20204
|
+
});
|
|
20205
|
+
} })
|
|
20206
|
+
});
|
|
20207
|
+
//#endregion
|
|
19788
20208
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19789
20209
|
const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19790
20210
|
const isJsxElementTypeReference = (node) => {
|
|
@@ -20107,9 +20527,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20107
20527
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20108
20528
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20109
20529
|
//#endregion
|
|
20110
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20111
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20112
|
-
//#endregion
|
|
20113
20530
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20114
20531
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20115
20532
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -27035,6 +27452,8 @@ const publicEnvSecretName = defineRule({
|
|
|
27035
27452
|
});
|
|
27036
27453
|
//#endregion
|
|
27037
27454
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
27455
|
+
const TANSTACK_QUERY_PACKAGE_PATTERN = /^@tanstack\/[\w-]*query[\w-]*$/;
|
|
27456
|
+
const isTanstackQuerySource = (source) => TANSTACK_QUERY_PACKAGE_PATTERN.test(source) || source === "react-query";
|
|
27038
27457
|
const queryDestructureResult = defineRule({
|
|
27039
27458
|
id: "query-destructure-result",
|
|
27040
27459
|
title: "Whole query result subscribes to every field",
|
|
@@ -27047,6 +27466,8 @@ const queryDestructureResult = defineRule({
|
|
|
27047
27466
|
if (!node.init || !isNodeOfType(node.init, "CallExpression")) return;
|
|
27048
27467
|
const calleeName = isNodeOfType(node.init.callee, "Identifier") ? node.init.callee.name : null;
|
|
27049
27468
|
if (!calleeName || !TANSTACK_QUERY_HOOKS.has(calleeName)) return;
|
|
27469
|
+
const importSource = getImportSourceForName(node, calleeName);
|
|
27470
|
+
if (importSource !== null && !isTanstackQuerySource(importSource)) return;
|
|
27050
27471
|
context.report({
|
|
27051
27472
|
node: node.id,
|
|
27052
27473
|
message: `Destructure ${calleeName}() results instead of assigning the whole query object, so TanStack Query only subscribes to the fields you use.`
|
|
@@ -27889,6 +28310,7 @@ const repositorySecretFile = defineRule({
|
|
|
27889
28310
|
id: "repository-secret-file",
|
|
27890
28311
|
title: "Secret file checked into repository",
|
|
27891
28312
|
severity: "error",
|
|
28313
|
+
committedFilesOnly: true,
|
|
27892
28314
|
recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
|
|
27893
28315
|
scan: (file) => {
|
|
27894
28316
|
if (!isRepositorySecretFilePath(file.relativePath)) return [];
|
|
@@ -27905,6 +28327,20 @@ const repositorySecretFile = defineRule({
|
|
|
27905
28327
|
}
|
|
27906
28328
|
});
|
|
27907
28329
|
//#endregion
|
|
28330
|
+
//#region src/plugin/rules/security-scan/request-body-mass-assignment.ts
|
|
28331
|
+
const REQUEST_INPUT_SOURCE = "(?:req|request|ctx\\.req|ctx\\.request)\\.(?:body|query|params)|await\\s+(?:req|request)\\.json\\(\\s*\\)";
|
|
28332
|
+
const requestBodyMassAssignment = defineRule({
|
|
28333
|
+
id: "request-body-mass-assignment",
|
|
28334
|
+
title: "Request input spread without field allowlist",
|
|
28335
|
+
severity: "warn",
|
|
28336
|
+
recommendation: "Assign explicit, allowlisted fields (or validate with a strict schema and no `.passthrough()`) instead of spreading/merging request input. Otherwise the client can set ownership, role, or price columns (mass assignment) or pollute the prototype.",
|
|
28337
|
+
scan: scanByPattern({
|
|
28338
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
28339
|
+
pattern: [new RegExp(`\\.\\.\\.\\s*(?:${REQUEST_INPUT_SOURCE})`, "i"), new RegExp(`(?:Object\\.assign\\s*\\(|_\\.(?:merge|mergeWith|defaultsDeep)\\s*\\(|(?:^|[^.\\w])(?:merge|defaultsDeep)\\s*\\()[\\s\\S]{0,80}?(?:${REQUEST_INPUT_SOURCE})`, "i")],
|
|
28340
|
+
message: "Request input is spread or merged into an object without a field allowlist, enabling mass assignment (client-set owner/role/price fields) or prototype pollution."
|
|
28341
|
+
})
|
|
28342
|
+
});
|
|
28343
|
+
//#endregion
|
|
27908
28344
|
//#region src/plugin/utils/function-body-has-return-with-value.ts
|
|
27909
28345
|
const functionBodyHasReturnWithValue = (functionNode) => {
|
|
27910
28346
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
@@ -34330,6 +34766,17 @@ const scope = defineRule({
|
|
|
34330
34766
|
});
|
|
34331
34767
|
} })
|
|
34332
34768
|
});
|
|
34769
|
+
const secretInFallback = defineRule({
|
|
34770
|
+
id: "secret-in-fallback",
|
|
34771
|
+
title: "Hardcoded secret fallback for env var",
|
|
34772
|
+
severity: "error",
|
|
34773
|
+
recommendation: "Remove the literal fallback and fail closed (throw when the variable is unset). The hardcoded value is a committed secret, and the `??`/`||` default makes the app run with it in any environment that forgot to set the var.",
|
|
34774
|
+
scan: scanByPattern({
|
|
34775
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
34776
|
+
pattern: /\bprocess\.env\.(?![A-Z0-9_]*(?:PUBLIC|PUBLISHABLE|ANON)\b)[A-Z][A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|PRIVATE_KEY|API_?KEY|APIKEY|ACCESS_KEY|CLIENT_SECRET|CREDENTIAL|SIGNING_KEY|ENCRYPTION_KEY|WEBHOOK_SECRET|SERVICE_ROLE)[A-Z0-9_]*(?<!_(?:NAME|HEADER|ENDPOINT|URL|URI|ID|PREFIX|SUFFIX|PARAM|PARAMS|FIELD|ISSUER|AUDIENCE|ALGORITHM|ALG|REGION|BUCKET|HOST|HOSTNAME|PORT|PATH|VERSION|SCOPE|TYPE|FORMAT|EXPIRY|TTL))\s*(?:\?\?|\|\|)\s*(["'`])(?!(?:changeme|change[_-]?me|placeholder|your[_-]|example|sample|dummy|development|local|todo|replace[_-]?me|https?:\/\/|x{3,}|\*{3,}))[^"'`\n]{8,}\1/i,
|
|
34777
|
+
message: "A secret env var has a hardcoded string fallback: the literal is a committed secret and the app fails open (uses it) when the variable is unset."
|
|
34778
|
+
})
|
|
34779
|
+
});
|
|
34333
34780
|
//#endregion
|
|
34334
34781
|
//#region src/plugin/rules/react-builtins/self-closing-comp.ts
|
|
34335
34782
|
const MESSAGE$2 = "This tag has no children, so the closing tag adds noise without changing output.";
|
|
@@ -35139,8 +35586,11 @@ const supabaseClientOwnedAuthzField = defineRule({
|
|
|
35139
35586
|
})
|
|
35140
35587
|
});
|
|
35141
35588
|
//#endregion
|
|
35589
|
+
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|
|
35590
|
+
const isSupabaseMigrationPath = (relativePath) => /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
|
|
35591
|
+
//#endregion
|
|
35142
35592
|
//#region src/plugin/rules/security-scan/utils/is-sql-path.ts
|
|
35143
|
-
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") ||
|
|
35593
|
+
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || isSupabaseMigrationPath(relativePath);
|
|
35144
35594
|
const supabaseRlsPolicyRisk = defineRule({
|
|
35145
35595
|
id: "supabase-rls-policy-risk",
|
|
35146
35596
|
title: "Permissive Supabase RLS policy",
|
|
@@ -35158,6 +35608,210 @@ const supabaseRlsPolicyRisk = defineRule({
|
|
|
35158
35608
|
})
|
|
35159
35609
|
});
|
|
35160
35610
|
//#endregion
|
|
35611
|
+
//#region src/plugin/rules/security-scan/utils/sanitize-sql-for-scan.ts
|
|
35612
|
+
const DOLLAR_QUOTE_TAG_PATTERN = /^\$[A-Za-z_]?\w*\$/;
|
|
35613
|
+
const CODE_BODY_KEYWORDS = new Set([
|
|
35614
|
+
"do",
|
|
35615
|
+
"plpgsql",
|
|
35616
|
+
"sql",
|
|
35617
|
+
"plpython3u",
|
|
35618
|
+
"plpythonu",
|
|
35619
|
+
"plperl",
|
|
35620
|
+
"plperlu",
|
|
35621
|
+
"plv8"
|
|
35622
|
+
]);
|
|
35623
|
+
const precedingKeyword = (content, beforeIndex) => {
|
|
35624
|
+
let lookBack = beforeIndex - 1;
|
|
35625
|
+
while (lookBack >= 0 && /\s/.test(content[lookBack] ?? "")) lookBack -= 1;
|
|
35626
|
+
let wordStart = lookBack;
|
|
35627
|
+
while (wordStart >= 0 && /[A-Za-z0-9_]/.test(content[wordStart] ?? "")) wordStart -= 1;
|
|
35628
|
+
return content.slice(wordStart + 1, lookBack + 1).toLowerCase();
|
|
35629
|
+
};
|
|
35630
|
+
const blankCodeBodyInterior = (content, characters, start, end) => {
|
|
35631
|
+
let index = start;
|
|
35632
|
+
let inExecuteStatement = false;
|
|
35633
|
+
while (index < end) {
|
|
35634
|
+
const character = content[index];
|
|
35635
|
+
if (character === ";") {
|
|
35636
|
+
inExecuteStatement = false;
|
|
35637
|
+
index += 1;
|
|
35638
|
+
continue;
|
|
35639
|
+
}
|
|
35640
|
+
if (/[A-Za-z_]/.test(character)) {
|
|
35641
|
+
let wordEnd = index;
|
|
35642
|
+
while (wordEnd < end && /[A-Za-z0-9_]/.test(content[wordEnd] ?? "")) wordEnd += 1;
|
|
35643
|
+
if (content.slice(index, wordEnd).toLowerCase() === "execute") inExecuteStatement = true;
|
|
35644
|
+
index = wordEnd;
|
|
35645
|
+
continue;
|
|
35646
|
+
}
|
|
35647
|
+
if (character === "'") {
|
|
35648
|
+
const keepVisible = inExecuteStatement;
|
|
35649
|
+
if (!keepVisible) characters[index] = " ";
|
|
35650
|
+
index += 1;
|
|
35651
|
+
while (index < end) {
|
|
35652
|
+
if (content[index] === "'") {
|
|
35653
|
+
if (content[index + 1] === "'") {
|
|
35654
|
+
if (!keepVisible) {
|
|
35655
|
+
characters[index] = " ";
|
|
35656
|
+
characters[index + 1] = " ";
|
|
35657
|
+
}
|
|
35658
|
+
index += 2;
|
|
35659
|
+
continue;
|
|
35660
|
+
}
|
|
35661
|
+
if (!keepVisible) characters[index] = " ";
|
|
35662
|
+
index += 1;
|
|
35663
|
+
break;
|
|
35664
|
+
}
|
|
35665
|
+
if (!keepVisible && content[index] !== "\n") characters[index] = " ";
|
|
35666
|
+
index += 1;
|
|
35667
|
+
}
|
|
35668
|
+
continue;
|
|
35669
|
+
}
|
|
35670
|
+
if (character === "\"") {
|
|
35671
|
+
index += 1;
|
|
35672
|
+
while (index < end) {
|
|
35673
|
+
if (content[index] === "\"") {
|
|
35674
|
+
if (content[index + 1] === "\"") {
|
|
35675
|
+
index += 2;
|
|
35676
|
+
continue;
|
|
35677
|
+
}
|
|
35678
|
+
index += 1;
|
|
35679
|
+
break;
|
|
35680
|
+
}
|
|
35681
|
+
index += 1;
|
|
35682
|
+
}
|
|
35683
|
+
continue;
|
|
35684
|
+
}
|
|
35685
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35686
|
+
while (index < end && content[index] !== "\n") {
|
|
35687
|
+
characters[index] = " ";
|
|
35688
|
+
index += 1;
|
|
35689
|
+
}
|
|
35690
|
+
continue;
|
|
35691
|
+
}
|
|
35692
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35693
|
+
while (index < end) {
|
|
35694
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35695
|
+
characters[index] = " ";
|
|
35696
|
+
characters[index + 1] = " ";
|
|
35697
|
+
index += 2;
|
|
35698
|
+
break;
|
|
35699
|
+
}
|
|
35700
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35701
|
+
index += 1;
|
|
35702
|
+
}
|
|
35703
|
+
continue;
|
|
35704
|
+
}
|
|
35705
|
+
index += 1;
|
|
35706
|
+
}
|
|
35707
|
+
};
|
|
35708
|
+
const sanitizeSqlForScan = (content) => {
|
|
35709
|
+
const characters = content.split("");
|
|
35710
|
+
let index = 0;
|
|
35711
|
+
while (index < content.length) {
|
|
35712
|
+
const character = content[index];
|
|
35713
|
+
if (character === "-" && content[index + 1] === "-") {
|
|
35714
|
+
while (index < content.length && content[index] !== "\n") {
|
|
35715
|
+
characters[index] = " ";
|
|
35716
|
+
index += 1;
|
|
35717
|
+
}
|
|
35718
|
+
continue;
|
|
35719
|
+
}
|
|
35720
|
+
if (character === "/" && content[index + 1] === "*") {
|
|
35721
|
+
while (index < content.length) {
|
|
35722
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
35723
|
+
characters[index] = " ";
|
|
35724
|
+
characters[index + 1] = " ";
|
|
35725
|
+
index += 2;
|
|
35726
|
+
break;
|
|
35727
|
+
}
|
|
35728
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35729
|
+
index += 1;
|
|
35730
|
+
}
|
|
35731
|
+
continue;
|
|
35732
|
+
}
|
|
35733
|
+
if (character === "'") {
|
|
35734
|
+
characters[index] = " ";
|
|
35735
|
+
index += 1;
|
|
35736
|
+
while (index < content.length) {
|
|
35737
|
+
if (content[index] === "'") {
|
|
35738
|
+
if (content[index + 1] === "'") {
|
|
35739
|
+
characters[index] = " ";
|
|
35740
|
+
characters[index + 1] = " ";
|
|
35741
|
+
index += 2;
|
|
35742
|
+
continue;
|
|
35743
|
+
}
|
|
35744
|
+
characters[index] = " ";
|
|
35745
|
+
index += 1;
|
|
35746
|
+
break;
|
|
35747
|
+
}
|
|
35748
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
35749
|
+
index += 1;
|
|
35750
|
+
}
|
|
35751
|
+
continue;
|
|
35752
|
+
}
|
|
35753
|
+
if (character === "$") {
|
|
35754
|
+
const tagMatch = DOLLAR_QUOTE_TAG_PATTERN.exec(content.slice(index));
|
|
35755
|
+
if (tagMatch !== null) {
|
|
35756
|
+
const tag = tagMatch[0];
|
|
35757
|
+
const closeIndex = content.indexOf(tag, index + tag.length);
|
|
35758
|
+
const endIndex = closeIndex < 0 ? content.length : closeIndex + tag.length;
|
|
35759
|
+
const keyword = precedingKeyword(content, index);
|
|
35760
|
+
if (CODE_BODY_KEYWORDS.has(keyword)) blankCodeBodyInterior(content, characters, index + tag.length, endIndex);
|
|
35761
|
+
else for (let blankIndex = index; blankIndex < endIndex; blankIndex += 1) if (content[blankIndex] !== "\n") characters[blankIndex] = " ";
|
|
35762
|
+
index = endIndex;
|
|
35763
|
+
continue;
|
|
35764
|
+
}
|
|
35765
|
+
}
|
|
35766
|
+
if (character === "\"") {
|
|
35767
|
+
index += 1;
|
|
35768
|
+
while (index < content.length) {
|
|
35769
|
+
if (content[index] === "\"") {
|
|
35770
|
+
if (content[index + 1] === "\"") {
|
|
35771
|
+
index += 2;
|
|
35772
|
+
continue;
|
|
35773
|
+
}
|
|
35774
|
+
index += 1;
|
|
35775
|
+
break;
|
|
35776
|
+
}
|
|
35777
|
+
index += 1;
|
|
35778
|
+
}
|
|
35779
|
+
continue;
|
|
35780
|
+
}
|
|
35781
|
+
index += 1;
|
|
35782
|
+
}
|
|
35783
|
+
return characters.join("");
|
|
35784
|
+
};
|
|
35785
|
+
//#endregion
|
|
35786
|
+
//#region src/plugin/rules/security-scan/supabase-table-missing-rls.ts
|
|
35787
|
+
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;
|
|
35788
|
+
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");
|
|
35789
|
+
const supabaseTableMissingRls = defineRule({
|
|
35790
|
+
id: "supabase-table-missing-rls",
|
|
35791
|
+
title: "Supabase table created without Row Level Security",
|
|
35792
|
+
severity: "error",
|
|
35793
|
+
recommendation: "Enable RLS in the same migration (`alter table <name> enable row level security;`) and add `auth.uid()`-scoped policies for select/insert/update/delete. A public table without RLS is fully readable and writable with the public anon key.",
|
|
35794
|
+
scan: (file) => {
|
|
35795
|
+
if (!isSupabaseMigrationPath(file.relativePath)) return [];
|
|
35796
|
+
const content = sanitizeSqlForScan(file.content);
|
|
35797
|
+
if (!/create\s+(?:unlogged\s+)?table/i.test(content)) return [];
|
|
35798
|
+
const findings = [];
|
|
35799
|
+
CREATE_PUBLIC_TABLE_PATTERN.lastIndex = 0;
|
|
35800
|
+
for (let match = CREATE_PUBLIC_TABLE_PATTERN.exec(content); match !== null; match = CREATE_PUBLIC_TABLE_PATTERN.exec(content)) {
|
|
35801
|
+
const tableName = match[1];
|
|
35802
|
+
if (tableName === void 0) continue;
|
|
35803
|
+
if (enableRlsForTablePattern(tableName).test(content.slice(match.index))) continue;
|
|
35804
|
+
const location = getLocationAtIndex(content, match.index);
|
|
35805
|
+
findings.push({
|
|
35806
|
+
message: "Supabase migration creates a public table but never enables Row Level Security, leaving every row exposed to the anon key.",
|
|
35807
|
+
line: location.line,
|
|
35808
|
+
column: location.column
|
|
35809
|
+
});
|
|
35810
|
+
}
|
|
35811
|
+
return findings;
|
|
35812
|
+
}
|
|
35813
|
+
});
|
|
35814
|
+
//#endregion
|
|
35161
35815
|
//#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
|
|
35162
35816
|
const svgFilterClickjackingRisk = defineRule({
|
|
35163
35817
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -35856,6 +36510,47 @@ const tenantStaticProxyRisk = defineRule({
|
|
|
35856
36510
|
})
|
|
35857
36511
|
});
|
|
35858
36512
|
//#endregion
|
|
36513
|
+
//#region src/plugin/rules/security-scan/unsafe-json-in-html.ts
|
|
36514
|
+
const SINK_JSON_STRINGIFY_PATTERNS = [/dangerouslySetInnerHTML\s*=\s*\{\{\s*__html\s*:[\s\S]{0,300}?\bJSON\.stringify\s*\(/gi, /<script\b[^>]*>(?:(?!<\/script>)[\s\S]){0,300}?\bJSON\.stringify\s*\(/gi];
|
|
36515
|
+
const RETURN_ESCAPE_PATTERN = /^[\s)]*\.replace\s*\([^)]*(?:\\u003[cC]|<|<)/;
|
|
36516
|
+
const ESCAPE_WRAPPER_PATTERN = /(?:\b(?:escapeHtml|escapeJSON|escapeJson|htmlEscape|jsesc)|(?<![.\w])(?:serialize|serializeJavascript|devalue|uneval|superjson))\s*\(\s*$/i;
|
|
36517
|
+
const JSON_STRINGIFY_TOKEN_PATTERN = /\bJSON\.stringify\s*\($/i;
|
|
36518
|
+
const RETURN_LOOKAHEAD_CHARS = 160;
|
|
36519
|
+
const unsafeJsonInHtml = defineRule({
|
|
36520
|
+
id: "unsafe-json-in-html",
|
|
36521
|
+
title: "Unescaped JSON in HTML or script sink",
|
|
36522
|
+
severity: "warn",
|
|
36523
|
+
recommendation: "JSON.stringify does not HTML-escape, so a `<\/script>` (or `<`) in the data breaks out and becomes XSS. Use an HTML-safe serializer (serialize-javascript, devalue) or escape `<`, `>`, and `&`, or pass data via a JSON `<script type=\"application/json\">` read with JSON.parse.",
|
|
36524
|
+
scan: (file) => {
|
|
36525
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
36526
|
+
const content = getScannableContent(file);
|
|
36527
|
+
if (!content.includes("JSON.stringify")) return [];
|
|
36528
|
+
const findings = [];
|
|
36529
|
+
const seenIndices = /* @__PURE__ */ new Set();
|
|
36530
|
+
for (const pattern of SINK_JSON_STRINGIFY_PATTERNS) {
|
|
36531
|
+
pattern.lastIndex = 0;
|
|
36532
|
+
for (let match = pattern.exec(content); match !== null; match = pattern.exec(content)) {
|
|
36533
|
+
const beforeStringify = match[0].replace(JSON_STRINGIFY_TOKEN_PATTERN, "");
|
|
36534
|
+
if (ESCAPE_WRAPPER_PATTERN.test(beforeStringify)) continue;
|
|
36535
|
+
const closeParenIndex = findMatchingBracket(content, match.index + match[0].length - 1);
|
|
36536
|
+
if (closeParenIndex >= 0) {
|
|
36537
|
+
const afterReturn = content.slice(closeParenIndex + 1, closeParenIndex + 1 + RETURN_LOOKAHEAD_CHARS);
|
|
36538
|
+
if (RETURN_ESCAPE_PATTERN.test(afterReturn)) continue;
|
|
36539
|
+
}
|
|
36540
|
+
if (seenIndices.has(match.index)) continue;
|
|
36541
|
+
seenIndices.add(match.index);
|
|
36542
|
+
const location = getLocationAtIndex(content, match.index);
|
|
36543
|
+
findings.push({
|
|
36544
|
+
message: "JSON.stringify is embedded in HTML/script markup without HTML-escaping; data containing `<\/script>` or `<` breaks out and becomes XSS.",
|
|
36545
|
+
line: location.line,
|
|
36546
|
+
column: location.column
|
|
36547
|
+
});
|
|
36548
|
+
}
|
|
36549
|
+
}
|
|
36550
|
+
return findings;
|
|
36551
|
+
}
|
|
36552
|
+
});
|
|
36553
|
+
//#endregion
|
|
35859
36554
|
//#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
|
|
35860
36555
|
const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
|
|
35861
36556
|
const CALLER_STYLE_URL_NAME_PATTERN = /\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i;
|
|
@@ -36003,7 +36698,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
36003
36698
|
//#region src/plugin/rules/security-scan/webhook-signature-risk.ts
|
|
36004
36699
|
const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
|
|
36005
36700
|
const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
|
|
36006
|
-
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']
|
|
36701
|
+
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = new RegExp(`${/verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/.source}|${/\b[A-Za-z]{0,40}(?:verif|valid|check|assert|authenticat|compare|guard)[A-Za-z]{0,40}(?:secret|signature|hmac|webhook|digest)[A-Za-z]{0,40}\s*\(/.source}`, "i");
|
|
36007
36702
|
const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
|
|
36008
36703
|
const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
|
|
36009
36704
|
const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
|
|
@@ -36663,6 +37358,17 @@ const reactDoctorRules = [
|
|
|
36663
37358
|
category: "Performance"
|
|
36664
37359
|
}
|
|
36665
37360
|
},
|
|
37361
|
+
{
|
|
37362
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37363
|
+
id: "auth-token-in-web-storage",
|
|
37364
|
+
source: "react-doctor",
|
|
37365
|
+
originallyExternal: false,
|
|
37366
|
+
rule: {
|
|
37367
|
+
...authTokenInWebStorage,
|
|
37368
|
+
framework: "global",
|
|
37369
|
+
category: "Security"
|
|
37370
|
+
}
|
|
37371
|
+
},
|
|
36666
37372
|
{
|
|
36667
37373
|
key: "react-doctor/autocomplete-valid",
|
|
36668
37374
|
id: "autocomplete-valid",
|
|
@@ -36879,6 +37585,18 @@ const reactDoctorRules = [
|
|
|
36879
37585
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
36880
37586
|
}
|
|
36881
37587
|
},
|
|
37588
|
+
{
|
|
37589
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37590
|
+
id: "dialog-has-accessible-name",
|
|
37591
|
+
source: "react-doctor",
|
|
37592
|
+
originallyExternal: false,
|
|
37593
|
+
rule: {
|
|
37594
|
+
...dialogHasAccessibleName,
|
|
37595
|
+
framework: "global",
|
|
37596
|
+
category: "Accessibility",
|
|
37597
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37598
|
+
}
|
|
37599
|
+
},
|
|
36882
37600
|
{
|
|
36883
37601
|
key: "react-doctor/display-name",
|
|
36884
37602
|
id: "display-name",
|
|
@@ -37164,6 +37882,18 @@ const reactDoctorRules = [
|
|
|
37164
37882
|
tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
|
|
37165
37883
|
}
|
|
37166
37884
|
},
|
|
37885
|
+
{
|
|
37886
|
+
key: "react-doctor/insecure-session-cookie",
|
|
37887
|
+
id: "insecure-session-cookie",
|
|
37888
|
+
source: "react-doctor",
|
|
37889
|
+
originallyExternal: false,
|
|
37890
|
+
rule: {
|
|
37891
|
+
...insecureSessionCookie,
|
|
37892
|
+
framework: "global",
|
|
37893
|
+
category: "Security",
|
|
37894
|
+
tags: [...new Set(["security-scan", ...insecureSessionCookie.tags ?? []])]
|
|
37895
|
+
}
|
|
37896
|
+
},
|
|
37167
37897
|
{
|
|
37168
37898
|
key: "react-doctor/interactive-supports-focus",
|
|
37169
37899
|
id: "interactive-supports-focus",
|
|
@@ -37606,6 +38336,18 @@ const reactDoctorRules = [
|
|
|
37606
38336
|
requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
|
|
37607
38337
|
}
|
|
37608
38338
|
},
|
|
38339
|
+
{
|
|
38340
|
+
key: "react-doctor/jwt-insecure-verification",
|
|
38341
|
+
id: "jwt-insecure-verification",
|
|
38342
|
+
source: "react-doctor",
|
|
38343
|
+
originallyExternal: false,
|
|
38344
|
+
rule: {
|
|
38345
|
+
...jwtInsecureVerification,
|
|
38346
|
+
framework: "global",
|
|
38347
|
+
category: "Security",
|
|
38348
|
+
tags: [...new Set(["security-scan", ...jwtInsecureVerification.tags ?? []])]
|
|
38349
|
+
}
|
|
38350
|
+
},
|
|
37609
38351
|
{
|
|
37610
38352
|
key: "react-doctor/key-lifecycle-risk",
|
|
37611
38353
|
id: "key-lifecycle-risk",
|
|
@@ -38014,6 +38756,18 @@ const reactDoctorRules = [
|
|
|
38014
38756
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38015
38757
|
}
|
|
38016
38758
|
},
|
|
38759
|
+
{
|
|
38760
|
+
key: "react-doctor/no-async-effect-callback",
|
|
38761
|
+
id: "no-async-effect-callback",
|
|
38762
|
+
source: "react-doctor",
|
|
38763
|
+
originallyExternal: false,
|
|
38764
|
+
rule: {
|
|
38765
|
+
...noAsyncEffectCallback,
|
|
38766
|
+
framework: "global",
|
|
38767
|
+
category: "Bugs",
|
|
38768
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
38769
|
+
}
|
|
38770
|
+
},
|
|
38017
38771
|
{
|
|
38018
38772
|
key: "react-doctor/no-autofocus",
|
|
38019
38773
|
id: "no-autofocus",
|
|
@@ -38037,6 +38791,18 @@ const reactDoctorRules = [
|
|
|
38037
38791
|
category: "Performance"
|
|
38038
38792
|
}
|
|
38039
38793
|
},
|
|
38794
|
+
{
|
|
38795
|
+
key: "react-doctor/no-call-component-as-function",
|
|
38796
|
+
id: "no-call-component-as-function",
|
|
38797
|
+
source: "react-doctor",
|
|
38798
|
+
originallyExternal: false,
|
|
38799
|
+
rule: {
|
|
38800
|
+
...noCallComponentAsFunction,
|
|
38801
|
+
framework: "global",
|
|
38802
|
+
category: "Bugs",
|
|
38803
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
38804
|
+
}
|
|
38805
|
+
},
|
|
38040
38806
|
{
|
|
38041
38807
|
key: "react-doctor/no-cascading-set-state",
|
|
38042
38808
|
id: "no-cascading-set-state",
|
|
@@ -38097,6 +38863,18 @@ const reactDoctorRules = [
|
|
|
38097
38863
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38098
38864
|
}
|
|
38099
38865
|
},
|
|
38866
|
+
{
|
|
38867
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
38868
|
+
id: "no-create-ref-in-function-component",
|
|
38869
|
+
source: "react-doctor",
|
|
38870
|
+
originallyExternal: false,
|
|
38871
|
+
rule: {
|
|
38872
|
+
...noCreateRefInFunctionComponent,
|
|
38873
|
+
framework: "global",
|
|
38874
|
+
category: "Bugs",
|
|
38875
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
38876
|
+
}
|
|
38877
|
+
},
|
|
38100
38878
|
{
|
|
38101
38879
|
key: "react-doctor/no-create-store-in-render",
|
|
38102
38880
|
id: "no-create-store-in-render",
|
|
@@ -38471,6 +39249,18 @@ const reactDoctorRules = [
|
|
|
38471
39249
|
category: "Accessibility"
|
|
38472
39250
|
}
|
|
38473
39251
|
},
|
|
39252
|
+
{
|
|
39253
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39254
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39255
|
+
source: "react-doctor",
|
|
39256
|
+
originallyExternal: false,
|
|
39257
|
+
rule: {
|
|
39258
|
+
...noImgLazyWithHighFetchpriority,
|
|
39259
|
+
framework: "global",
|
|
39260
|
+
category: "Performance",
|
|
39261
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39262
|
+
}
|
|
39263
|
+
},
|
|
38474
39264
|
{
|
|
38475
39265
|
key: "react-doctor/no-initialize-state",
|
|
38476
39266
|
id: "no-initialize-state",
|
|
@@ -38541,6 +39331,17 @@ const reactDoctorRules = [
|
|
|
38541
39331
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
38542
39332
|
}
|
|
38543
39333
|
},
|
|
39334
|
+
{
|
|
39335
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39336
|
+
id: "no-json-parse-stringify-clone",
|
|
39337
|
+
source: "react-doctor",
|
|
39338
|
+
originallyExternal: false,
|
|
39339
|
+
rule: {
|
|
39340
|
+
...noJsonParseStringifyClone,
|
|
39341
|
+
framework: "global",
|
|
39342
|
+
category: "Performance"
|
|
39343
|
+
}
|
|
39344
|
+
},
|
|
38544
39345
|
{
|
|
38545
39346
|
key: "react-doctor/no-jsx-element-type",
|
|
38546
39347
|
id: "no-jsx-element-type",
|
|
@@ -39756,6 +40557,18 @@ const reactDoctorRules = [
|
|
|
39756
40557
|
tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
|
|
39757
40558
|
}
|
|
39758
40559
|
},
|
|
40560
|
+
{
|
|
40561
|
+
key: "react-doctor/request-body-mass-assignment",
|
|
40562
|
+
id: "request-body-mass-assignment",
|
|
40563
|
+
source: "react-doctor",
|
|
40564
|
+
originallyExternal: false,
|
|
40565
|
+
rule: {
|
|
40566
|
+
...requestBodyMassAssignment,
|
|
40567
|
+
framework: "global",
|
|
40568
|
+
category: "Security",
|
|
40569
|
+
tags: [...new Set(["security-scan", ...requestBodyMassAssignment.tags ?? []])]
|
|
40570
|
+
}
|
|
40571
|
+
},
|
|
39759
40572
|
{
|
|
39760
40573
|
key: "react-doctor/require-render-return",
|
|
39761
40574
|
id: "require-render-return",
|
|
@@ -40344,6 +41157,18 @@ const reactDoctorRules = [
|
|
|
40344
41157
|
requires: [...new Set(["react", ...scope.requires ?? []])]
|
|
40345
41158
|
}
|
|
40346
41159
|
},
|
|
41160
|
+
{
|
|
41161
|
+
key: "react-doctor/secret-in-fallback",
|
|
41162
|
+
id: "secret-in-fallback",
|
|
41163
|
+
source: "react-doctor",
|
|
41164
|
+
originallyExternal: false,
|
|
41165
|
+
rule: {
|
|
41166
|
+
...secretInFallback,
|
|
41167
|
+
framework: "global",
|
|
41168
|
+
category: "Security",
|
|
41169
|
+
tags: [...new Set(["security-scan", ...secretInFallback.tags ?? []])]
|
|
41170
|
+
}
|
|
41171
|
+
},
|
|
40347
41172
|
{
|
|
40348
41173
|
key: "react-doctor/self-closing-comp",
|
|
40349
41174
|
id: "self-closing-comp",
|
|
@@ -40500,6 +41325,18 @@ const reactDoctorRules = [
|
|
|
40500
41325
|
tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
|
|
40501
41326
|
}
|
|
40502
41327
|
},
|
|
41328
|
+
{
|
|
41329
|
+
key: "react-doctor/supabase-table-missing-rls",
|
|
41330
|
+
id: "supabase-table-missing-rls",
|
|
41331
|
+
source: "react-doctor",
|
|
41332
|
+
originallyExternal: false,
|
|
41333
|
+
rule: {
|
|
41334
|
+
...supabaseTableMissingRls,
|
|
41335
|
+
framework: "global",
|
|
41336
|
+
category: "Security",
|
|
41337
|
+
tags: [...new Set(["security-scan", ...supabaseTableMissingRls.tags ?? []])]
|
|
41338
|
+
}
|
|
41339
|
+
},
|
|
40503
41340
|
{
|
|
40504
41341
|
key: "react-doctor/svg-filter-clickjacking-risk",
|
|
40505
41342
|
id: "svg-filter-clickjacking-risk",
|
|
@@ -40690,6 +41527,18 @@ const reactDoctorRules = [
|
|
|
40690
41527
|
tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
|
|
40691
41528
|
}
|
|
40692
41529
|
},
|
|
41530
|
+
{
|
|
41531
|
+
key: "react-doctor/unsafe-json-in-html",
|
|
41532
|
+
id: "unsafe-json-in-html",
|
|
41533
|
+
source: "react-doctor",
|
|
41534
|
+
originallyExternal: false,
|
|
41535
|
+
rule: {
|
|
41536
|
+
...unsafeJsonInHtml,
|
|
41537
|
+
framework: "global",
|
|
41538
|
+
category: "Security",
|
|
41539
|
+
tags: [...new Set(["security-scan", ...unsafeJsonInHtml.tags ?? []])]
|
|
41540
|
+
}
|
|
41541
|
+
},
|
|
40693
41542
|
{
|
|
40694
41543
|
key: "react-doctor/untrusted-redirect-following",
|
|
40695
41544
|
id: "untrusted-redirect-following",
|