oxlint-plugin-react-doctor 0.5.8-dev.7ef9f0e → 0.5.8-dev.80e3093
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +64 -288
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1160,61 +1160,6 @@ const isFirebaseRulesPath = (relativePath) => /(?:^|\/)(?:firestore\.rules|stora
|
|
|
1160
1160
|
//#endregion
|
|
1161
1161
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
1162
1162
|
const WHITESPACE_PATTERN = /\s/;
|
|
1163
|
-
const IDENTIFIER_CHARACTER_PATTERN = /[\p{ID_Continue}$]/u;
|
|
1164
|
-
const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
1165
|
-
"return",
|
|
1166
|
-
"typeof",
|
|
1167
|
-
"case",
|
|
1168
|
-
"in",
|
|
1169
|
-
"of",
|
|
1170
|
-
"new",
|
|
1171
|
-
"delete",
|
|
1172
|
-
"void",
|
|
1173
|
-
"instanceof",
|
|
1174
|
-
"do",
|
|
1175
|
-
"else",
|
|
1176
|
-
"yield",
|
|
1177
|
-
"await",
|
|
1178
|
-
"throw"
|
|
1179
|
-
]);
|
|
1180
|
-
const isRegexLiteralStart = (characters, slashIndex) => {
|
|
1181
|
-
let cursor = slashIndex - 1;
|
|
1182
|
-
while (cursor >= 0 && WHITESPACE_PATTERN.test(characters[cursor])) cursor -= 1;
|
|
1183
|
-
if (cursor < 0) return true;
|
|
1184
|
-
const previousCharacter = characters[cursor];
|
|
1185
|
-
const characterBefore = cursor > 0 ? characters[cursor - 1] : "";
|
|
1186
|
-
if (IDENTIFIER_CHARACTER_PATTERN.test(previousCharacter)) {
|
|
1187
|
-
let wordStartIndex = cursor;
|
|
1188
|
-
while (wordStartIndex > 0 && IDENTIFIER_CHARACTER_PATTERN.test(characters[wordStartIndex - 1])) wordStartIndex -= 1;
|
|
1189
|
-
if (wordStartIndex > 0 && characters[wordStartIndex - 1] === ".") return false;
|
|
1190
|
-
return REGEX_PRECEDING_KEYWORDS.has(characters.slice(wordStartIndex, cursor + 1).join(""));
|
|
1191
|
-
}
|
|
1192
|
-
if (previousCharacter === "<") return false;
|
|
1193
|
-
if (previousCharacter === ">") return characterBefore === "=";
|
|
1194
|
-
if (previousCharacter === "+" || previousCharacter === "-") return characterBefore !== previousCharacter;
|
|
1195
|
-
if (previousCharacter === "!") return !(IDENTIFIER_CHARACTER_PATTERN.test(characterBefore) || characterBefore === ")" || characterBefore === "]");
|
|
1196
|
-
return !(previousCharacter === ")" || previousCharacter === "]" || previousCharacter === "}" || previousCharacter === "\"" || previousCharacter === "'" || previousCharacter === "`");
|
|
1197
|
-
};
|
|
1198
|
-
const findRegexLiteralEnd = (content, slashIndex) => {
|
|
1199
|
-
let cursor = slashIndex + 1;
|
|
1200
|
-
let isInsideCharacterClass = false;
|
|
1201
|
-
while (cursor < content.length) {
|
|
1202
|
-
const character = content[cursor];
|
|
1203
|
-
if (character === "\\") {
|
|
1204
|
-
cursor += 2;
|
|
1205
|
-
continue;
|
|
1206
|
-
}
|
|
1207
|
-
if (character === "\n") return null;
|
|
1208
|
-
if (character === "[") isInsideCharacterClass = true;
|
|
1209
|
-
else if (character === "]") isInsideCharacterClass = false;
|
|
1210
|
-
else if (character === "/" && !isInsideCharacterClass) {
|
|
1211
|
-
if (content[cursor + 1] === "/") return null;
|
|
1212
|
-
return cursor + 1;
|
|
1213
|
-
}
|
|
1214
|
-
cursor += 1;
|
|
1215
|
-
}
|
|
1216
|
-
return null;
|
|
1217
|
-
};
|
|
1218
1163
|
const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
1219
1164
|
for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
|
|
1220
1165
|
const character = content[cursor];
|
|
@@ -1248,11 +1193,6 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1248
1193
|
index += 2;
|
|
1249
1194
|
continue;
|
|
1250
1195
|
}
|
|
1251
|
-
if (character === "\n" && stringDelimiter !== "`") {
|
|
1252
|
-
stringDelimiter = null;
|
|
1253
|
-
index += 1;
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
1196
|
if (character === stringDelimiter) {
|
|
1257
1197
|
stringDelimiter = null;
|
|
1258
1198
|
index += 1;
|
|
@@ -1300,14 +1240,6 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1300
1240
|
}
|
|
1301
1241
|
continue;
|
|
1302
1242
|
}
|
|
1303
|
-
if (character === "/") {
|
|
1304
|
-
const regexEndIndex = isRegexLiteralStart(characters, index) ? findRegexLiteralEnd(content, index) : null;
|
|
1305
|
-
if (regexEndIndex !== null) {
|
|
1306
|
-
if (blankStringContents) for (let interiorIndex = index + 1; interiorIndex < regexEndIndex - 1; interiorIndex += 1) blankUnlessNewline(interiorIndex);
|
|
1307
|
-
index = regexEndIndex;
|
|
1308
|
-
continue;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
1243
|
if (templateExpressionDepths.length > 0) {
|
|
1312
1244
|
const innermost = templateExpressionDepths.length - 1;
|
|
1313
1245
|
if (character === "{") templateExpressionDepths[innermost] += 1;
|
|
@@ -2086,7 +2018,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
2086
2018
|
});
|
|
2087
2019
|
//#endregion
|
|
2088
2020
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
2089
|
-
const MESSAGE$
|
|
2021
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
2090
2022
|
const anchorHasContent = defineRule({
|
|
2091
2023
|
id: "anchor-has-content",
|
|
2092
2024
|
title: "Anchor has no content",
|
|
@@ -2109,7 +2041,7 @@ const anchorHasContent = defineRule({
|
|
|
2109
2041
|
]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
2110
2042
|
context.report({
|
|
2111
2043
|
node: opening.name,
|
|
2112
|
-
message: MESSAGE$
|
|
2044
|
+
message: MESSAGE$59
|
|
2113
2045
|
});
|
|
2114
2046
|
} };
|
|
2115
2047
|
}
|
|
@@ -2495,7 +2427,7 @@ const parseJsxValue = (value) => {
|
|
|
2495
2427
|
};
|
|
2496
2428
|
//#endregion
|
|
2497
2429
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2498
|
-
const MESSAGE$
|
|
2430
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2499
2431
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2500
2432
|
id: "aria-activedescendant-has-tabindex",
|
|
2501
2433
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2513,14 +2445,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2513
2445
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2514
2446
|
context.report({
|
|
2515
2447
|
node: node.name,
|
|
2516
|
-
message: MESSAGE$
|
|
2448
|
+
message: MESSAGE$58
|
|
2517
2449
|
});
|
|
2518
2450
|
return;
|
|
2519
2451
|
}
|
|
2520
2452
|
if (isInteractiveElement(tag, node)) return;
|
|
2521
2453
|
context.report({
|
|
2522
2454
|
node: node.name,
|
|
2523
|
-
message: MESSAGE$
|
|
2455
|
+
message: MESSAGE$58
|
|
2524
2456
|
});
|
|
2525
2457
|
} })
|
|
2526
2458
|
});
|
|
@@ -4625,7 +4557,7 @@ const asyncParallel = defineRule({
|
|
|
4625
4557
|
});
|
|
4626
4558
|
//#endregion
|
|
4627
4559
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4628
|
-
const MESSAGE$
|
|
4560
|
+
const MESSAGE$57 = "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.";
|
|
4629
4561
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4630
4562
|
const STORAGE_GLOBALS = new Set([
|
|
4631
4563
|
"window",
|
|
@@ -4666,7 +4598,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4666
4598
|
if (!isAuthCredentialKey(keyArgument.value)) return;
|
|
4667
4599
|
context.report({
|
|
4668
4600
|
node,
|
|
4669
|
-
message: MESSAGE$
|
|
4601
|
+
message: MESSAGE$57
|
|
4670
4602
|
});
|
|
4671
4603
|
},
|
|
4672
4604
|
AssignmentExpression(node) {
|
|
@@ -4677,7 +4609,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4677
4609
|
if (!propertyName || !isAuthCredentialKey(propertyName)) return;
|
|
4678
4610
|
context.report({
|
|
4679
4611
|
node: target,
|
|
4680
|
-
message: MESSAGE$
|
|
4612
|
+
message: MESSAGE$57
|
|
4681
4613
|
});
|
|
4682
4614
|
}
|
|
4683
4615
|
}))
|
|
@@ -5167,7 +5099,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5167
5099
|
};
|
|
5168
5100
|
//#endregion
|
|
5169
5101
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
5170
|
-
const MESSAGE$
|
|
5102
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
5171
5103
|
const KEY_HANDLERS = [
|
|
5172
5104
|
"onKeyUp",
|
|
5173
5105
|
"onKeyDown",
|
|
@@ -5195,7 +5127,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
5195
5127
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
5196
5128
|
context.report({
|
|
5197
5129
|
node: node.name,
|
|
5198
|
-
message: MESSAGE$
|
|
5130
|
+
message: MESSAGE$56
|
|
5199
5131
|
});
|
|
5200
5132
|
} };
|
|
5201
5133
|
}
|
|
@@ -5412,7 +5344,7 @@ const getClassNameLiteral = (classAttribute) => {
|
|
|
5412
5344
|
};
|
|
5413
5345
|
//#endregion
|
|
5414
5346
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
5415
|
-
const MESSAGE$
|
|
5347
|
+
const MESSAGE$55 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
5416
5348
|
const DISPLAY_NONE_CLASS_TOKEN = "hidden";
|
|
5417
5349
|
const isDisplayNoneClassToken = (token) => token.toLowerCase() === DISPLAY_NONE_CLASS_TOKEN;
|
|
5418
5350
|
const collectStaticTemplateClassTokens = (templateLiteral) => {
|
|
@@ -5607,7 +5539,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5607
5539
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5608
5540
|
context.report({
|
|
5609
5541
|
node: opening,
|
|
5610
|
-
message: MESSAGE$
|
|
5542
|
+
message: MESSAGE$55
|
|
5611
5543
|
});
|
|
5612
5544
|
} };
|
|
5613
5545
|
}
|
|
@@ -5630,7 +5562,7 @@ const corsCookieTrustRisk = defineRule({
|
|
|
5630
5562
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
5631
5563
|
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
5632
5564
|
const HTML_TAINT_PATTERN = /searchParams|query|params|request|req\.|response\.|result\.|data\.|await|fetch|props\.|children|content|html|body|text|message|\blocation\b|document\.cookie|\breferrer\b|\blocalStorage\b|\bsessionStorage\b|URLSearchParams|window\.name/i;
|
|
5633
|
-
const STRING_LITERAL_VALUE_PATTERN = /^(?:"
|
|
5565
|
+
const STRING_LITERAL_VALUE_PATTERN = /^(?:["'][^"']*["']|`[^`$]*`)\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
|
|
5634
5566
|
const MODULE_CONSTANT_VALUE_PATTERN = /^[A-Z][A-Z0-9_]*\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
|
|
5635
5567
|
const DOM_CONTENT_SOURCE_VALUE_PATTERN = /^[\w$]+(?:\??\.[\w$]+)*\??\.(?:inner|outer)HTML\b/;
|
|
5636
5568
|
const SANITIZER_PATTERN = /\b(?:DOMPurify|sanitize\w*|purify|(?:escape|encode)[A-Za-z]*(?:Html|HTML|Entit\w*)|insane|xss)\b|(?<!un)safe|(?<!un)saniti[sz]/i;
|
|
@@ -6062,7 +5994,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
6062
5994
|
});
|
|
6063
5995
|
//#endregion
|
|
6064
5996
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
6065
|
-
const MESSAGE$
|
|
5997
|
+
const MESSAGE$54 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
6066
5998
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
6067
5999
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
6068
6000
|
"aria-label",
|
|
@@ -6085,7 +6017,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
6085
6017
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
6086
6018
|
context.report({
|
|
6087
6019
|
node: node.name,
|
|
6088
|
-
message: MESSAGE$
|
|
6020
|
+
message: MESSAGE$54
|
|
6089
6021
|
});
|
|
6090
6022
|
} })
|
|
6091
6023
|
});
|
|
@@ -6124,7 +6056,7 @@ const isEs6Component = (node) => {
|
|
|
6124
6056
|
};
|
|
6125
6057
|
//#endregion
|
|
6126
6058
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
6127
|
-
const MESSAGE$
|
|
6059
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
6128
6060
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
6129
6061
|
"observer",
|
|
6130
6062
|
"lazy",
|
|
@@ -6327,7 +6259,7 @@ const displayName = defineRule({
|
|
|
6327
6259
|
const reportAt = (node) => {
|
|
6328
6260
|
context.report({
|
|
6329
6261
|
node,
|
|
6330
|
-
message: MESSAGE$
|
|
6262
|
+
message: MESSAGE$53
|
|
6331
6263
|
});
|
|
6332
6264
|
};
|
|
6333
6265
|
return {
|
|
@@ -8491,7 +8423,7 @@ const forbidElements = defineRule({
|
|
|
8491
8423
|
});
|
|
8492
8424
|
//#endregion
|
|
8493
8425
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
8494
|
-
const MESSAGE$
|
|
8426
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
8495
8427
|
const forwardRefUsesRef = defineRule({
|
|
8496
8428
|
id: "forward-ref-uses-ref",
|
|
8497
8429
|
title: "forwardRef without ref parameter",
|
|
@@ -8511,7 +8443,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
8511
8443
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
8512
8444
|
context.report({
|
|
8513
8445
|
node: inner,
|
|
8514
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$52
|
|
8515
8447
|
});
|
|
8516
8448
|
} })
|
|
8517
8449
|
});
|
|
@@ -8548,7 +8480,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
8548
8480
|
});
|
|
8549
8481
|
//#endregion
|
|
8550
8482
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
8551
|
-
const MESSAGE$
|
|
8483
|
+
const MESSAGE$51 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
8552
8484
|
const DEFAULT_HEADING_TAGS = [
|
|
8553
8485
|
"h1",
|
|
8554
8486
|
"h2",
|
|
@@ -8582,7 +8514,7 @@ const headingHasContent = defineRule({
|
|
|
8582
8514
|
for (const attribute of ["aria-label", "aria-labelledby"]) if (hasJsxPropIgnoreCase(node.attributes, attribute)) return;
|
|
8583
8515
|
context.report({
|
|
8584
8516
|
node,
|
|
8585
|
-
message: MESSAGE$
|
|
8517
|
+
message: MESSAGE$51
|
|
8586
8518
|
});
|
|
8587
8519
|
} };
|
|
8588
8520
|
}
|
|
@@ -8720,7 +8652,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8720
8652
|
});
|
|
8721
8653
|
//#endregion
|
|
8722
8654
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8723
|
-
const MESSAGE$
|
|
8655
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8724
8656
|
const resolveSettings$38 = (settings) => {
|
|
8725
8657
|
const reactDoctor = settings?.["react-doctor"];
|
|
8726
8658
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8767,13 +8699,13 @@ const htmlHasLang = defineRule({
|
|
|
8767
8699
|
if (!lang) {
|
|
8768
8700
|
context.report({
|
|
8769
8701
|
node: node.name,
|
|
8770
|
-
message: MESSAGE$
|
|
8702
|
+
message: MESSAGE$50
|
|
8771
8703
|
});
|
|
8772
8704
|
return;
|
|
8773
8705
|
}
|
|
8774
8706
|
if (evaluateLang(lang.value) === "empty") context.report({
|
|
8775
8707
|
node: lang,
|
|
8776
|
-
message: MESSAGE$
|
|
8708
|
+
message: MESSAGE$50
|
|
8777
8709
|
});
|
|
8778
8710
|
} };
|
|
8779
8711
|
}
|
|
@@ -8993,7 +8925,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8993
8925
|
});
|
|
8994
8926
|
//#endregion
|
|
8995
8927
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8996
|
-
const MESSAGE$
|
|
8928
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8997
8929
|
const evaluateTitleValue = (value) => {
|
|
8998
8930
|
if (!value) return "missing";
|
|
8999
8931
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -9033,14 +8965,14 @@ const iframeHasTitle = defineRule({
|
|
|
9033
8965
|
if (!titleAttr) {
|
|
9034
8966
|
if (hasSpread || tag === "iframe") context.report({
|
|
9035
8967
|
node: node.name,
|
|
9036
|
-
message: MESSAGE$
|
|
8968
|
+
message: MESSAGE$49
|
|
9037
8969
|
});
|
|
9038
8970
|
return;
|
|
9039
8971
|
}
|
|
9040
8972
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
9041
8973
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
9042
8974
|
node: titleAttr,
|
|
9043
|
-
message: MESSAGE$
|
|
8975
|
+
message: MESSAGE$49
|
|
9044
8976
|
});
|
|
9045
8977
|
} })
|
|
9046
8978
|
});
|
|
@@ -9155,7 +9087,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
9155
9087
|
});
|
|
9156
9088
|
//#endregion
|
|
9157
9089
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
9158
|
-
const MESSAGE$
|
|
9090
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
9159
9091
|
const DEFAULT_COMPONENTS = ["img"];
|
|
9160
9092
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
9161
9093
|
"image",
|
|
@@ -9220,7 +9152,7 @@ const imgRedundantAlt = defineRule({
|
|
|
9220
9152
|
if (!altAttribute) return;
|
|
9221
9153
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
9222
9154
|
node: altAttribute,
|
|
9223
|
-
message: MESSAGE$
|
|
9155
|
+
message: MESSAGE$48
|
|
9224
9156
|
});
|
|
9225
9157
|
} };
|
|
9226
9158
|
}
|
|
@@ -9259,7 +9191,7 @@ const DEPRECATED_CIPHER_API_PATTERN = /(?<!cipher\.)\bcreate(?:Cipher|Decipher)\
|
|
|
9259
9191
|
const WEAK_CIPHER_ALGORITHM_PATTERN = /\bcreate(?:Cipher|Decipher)iv\s*\(\s*["'](?:des|des3|des-?ede3?|rc4|rc2|bf|blowfish)\b/i;
|
|
9260
9192
|
const WEAK_CIPHER_NAME_PATTERN = /\b(?:DES|RC4|Blowfish)\b/;
|
|
9261
9193
|
const CIPHER_CONTEXT_PATTERN = /\b(?:cipher|decipher|encrypt|decrypt|crypto)\b/i;
|
|
9262
|
-
const UNSAFE_SIGNATURE_COMPARISON_PATTERN = /[A-Za-z_$][\w$.]
|
|
9194
|
+
const UNSAFE_SIGNATURE_COMPARISON_PATTERN = /[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*(?:\([^)]*\))?|[A-Za-z_$][\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?/i;
|
|
9263
9195
|
const ENUM_MEMBER_COMPARAND_PATTERN = /(?:===?|!==?)\s*[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))|^[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))[\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)/;
|
|
9264
9196
|
const SIGNATURE_METADATA_IDENTIFIER_PATTERN = /signature(?:Method|Type|Status|Algorithm|Kind|Mode|Version)\b/i;
|
|
9265
9197
|
const BOOLEAN_COMPARAND_PATTERN = /(?:===?|!==?)\s*(?:true|false|null|undefined)\b/;
|
|
@@ -9299,18 +9231,17 @@ const insecureCryptoRisk = defineRule({
|
|
|
9299
9231
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
9300
9232
|
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9301
9233
|
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
if (matchIndex < 0) matchIndex = content.search(
|
|
9305
|
-
if (matchIndex < 0) matchIndex = content.search(
|
|
9306
|
-
if (matchIndex < 0 &&
|
|
9307
|
-
|
|
9308
|
-
const comparisonMatch = UNSAFE_SIGNATURE_COMPARISON_PATTERN.exec(content);
|
|
9234
|
+
let matchIndex = findMatchIndexNearContext(file.content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
9235
|
+
if (matchIndex < 0) matchIndex = file.content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
9236
|
+
if (matchIndex < 0) matchIndex = file.content.search(DEPRECATED_CIPHER_API_PATTERN);
|
|
9237
|
+
if (matchIndex < 0 && CIPHER_CONTEXT_PATTERN.test(file.content)) matchIndex = file.content.search(WEAK_CIPHER_NAME_PATTERN);
|
|
9238
|
+
if (matchIndex < 0 && !TIMING_SAFE_COMPARISON_PATTERN.test(file.content) && !CLIENT_COMPONENT_FILE_PATTERN.test(file.relativePath)) {
|
|
9239
|
+
const comparisonMatch = UNSAFE_SIGNATURE_COMPARISON_PATTERN.exec(file.content);
|
|
9309
9240
|
if (comparisonMatch !== null && !ENUM_MEMBER_COMPARAND_PATTERN.test(comparisonMatch[0]) && !SIGNATURE_METADATA_IDENTIFIER_PATTERN.test(comparisonMatch[0]) && !BOOLEAN_COMPARAND_PATTERN.test(comparisonMatch[0])) matchIndex = comparisonMatch.index;
|
|
9310
9241
|
}
|
|
9311
|
-
if (matchIndex < 0) matchIndex = findRandomCallIndexWithSameLineContext(content, MATH_RANDOM_CALL_PATTERN, SECURITY_RANDOM_CONTEXT_PATTERN, UI_NONCE_CONTEXT_PATTERN);
|
|
9242
|
+
if (matchIndex < 0) matchIndex = findRandomCallIndexWithSameLineContext(file.content, MATH_RANDOM_CALL_PATTERN, SECURITY_RANDOM_CONTEXT_PATTERN, UI_NONCE_CONTEXT_PATTERN);
|
|
9312
9243
|
if (matchIndex < 0) return [];
|
|
9313
|
-
const location = getLocationAtIndex(content, matchIndex);
|
|
9244
|
+
const location = getLocationAtIndex(file.content, matchIndex);
|
|
9314
9245
|
return [{
|
|
9315
9246
|
message: "Code uses weak hashes, deprecated ciphers, timing-unsafe comparisons, or Math.random in a security-shaped context.",
|
|
9316
9247
|
line: location.line,
|
|
@@ -12200,7 +12131,7 @@ const jsxMaxDepth = defineRule({
|
|
|
12200
12131
|
});
|
|
12201
12132
|
//#endregion
|
|
12202
12133
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
12203
|
-
const MESSAGE$
|
|
12134
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
12204
12135
|
const LITERAL_TEXT_TAGS = new Set([
|
|
12205
12136
|
"code",
|
|
12206
12137
|
"pre",
|
|
@@ -12248,7 +12179,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
12248
12179
|
if (isInsideLiteralTextTag(node)) return;
|
|
12249
12180
|
context.report({
|
|
12250
12181
|
node,
|
|
12251
|
-
message: MESSAGE$
|
|
12182
|
+
message: MESSAGE$47
|
|
12252
12183
|
});
|
|
12253
12184
|
} })
|
|
12254
12185
|
});
|
|
@@ -12279,7 +12210,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
12279
12210
|
};
|
|
12280
12211
|
//#endregion
|
|
12281
12212
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
12282
|
-
const MESSAGE$
|
|
12213
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
12283
12214
|
const CONTEXT_MODULES$1 = [
|
|
12284
12215
|
"react",
|
|
12285
12216
|
"use-context-selector",
|
|
@@ -12377,7 +12308,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
12377
12308
|
if (!isConstructedValue(innerExpression)) continue;
|
|
12378
12309
|
context.report({
|
|
12379
12310
|
node: attribute,
|
|
12380
|
-
message: MESSAGE$
|
|
12311
|
+
message: MESSAGE$46
|
|
12381
12312
|
});
|
|
12382
12313
|
}
|
|
12383
12314
|
}
|
|
@@ -12461,8 +12392,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
12461
12392
|
};
|
|
12462
12393
|
//#endregion
|
|
12463
12394
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
12464
|
-
const
|
|
12465
|
-
const MESSAGE_UNKNOWN = "If this child is memoized, it still redraws every render because the prop gets brand new JSX each time.";
|
|
12395
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
12466
12396
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
12467
12397
|
"icon",
|
|
12468
12398
|
"Icon",
|
|
@@ -12725,8 +12655,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
12725
12655
|
if (isJsxAttributeOnIntrinsicHtmlElement(node)) return;
|
|
12726
12656
|
const parentJsxOpening = node.parent;
|
|
12727
12657
|
const openingName = parentJsxOpening && isNodeOfType(parentJsxOpening, "JSXOpeningElement") ? parentJsxOpening.name : null;
|
|
12728
|
-
|
|
12729
|
-
if (memoStatus === "not-memoised") return;
|
|
12658
|
+
if (memoStatusForJsxOpeningName(memoRegistry, openingName) === "not-memoised") return;
|
|
12730
12659
|
if (isNodeOfType(node.name, "JSXIdentifier") && isSlotPropName(node.name.name)) return;
|
|
12731
12660
|
if (!isInsideFunctionScope(node)) return;
|
|
12732
12661
|
const value = node.value;
|
|
@@ -12737,7 +12666,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
12737
12666
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
12738
12667
|
context.report({
|
|
12739
12668
|
node,
|
|
12740
|
-
message:
|
|
12669
|
+
message: MESSAGE$45
|
|
12741
12670
|
});
|
|
12742
12671
|
}
|
|
12743
12672
|
};
|
|
@@ -18017,108 +17946,19 @@ const isArrayFromLengthObjectCall = (node) => {
|
|
|
18017
17946
|
}
|
|
18018
17947
|
return false;
|
|
18019
17948
|
};
|
|
18020
|
-
const
|
|
18021
|
-
const isStringKeywordAnnotation = (typeAnnotation) => Boolean(typeAnnotation && isNodeOfType(typeAnnotation, "TSTypeAnnotation") && isNodeOfType(typeAnnotation.typeAnnotation, "TSStringKeyword"));
|
|
18022
|
-
const findSameFileTypeDeclaration = (referenceNode, typeName) => {
|
|
18023
|
-
const programRoot = findProgramRoot(referenceNode);
|
|
18024
|
-
if (!programRoot || !isNodeOfType(programRoot, "Program")) return null;
|
|
18025
|
-
for (const statement of programRoot.body) {
|
|
18026
|
-
const declaration = isNodeOfType(statement, "ExportNamedDeclaration") ? statement.declaration : statement;
|
|
18027
|
-
if (!declaration) continue;
|
|
18028
|
-
if ((isNodeOfType(declaration, "TSInterfaceDeclaration") || isNodeOfType(declaration, "TSTypeAliasDeclaration")) && isNodeOfType(declaration.id, "Identifier") && declaration.id.name === typeName) return declaration;
|
|
18029
|
-
}
|
|
18030
|
-
return null;
|
|
18031
|
-
};
|
|
18032
|
-
const typeDeclaresStringProperty = (typeNode, propertyName, referenceNode, depth) => {
|
|
18033
|
-
if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return false;
|
|
18034
|
-
let members = null;
|
|
18035
|
-
if (isNodeOfType(typeNode, "TSTypeLiteral")) members = typeNode.members;
|
|
18036
|
-
else if (isNodeOfType(typeNode, "TSInterfaceDeclaration")) members = typeNode.body.body;
|
|
18037
|
-
if (members) {
|
|
18038
|
-
for (const member of members) {
|
|
18039
|
-
if (!isNodeOfType(member, "TSPropertySignature")) continue;
|
|
18040
|
-
if (!isNodeOfType(member.key, "Identifier") || member.key.name !== propertyName) continue;
|
|
18041
|
-
return isStringKeywordAnnotation(member.typeAnnotation);
|
|
18042
|
-
}
|
|
18043
|
-
return false;
|
|
18044
|
-
}
|
|
18045
|
-
if (isNodeOfType(typeNode, "TSTypeAliasDeclaration")) return typeDeclaresStringProperty(typeNode.typeAnnotation, propertyName, referenceNode, depth + 1);
|
|
18046
|
-
if (isNodeOfType(typeNode, "TSTypeReference") && isNodeOfType(typeNode.typeName, "Identifier")) {
|
|
18047
|
-
const declaration = findSameFileTypeDeclaration(referenceNode, typeNode.typeName.name);
|
|
18048
|
-
if (!declaration) return false;
|
|
18049
|
-
return typeDeclaresStringProperty(declaration, propertyName, referenceNode, depth + 1);
|
|
18050
|
-
}
|
|
18051
|
-
return false;
|
|
18052
|
-
};
|
|
18053
|
-
const isDestructuredFromStringTypedPattern = (bindingIdentifier) => {
|
|
18054
|
-
const property = bindingIdentifier.parent;
|
|
18055
|
-
if (!property || !isNodeOfType(property, "Property")) return false;
|
|
18056
|
-
if (!isNodeOfType(property.key, "Identifier")) return false;
|
|
18057
|
-
const pattern = property.parent;
|
|
18058
|
-
if (!pattern || !isNodeOfType(pattern, "ObjectPattern")) return false;
|
|
18059
|
-
const typeAnnotation = pattern.typeAnnotation;
|
|
18060
|
-
if (!typeAnnotation || !isNodeOfType(typeAnnotation, "TSTypeAnnotation")) return false;
|
|
18061
|
-
return typeDeclaresStringProperty(typeAnnotation.typeAnnotation, property.key.name, bindingIdentifier, 0);
|
|
18062
|
-
};
|
|
18063
|
-
const isProvablyStringValued = (expression, depth) => {
|
|
18064
|
-
if (depth > TYPE_RESOLUTION_DEPTH_LIMIT) return false;
|
|
18065
|
-
const node = stripParenExpression(expression);
|
|
18066
|
-
if (isNodeOfType(node, "Literal")) return typeof node.value === "string";
|
|
18067
|
-
if (isNodeOfType(node, "TemplateLiteral")) return true;
|
|
18068
|
-
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "String") return true;
|
|
18069
|
-
if (isNodeOfType(node, "Identifier")) {
|
|
18070
|
-
const binding = findVariableInitializer(node, node.name);
|
|
18071
|
-
if (!binding) return false;
|
|
18072
|
-
if (binding.initializer && isProvablyStringValued(binding.initializer, depth + 1)) return true;
|
|
18073
|
-
if (isNodeOfType(binding.bindingIdentifier, "Identifier") && isStringKeywordAnnotation(binding.bindingIdentifier.typeAnnotation)) return true;
|
|
18074
|
-
return isDestructuredFromStringTypedPattern(binding.bindingIdentifier);
|
|
18075
|
-
}
|
|
18076
|
-
return false;
|
|
18077
|
-
};
|
|
18078
|
-
const hasProvablyStringFirstArgument = (callNode) => {
|
|
18079
|
-
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
18080
|
-
const source = callNode.arguments?.[0];
|
|
18081
|
-
return Boolean(source && isProvablyStringValued(source, 0));
|
|
18082
|
-
};
|
|
18083
|
-
/**
|
|
18084
|
-
* `[...str]`, `str.split(...)`, and `Array.from(str)` slice ONE string
|
|
18085
|
-
* into positional fragments (characters, lines, tokens). Fragment
|
|
18086
|
-
* position IS the entry's stable identity — nothing reorders, filters,
|
|
18087
|
-
* or carries per-item state — so an index key is correct there.
|
|
18088
|
-
* `.split(...)` needs no string proof (only strings have `.split`);
|
|
18089
|
-
* the spread / `Array.from` forms do, because both are equally common
|
|
18090
|
-
* on arrays.
|
|
18091
|
-
*/
|
|
18092
|
-
const isStringDerivedReceiver = (receiver, depth = 0) => {
|
|
18093
|
-
const node = stripParenExpression(receiver);
|
|
18094
|
-
if (isNodeOfType(node, "Identifier")) {
|
|
18095
|
-
if (depth >= TYPE_RESOLUTION_DEPTH_LIMIT) return false;
|
|
18096
|
-
const binding = findVariableInitializer(node, node.name);
|
|
18097
|
-
if (!binding?.initializer) return false;
|
|
18098
|
-
return isStringDerivedReceiver(binding.initializer, depth + 1);
|
|
18099
|
-
}
|
|
18100
|
-
if (isNodeOfType(node, "ArrayExpression") && node.elements?.length === 1) {
|
|
18101
|
-
const only = node.elements[0];
|
|
18102
|
-
if (only && isNodeOfType(only, "SpreadElement")) return isProvablyStringValued(only.argument, 0);
|
|
18103
|
-
}
|
|
18104
|
-
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "MemberExpression") && isNodeOfType(node.callee.property, "Identifier") && node.callee.property.name === "split") return true;
|
|
18105
|
-
return isArrayFromCall(node) && hasProvablyStringFirstArgument(node);
|
|
18106
|
-
};
|
|
18107
|
-
const isInsideIteratorMapMatching = (node, matchesMethodReceiver, matchesArrayFromCall) => {
|
|
17949
|
+
const isInsideStaticPlaceholderMap = (node) => {
|
|
18108
17950
|
let current = node;
|
|
18109
17951
|
while (current.parent) {
|
|
18110
17952
|
const parent = current.parent;
|
|
18111
17953
|
if (isFunctionLike$1(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
18112
17954
|
const callee = parent.callee;
|
|
18113
|
-
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return
|
|
18114
|
-
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return
|
|
17955
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
|
|
17956
|
+
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
18115
17957
|
}
|
|
18116
17958
|
current = parent;
|
|
18117
17959
|
}
|
|
18118
17960
|
return false;
|
|
18119
17961
|
};
|
|
18120
|
-
const isInsideStringDerivedMap = (node) => isInsideIteratorMapMatching(node, isStringDerivedReceiver, hasProvablyStringFirstArgument);
|
|
18121
|
-
const isInsideStaticPlaceholderMap = (node) => isInsideIteratorMapMatching(node, isStaticPlaceholderReceiver, isArrayFromLengthObjectCall);
|
|
18122
17962
|
/**
|
|
18123
17963
|
* Walk up from a JSXAttribute node looking for the enclosing iterator
|
|
18124
17964
|
* callback (`.map(cb)`, `.flatMap(cb)`, `.forEach(cb)`, `Array.from(_, cb)`)
|
|
@@ -18174,7 +18014,6 @@ const noArrayIndexAsKey = defineRule({
|
|
|
18174
18014
|
const indexName = extractIndexName(node.value.expression);
|
|
18175
18015
|
if (!indexName) return;
|
|
18176
18016
|
if (isInsideStaticPlaceholderMap(node)) return;
|
|
18177
|
-
if (isInsideStringDerivedMap(node)) return;
|
|
18178
18017
|
if (isCompositeKeyWithIteratorIdentity(node.value.expression, node)) return;
|
|
18179
18018
|
const openingElement = node.parent;
|
|
18180
18019
|
if (openingElement && isNodeOfType(openingElement, "JSXOpeningElement")) {
|
|
@@ -20012,25 +19851,6 @@ const countSetterCallSites = (ref) => {
|
|
|
20012
19851
|
}
|
|
20013
19852
|
return count;
|
|
20014
19853
|
};
|
|
20015
|
-
const collectUpdaterParameterReads = (analysis, updaterFn) => {
|
|
20016
|
-
const reads = [];
|
|
20017
|
-
for (const ref of getDownstreamRefs(analysis, updaterFn.body)) if (ref.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updaterFn)) reads.push(ref.identifier);
|
|
20018
|
-
return reads;
|
|
20019
|
-
};
|
|
20020
|
-
const readsParameterOnlyViaObjectSpread = (parameterReads) => parameterReads.every((read) => {
|
|
20021
|
-
const spread = read.parent;
|
|
20022
|
-
if (!spread || !isNodeOfType(spread, "SpreadElement")) return false;
|
|
20023
|
-
const container = spread.parent;
|
|
20024
|
-
return Boolean(container && isNodeOfType(container, "ObjectExpression"));
|
|
20025
|
-
});
|
|
20026
|
-
const isAccumulatingFunctionalUpdater = (analysis, callExpr) => {
|
|
20027
|
-
if (!isNodeOfType(callExpr, "CallExpression")) return false;
|
|
20028
|
-
const firstArgument = callExpr.arguments?.[0];
|
|
20029
|
-
if (!firstArgument || !isFunctionLike$1(firstArgument)) return false;
|
|
20030
|
-
const parameterReads = collectUpdaterParameterReads(analysis, firstArgument);
|
|
20031
|
-
if (parameterReads.length === 0) return false;
|
|
20032
|
-
return !readsParameterOnlyViaObjectSpread(parameterReads);
|
|
20033
|
-
};
|
|
20034
19854
|
const getStateNameForUseStateDecl = (useStateNode) => {
|
|
20035
19855
|
if (!useStateNode || !isNodeOfType(useStateNode, "VariableDeclarator")) return null;
|
|
20036
19856
|
if (!isNodeOfType(useStateNode.id, "ArrayPattern")) return null;
|
|
@@ -20065,7 +19885,6 @@ const noDerivedState = defineRule({
|
|
|
20065
19885
|
const depsUpstreamRefs = depsRefs.flatMap((depRef) => getUpstreamRefs(analysis, depRef));
|
|
20066
19886
|
if (isInitialOnlySetterCall(callExpr)) continue;
|
|
20067
19887
|
if (isControlledPropMirror(node, callExpr)) continue;
|
|
20068
|
-
if (isAccumulatingFunctionalUpdater(analysis, callExpr)) continue;
|
|
20069
19888
|
const isSomeArgsInternal = argsUpstreamRefs.some((argRef) => isState(analysis, argRef) || isProp(analysis, argRef));
|
|
20070
19889
|
const isValueAlwaysInSync = argsUpstreamRefs.length > 0 && argsUpstreamRefs.every((argRef) => depsUpstreamRefs.some((depRef) => argRef.resolved === depRef.resolved)) && countSetterCallSites(ref) === 1;
|
|
20071
19890
|
if (isSomeArgsInternal) context.report({
|
|
@@ -31055,67 +30874,35 @@ const preferUseSyncExternalStore = defineRule({
|
|
|
31055
30874
|
});
|
|
31056
30875
|
//#endregion
|
|
31057
30876
|
//#region src/plugin/rules/state-and-effects/prefer-use-reducer.ts
|
|
31058
|
-
const getSetterNameFromStatement = (statement, setterNames) => {
|
|
31059
|
-
let expression = null;
|
|
31060
|
-
if (isNodeOfType(statement, "ExpressionStatement")) expression = statement.expression;
|
|
31061
|
-
else if (isNodeOfType(statement, "ReturnStatement")) expression = statement.argument;
|
|
31062
|
-
if (!expression) return null;
|
|
31063
|
-
const call = stripParenExpression(expression);
|
|
31064
|
-
if (!isNodeOfType(call, "CallExpression")) return null;
|
|
31065
|
-
if (!isNodeOfType(call.callee, "Identifier")) return null;
|
|
31066
|
-
return setterNames.has(call.callee.name) ? call.callee.name : null;
|
|
31067
|
-
};
|
|
31068
|
-
const findLargestCoUpdatedSetterGroup = (componentBody, setterNames) => {
|
|
31069
|
-
let largestGroupSize = 0;
|
|
31070
|
-
const visit = (node, isInsideNestedFunction) => {
|
|
31071
|
-
if (isNodeOfType(node, "BlockStatement") && isInsideNestedFunction) {
|
|
31072
|
-
const groupSetterNames = /* @__PURE__ */ new Set();
|
|
31073
|
-
for (const statement of node.body ?? []) {
|
|
31074
|
-
const setterName = getSetterNameFromStatement(statement, setterNames);
|
|
31075
|
-
if (setterName) groupSetterNames.add(setterName);
|
|
31076
|
-
}
|
|
31077
|
-
largestGroupSize = Math.max(largestGroupSize, groupSetterNames.size);
|
|
31078
|
-
}
|
|
31079
|
-
const enteringNestedFunction = isInsideNestedFunction || isFunctionLike$1(node);
|
|
31080
|
-
const record = node;
|
|
31081
|
-
for (const key of Object.keys(record)) {
|
|
31082
|
-
if (key === "parent" || key === "type") continue;
|
|
31083
|
-
const child = record[key];
|
|
31084
|
-
if (Array.isArray(child)) {
|
|
31085
|
-
for (const item of child) if (isAstNode(item)) visit(item, enteringNestedFunction);
|
|
31086
|
-
} else if (isAstNode(child)) visit(child, enteringNestedFunction);
|
|
31087
|
-
}
|
|
31088
|
-
};
|
|
31089
|
-
visit(componentBody, false);
|
|
31090
|
-
return largestGroupSize;
|
|
31091
|
-
};
|
|
31092
30877
|
const preferUseReducer = defineRule({
|
|
31093
30878
|
id: "prefer-useReducer",
|
|
31094
30879
|
title: "Many related useState calls",
|
|
31095
30880
|
tags: ["test-noise"],
|
|
31096
30881
|
severity: "warn",
|
|
31097
|
-
recommendation: "Group state
|
|
30882
|
+
recommendation: "Group related state in `useReducer` so one logical update does not fan out into separate renders.",
|
|
31098
30883
|
create: (context) => {
|
|
31099
|
-
const
|
|
30884
|
+
const reportExcessiveUseState = (body, componentName) => {
|
|
31100
30885
|
if (!isNodeOfType(body, "BlockStatement")) return;
|
|
31101
|
-
|
|
31102
|
-
|
|
31103
|
-
|
|
31104
|
-
|
|
30886
|
+
let useStateCount = 0;
|
|
30887
|
+
for (const statement of body.body ?? []) {
|
|
30888
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
30889
|
+
for (const declarator of statement.declarations ?? []) if (declarator.init && isHookCall$1(declarator.init, "useState")) useStateCount++;
|
|
30890
|
+
}
|
|
30891
|
+
if (useStateCount >= 5) context.report({
|
|
31105
30892
|
node: body,
|
|
31106
|
-
message:
|
|
30893
|
+
message: `${useStateCount} useState calls in "${componentName}" can each trigger a separate render.`
|
|
31107
30894
|
});
|
|
31108
30895
|
};
|
|
31109
30896
|
return {
|
|
31110
30897
|
FunctionDeclaration(node) {
|
|
31111
30898
|
if (!node.id?.name || !isUppercaseName(node.id.name)) return;
|
|
31112
|
-
|
|
30899
|
+
reportExcessiveUseState(node.body, node.id.name);
|
|
31113
30900
|
},
|
|
31114
30901
|
VariableDeclarator(node) {
|
|
31115
30902
|
if (!isComponentAssignment(node)) return;
|
|
31116
30903
|
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
31117
30904
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
31118
|
-
|
|
30905
|
+
reportExcessiveUseState(node.init.body, node.id.name);
|
|
31119
30906
|
}
|
|
31120
30907
|
};
|
|
31121
30908
|
}
|
|
@@ -34382,17 +34169,6 @@ const collectReturnedJsxRoots = (functionNode) => {
|
|
|
34382
34169
|
return roots;
|
|
34383
34170
|
};
|
|
34384
34171
|
const isChildrenForwardingJsxChild = (child, bindings) => isNodeOfType(child, "JSXExpressionContainer") && isChildrenValueExpression(child.expression, bindings);
|
|
34385
|
-
const fragmentChildrenOrNull = (child) => {
|
|
34386
|
-
if (isNodeOfType(child, "JSXFragment")) return child.children ?? [];
|
|
34387
|
-
if (isNodeOfType(child, "JSXElement") && isJsxFragmentElement(child.openingElement)) return child.children ?? [];
|
|
34388
|
-
return null;
|
|
34389
|
-
};
|
|
34390
|
-
const isChildrenForwardingJsxChildThroughFragments = (child, bindings) => {
|
|
34391
|
-
if (isChildrenForwardingJsxChild(child, bindings)) return true;
|
|
34392
|
-
const fragmentChildren = fragmentChildrenOrNull(child);
|
|
34393
|
-
if (fragmentChildren === null) return false;
|
|
34394
|
-
return fragmentChildren.some((innerChild) => isChildrenForwardingJsxChildThroughFragments(innerChild, bindings));
|
|
34395
|
-
};
|
|
34396
34172
|
const isChildrenForwardingAttribute = (attribute, bindings) => {
|
|
34397
34173
|
if (isNodeOfType(attribute, "JSXSpreadAttribute")) return isPropsObjectExpression(attribute.argument, bindings);
|
|
34398
34174
|
return isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && attribute.name.name === "children" && isNodeOfType(attribute.value, "JSXExpressionContainer") && isChildrenValueExpression(attribute.value.expression, bindings);
|
|
@@ -34404,7 +34180,7 @@ const jsxRootForwardsChildrenIntoText = (jsxRoot, bindings, isTextHandlingElemen
|
|
|
34404
34180
|
if (!isNodeOfType(node, "JSXElement")) return void 0;
|
|
34405
34181
|
const elementName = resolveJsxElementName(node.openingElement);
|
|
34406
34182
|
if (!elementName || !isTextHandlingElement(elementName)) return;
|
|
34407
|
-
didForwardIntoText = (node.children ?? []).some((child) =>
|
|
34183
|
+
didForwardIntoText = (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings)) || (node.openingElement.attributes ?? []).some((attribute) => isChildrenForwardingAttribute(attribute, bindings));
|
|
34408
34184
|
});
|
|
34409
34185
|
return didForwardIntoText;
|
|
34410
34186
|
};
|
|
@@ -34422,7 +34198,7 @@ const jsxRootForwardsChildren = (jsxRoot, bindings, isTextHandlingElement, count
|
|
|
34422
34198
|
return;
|
|
34423
34199
|
}
|
|
34424
34200
|
}
|
|
34425
|
-
if (countsAsForwardTarget(node) && (node.children ?? []).some((child) =>
|
|
34201
|
+
if (countsAsForwardTarget(node) && (node.children ?? []).some((child) => isChildrenForwardingJsxChild(child, bindings))) didForward = true;
|
|
34426
34202
|
});
|
|
34427
34203
|
return didForward;
|
|
34428
34204
|
};
|
|
@@ -34500,13 +34276,14 @@ const recordWrapperFromDeclaration = (componentName, definitionNode, isTextHandl
|
|
|
34500
34276
|
if (kind === "text") wrappers.add(componentName);
|
|
34501
34277
|
else if (kind === "nonText") nonTextWrappers.add(componentName);
|
|
34502
34278
|
};
|
|
34279
|
+
const MAX_TRANSITIVE_WRAPPER_PASSES = 3;
|
|
34503
34280
|
const collectTextWrapperComponents = (programNode, isTextHandlingRoot, isNonTextHostRoot) => {
|
|
34504
34281
|
const wrappers = /* @__PURE__ */ new Set();
|
|
34505
34282
|
const nonTextWrappers = /* @__PURE__ */ new Set();
|
|
34506
34283
|
const isTextHandlingElement = (elementName) => isTextHandlingRoot(elementName) || wrappers.has(elementName);
|
|
34507
34284
|
const isNonTextHostElement = (elementName) => isNonTextHostRoot(elementName) || nonTextWrappers.has(elementName);
|
|
34508
34285
|
const recordDeclaration = (componentName, definitionNode) => recordWrapperFromDeclaration(componentName, definitionNode, isTextHandlingElement, isNonTextHostElement, wrappers, nonTextWrappers);
|
|
34509
|
-
|
|
34286
|
+
for (let pass = 0; pass < MAX_TRANSITIVE_WRAPPER_PASSES; pass += 1) {
|
|
34510
34287
|
const wrappersSizeBeforePass = wrappers.size;
|
|
34511
34288
|
const nonTextSizeBeforePass = nonTextWrappers.size;
|
|
34512
34289
|
walkAst(programNode, (node) => {
|
|
@@ -39188,7 +38965,7 @@ const isAuthGuardName = (calleeName) => {
|
|
|
39188
38965
|
//#endregion
|
|
39189
38966
|
//#region src/plugin/utils/is-non-privileged-server-action.ts
|
|
39190
38967
|
const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
|
|
39191
|
-
const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name)
|
|
38968
|
+
const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name);
|
|
39192
38969
|
const unwrapExpression = (node) => {
|
|
39193
38970
|
let current = node;
|
|
39194
38971
|
while (current) {
|
|
@@ -39213,7 +38990,6 @@ const isDataExposingValue = (node) => {
|
|
|
39213
38990
|
const isLiteralOnlyExpression = (node) => {
|
|
39214
38991
|
if (!node) return false;
|
|
39215
38992
|
if (isNodeOfType(node, "Literal")) return true;
|
|
39216
|
-
if (isNodeOfType(node, "Identifier")) return node.name === "undefined";
|
|
39217
38993
|
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every(isLiteralOnlyExpression);
|
|
39218
38994
|
if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression(node.argument);
|
|
39219
38995
|
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => element === null || !isNodeOfType(element, "SpreadElement") && isLiteralOnlyExpression(element));
|