oxlint-plugin-react-doctor 0.5.6-dev.b08ca1c → 0.5.6-dev.b8170f8
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 +126 -0
- package/dist/index.js +352 -150
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -890,24 +890,64 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
890
890
|
});
|
|
891
891
|
//#endregion
|
|
892
892
|
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
893
|
-
const
|
|
893
|
+
const WHITESPACE_PATTERN = /\s/;
|
|
894
|
+
const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
895
|
+
for (let cursor = openQuoteIndex + 1; cursor < content.length; cursor += 1) {
|
|
896
|
+
const character = content[cursor];
|
|
897
|
+
if (character === "\\") {
|
|
898
|
+
cursor += 1;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (character === delimiter) return false;
|
|
902
|
+
if (WHITESPACE_PATTERN.test(character)) return true;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
};
|
|
906
|
+
const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
894
907
|
const characters = content.split("");
|
|
895
908
|
let stringDelimiter = null;
|
|
909
|
+
let isBlankingString = false;
|
|
910
|
+
const templateExpressionDepths = [];
|
|
896
911
|
let index = 0;
|
|
912
|
+
const blankUnlessNewline = (offset) => {
|
|
913
|
+
if (offset < content.length && content[offset] !== "\n") characters[offset] = " ";
|
|
914
|
+
};
|
|
897
915
|
while (index < content.length) {
|
|
898
916
|
const character = content[index];
|
|
899
917
|
const nextCharacter = content[index + 1];
|
|
900
918
|
if (stringDelimiter !== null) {
|
|
901
919
|
if (character === "\\") {
|
|
920
|
+
if (isBlankingString) {
|
|
921
|
+
blankUnlessNewline(index);
|
|
922
|
+
blankUnlessNewline(index + 1);
|
|
923
|
+
}
|
|
902
924
|
index += 2;
|
|
903
925
|
continue;
|
|
904
926
|
}
|
|
905
|
-
if (character === stringDelimiter)
|
|
927
|
+
if (character === stringDelimiter) {
|
|
928
|
+
stringDelimiter = null;
|
|
929
|
+
index += 1;
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
if (blankStringContents && stringDelimiter === "`" && character === "$" && nextCharacter === "{") {
|
|
933
|
+
templateExpressionDepths.push(0);
|
|
934
|
+
stringDelimiter = null;
|
|
935
|
+
index += 2;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
if (isBlankingString) blankUnlessNewline(index);
|
|
906
939
|
index += 1;
|
|
907
940
|
continue;
|
|
908
941
|
}
|
|
909
|
-
if (character === "\"" || character === "'"
|
|
942
|
+
if (character === "\"" || character === "'") {
|
|
910
943
|
stringDelimiter = character;
|
|
944
|
+
isBlankingString = blankStringContents && quotedLiteralHasWhitespace(content, index, character);
|
|
945
|
+
index += 1;
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (character === "`") {
|
|
949
|
+
stringDelimiter = "`";
|
|
950
|
+
isBlankingString = blankStringContents;
|
|
911
951
|
index += 1;
|
|
912
952
|
continue;
|
|
913
953
|
}
|
|
@@ -926,29 +966,42 @@ const stripCommentsPreservingPositions = (content) => {
|
|
|
926
966
|
index += 2;
|
|
927
967
|
break;
|
|
928
968
|
}
|
|
929
|
-
|
|
969
|
+
blankUnlessNewline(index);
|
|
930
970
|
index += 1;
|
|
931
971
|
}
|
|
932
972
|
continue;
|
|
933
973
|
}
|
|
974
|
+
if (templateExpressionDepths.length > 0) {
|
|
975
|
+
const innermost = templateExpressionDepths.length - 1;
|
|
976
|
+
if (character === "{") templateExpressionDepths[innermost] += 1;
|
|
977
|
+
else if (character === "}") if (templateExpressionDepths[innermost] === 0) {
|
|
978
|
+
templateExpressionDepths.pop();
|
|
979
|
+
stringDelimiter = "`";
|
|
980
|
+
isBlankingString = blankStringContents;
|
|
981
|
+
} else templateExpressionDepths[innermost] -= 1;
|
|
982
|
+
}
|
|
934
983
|
index += 1;
|
|
935
984
|
}
|
|
936
985
|
return characters.join("");
|
|
937
986
|
};
|
|
987
|
+
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
988
|
+
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
938
989
|
//#endregion
|
|
939
990
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
940
991
|
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
941
|
-
const
|
|
992
|
+
const stringStrippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
993
|
+
const getScannableContent = (file, ignoreStringLiterals = false) => {
|
|
942
994
|
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
943
|
-
const
|
|
995
|
+
const cache = ignoreStringLiterals ? stringStrippedContentCache : strippedContentCache;
|
|
996
|
+
const cachedContent = cache.get(file);
|
|
944
997
|
if (cachedContent !== void 0) return cachedContent;
|
|
945
|
-
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
946
|
-
|
|
998
|
+
const strippedContent = ignoreStringLiterals ? stripCommentsAndStringLiteralsPreservingPositions(file.content) : stripCommentsPreservingPositions(file.content);
|
|
999
|
+
cache.set(file, strippedContent);
|
|
947
1000
|
return strippedContent;
|
|
948
1001
|
};
|
|
949
|
-
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
1002
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreStringLiterals, message }) => (file) => {
|
|
950
1003
|
if (!shouldScan(file)) return [];
|
|
951
|
-
const content = getScannableContent(file);
|
|
1004
|
+
const content = getScannableContent(file, ignoreStringLiterals);
|
|
952
1005
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
953
1006
|
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
954
1007
|
if (matchedPattern === void 0) return [];
|
|
@@ -973,6 +1026,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
973
1026
|
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
974
1027
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
975
1028
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1029
|
+
ignoreStringLiterals: true,
|
|
976
1030
|
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
977
1031
|
})
|
|
978
1032
|
});
|
|
@@ -1861,7 +1915,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1915
|
});
|
|
1862
1916
|
//#endregion
|
|
1863
1917
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1918
|
+
const MESSAGE$59 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1919
|
const anchorHasContent = defineRule({
|
|
1866
1920
|
id: "anchor-has-content",
|
|
1867
1921
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1931,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1931
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1932
|
context.report({
|
|
1879
1933
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1934
|
+
message: MESSAGE$59
|
|
1881
1935
|
});
|
|
1882
1936
|
} })
|
|
1883
1937
|
});
|
|
@@ -2271,7 +2325,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2325
|
};
|
|
2272
2326
|
//#endregion
|
|
2273
2327
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2328
|
+
const MESSAGE$58 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2329
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2330
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2331
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2343,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2343
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2344
|
context.report({
|
|
2291
2345
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2346
|
+
message: MESSAGE$58
|
|
2293
2347
|
});
|
|
2294
2348
|
return;
|
|
2295
2349
|
}
|
|
2296
2350
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2351
|
context.report({
|
|
2298
2352
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2353
|
+
message: MESSAGE$58
|
|
2300
2354
|
});
|
|
2301
2355
|
} })
|
|
2302
2356
|
});
|
|
@@ -3085,7 +3139,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3139
|
scan: scanByPattern({
|
|
3086
3140
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3141
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3142
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3143
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3144
|
})
|
|
3091
3145
|
});
|
|
@@ -4272,7 +4326,7 @@ const asyncParallel = defineRule({
|
|
|
4272
4326
|
});
|
|
4273
4327
|
//#endregion
|
|
4274
4328
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4275
|
-
const MESSAGE$
|
|
4329
|
+
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.";
|
|
4276
4330
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
4331
|
const STORAGE_GLOBALS = new Set([
|
|
4278
4332
|
"window",
|
|
@@ -4306,7 +4360,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4306
4360
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
4361
|
context.report({
|
|
4308
4362
|
node,
|
|
4309
|
-
message: MESSAGE$
|
|
4363
|
+
message: MESSAGE$57
|
|
4310
4364
|
});
|
|
4311
4365
|
},
|
|
4312
4366
|
AssignmentExpression(node) {
|
|
@@ -4317,7 +4371,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4317
4371
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
4372
|
context.report({
|
|
4319
4373
|
node: target,
|
|
4320
|
-
message: MESSAGE$
|
|
4374
|
+
message: MESSAGE$57
|
|
4321
4375
|
});
|
|
4322
4376
|
}
|
|
4323
4377
|
})
|
|
@@ -4694,7 +4748,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4694
4748
|
//#endregion
|
|
4695
4749
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4696
4750
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4697
|
-
const MESSAGE$
|
|
4751
|
+
const MESSAGE$56 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4698
4752
|
const KEY_HANDLERS = [
|
|
4699
4753
|
"onKeyUp",
|
|
4700
4754
|
"onKeyDown",
|
|
@@ -4726,7 +4780,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4726
4780
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4727
4781
|
context.report({
|
|
4728
4782
|
node: node.name,
|
|
4729
|
-
message: MESSAGE$
|
|
4783
|
+
message: MESSAGE$56
|
|
4730
4784
|
});
|
|
4731
4785
|
} };
|
|
4732
4786
|
}
|
|
@@ -4841,7 +4895,7 @@ const isReactComponentName = (name) => {
|
|
|
4841
4895
|
};
|
|
4842
4896
|
//#endregion
|
|
4843
4897
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4844
|
-
const MESSAGE$
|
|
4898
|
+
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`.";
|
|
4845
4899
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4846
4900
|
const DEFAULT_LABELLING_PROPS = [
|
|
4847
4901
|
"alt",
|
|
@@ -5002,7 +5056,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
5002
5056
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
5003
5057
|
context.report({
|
|
5004
5058
|
node: opening,
|
|
5005
|
-
message: MESSAGE$
|
|
5059
|
+
message: MESSAGE$55
|
|
5006
5060
|
});
|
|
5007
5061
|
} };
|
|
5008
5062
|
}
|
|
@@ -5432,7 +5486,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
5432
5486
|
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5433
5487
|
//#endregion
|
|
5434
5488
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5435
|
-
const MESSAGE$
|
|
5489
|
+
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.";
|
|
5436
5490
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5437
5491
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5438
5492
|
"aria-label",
|
|
@@ -5455,7 +5509,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5455
5509
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5456
5510
|
context.report({
|
|
5457
5511
|
node: node.name,
|
|
5458
|
-
message: MESSAGE$
|
|
5512
|
+
message: MESSAGE$54
|
|
5459
5513
|
});
|
|
5460
5514
|
} })
|
|
5461
5515
|
});
|
|
@@ -5494,7 +5548,7 @@ const isEs6Component = (node) => {
|
|
|
5494
5548
|
};
|
|
5495
5549
|
//#endregion
|
|
5496
5550
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5497
|
-
const MESSAGE$
|
|
5551
|
+
const MESSAGE$53 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5498
5552
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5499
5553
|
"observer",
|
|
5500
5554
|
"lazy",
|
|
@@ -5697,7 +5751,7 @@ const displayName = defineRule({
|
|
|
5697
5751
|
const reportAt = (node) => {
|
|
5698
5752
|
context.report({
|
|
5699
5753
|
node,
|
|
5700
|
-
message: MESSAGE$
|
|
5754
|
+
message: MESSAGE$53
|
|
5701
5755
|
});
|
|
5702
5756
|
};
|
|
5703
5757
|
return {
|
|
@@ -7845,7 +7899,7 @@ const forbidElements = defineRule({
|
|
|
7845
7899
|
});
|
|
7846
7900
|
//#endregion
|
|
7847
7901
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7848
|
-
const MESSAGE$
|
|
7902
|
+
const MESSAGE$52 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7849
7903
|
const forwardRefUsesRef = defineRule({
|
|
7850
7904
|
id: "forward-ref-uses-ref",
|
|
7851
7905
|
title: "forwardRef without ref parameter",
|
|
@@ -7865,7 +7919,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7865
7919
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7866
7920
|
context.report({
|
|
7867
7921
|
node: inner,
|
|
7868
|
-
message: MESSAGE$
|
|
7922
|
+
message: MESSAGE$52
|
|
7869
7923
|
});
|
|
7870
7924
|
} })
|
|
7871
7925
|
});
|
|
@@ -7902,7 +7956,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7902
7956
|
});
|
|
7903
7957
|
//#endregion
|
|
7904
7958
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7905
|
-
const MESSAGE$
|
|
7959
|
+
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`.";
|
|
7906
7960
|
const DEFAULT_HEADING_TAGS = [
|
|
7907
7961
|
"h1",
|
|
7908
7962
|
"h2",
|
|
@@ -7935,7 +7989,7 @@ const headingHasContent = defineRule({
|
|
|
7935
7989
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7936
7990
|
context.report({
|
|
7937
7991
|
node,
|
|
7938
|
-
message: MESSAGE$
|
|
7992
|
+
message: MESSAGE$51
|
|
7939
7993
|
});
|
|
7940
7994
|
} };
|
|
7941
7995
|
}
|
|
@@ -8073,7 +8127,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8073
8127
|
});
|
|
8074
8128
|
//#endregion
|
|
8075
8129
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8076
|
-
const MESSAGE$
|
|
8130
|
+
const MESSAGE$50 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8077
8131
|
const resolveSettings$38 = (settings) => {
|
|
8078
8132
|
const reactDoctor = settings?.["react-doctor"];
|
|
8079
8133
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8121,7 +8175,7 @@ const htmlHasLang = defineRule({
|
|
|
8121
8175
|
if (!lang) {
|
|
8122
8176
|
context.report({
|
|
8123
8177
|
node: node.name,
|
|
8124
|
-
message: MESSAGE$
|
|
8178
|
+
message: MESSAGE$50
|
|
8125
8179
|
});
|
|
8126
8180
|
return;
|
|
8127
8181
|
}
|
|
@@ -8129,13 +8183,13 @@ const htmlHasLang = defineRule({
|
|
|
8129
8183
|
if (verdict === "missing" || verdict === "empty") {
|
|
8130
8184
|
context.report({
|
|
8131
8185
|
node: lang,
|
|
8132
|
-
message: MESSAGE$
|
|
8186
|
+
message: MESSAGE$50
|
|
8133
8187
|
});
|
|
8134
8188
|
return;
|
|
8135
8189
|
}
|
|
8136
8190
|
if (hasSpread && !lang) context.report({
|
|
8137
8191
|
node: node.name,
|
|
8138
|
-
message: MESSAGE$
|
|
8192
|
+
message: MESSAGE$50
|
|
8139
8193
|
});
|
|
8140
8194
|
} };
|
|
8141
8195
|
}
|
|
@@ -8349,7 +8403,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8349
8403
|
});
|
|
8350
8404
|
//#endregion
|
|
8351
8405
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8352
|
-
const MESSAGE$
|
|
8406
|
+
const MESSAGE$49 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8353
8407
|
const evaluateTitleValue = (value) => {
|
|
8354
8408
|
if (!value) return "missing";
|
|
8355
8409
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8389,14 +8443,14 @@ const iframeHasTitle = defineRule({
|
|
|
8389
8443
|
if (!titleAttr) {
|
|
8390
8444
|
if (hasSpread || tag === "iframe") context.report({
|
|
8391
8445
|
node: node.name,
|
|
8392
|
-
message: MESSAGE$
|
|
8446
|
+
message: MESSAGE$49
|
|
8393
8447
|
});
|
|
8394
8448
|
return;
|
|
8395
8449
|
}
|
|
8396
8450
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8397
8451
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8398
8452
|
node: titleAttr,
|
|
8399
|
-
message: MESSAGE$
|
|
8453
|
+
message: MESSAGE$49
|
|
8400
8454
|
});
|
|
8401
8455
|
} })
|
|
8402
8456
|
});
|
|
@@ -8500,7 +8554,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8500
8554
|
});
|
|
8501
8555
|
//#endregion
|
|
8502
8556
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8503
|
-
const MESSAGE$
|
|
8557
|
+
const MESSAGE$48 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8504
8558
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8505
8559
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8506
8560
|
"image",
|
|
@@ -8565,7 +8619,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8565
8619
|
if (!altAttribute) return;
|
|
8566
8620
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8567
8621
|
node: altAttribute,
|
|
8568
|
-
message: MESSAGE$
|
|
8622
|
+
message: MESSAGE$48
|
|
8569
8623
|
});
|
|
8570
8624
|
} };
|
|
8571
8625
|
}
|
|
@@ -10630,6 +10684,24 @@ const hasJsxKeyAttribute = (openingElement) => {
|
|
|
10630
10684
|
return false;
|
|
10631
10685
|
};
|
|
10632
10686
|
//#endregion
|
|
10687
|
+
//#region src/plugin/utils/is-non-children-jsx-attribute-value.ts
|
|
10688
|
+
const ascendThroughJsxValueWrappers = (node) => {
|
|
10689
|
+
let current = node;
|
|
10690
|
+
while (current.parent) {
|
|
10691
|
+
const parent = current.parent;
|
|
10692
|
+
if (!(isNodeOfType(parent, "ChainExpression") || isNodeOfType(parent, "TSAsExpression") || isNodeOfType(parent, "TSSatisfiesExpression") || isNodeOfType(parent, "TSNonNullExpression") || isNodeOfType(parent, "LogicalExpression") || isNodeOfType(parent, "ConditionalExpression") && parent.test !== current)) break;
|
|
10693
|
+
current = parent;
|
|
10694
|
+
}
|
|
10695
|
+
return current;
|
|
10696
|
+
};
|
|
10697
|
+
const isNonChildrenJsxAttributeValue = (node) => {
|
|
10698
|
+
const container = ascendThroughJsxValueWrappers(node).parent;
|
|
10699
|
+
if (!container || !isNodeOfType(container, "JSXExpressionContainer")) return false;
|
|
10700
|
+
const attribute = container.parent;
|
|
10701
|
+
if (!attribute || !isNodeOfType(attribute, "JSXAttribute")) return false;
|
|
10702
|
+
return getJsxAttributeName(attribute.name) !== "children";
|
|
10703
|
+
};
|
|
10704
|
+
//#endregion
|
|
10633
10705
|
//#region src/plugin/rules/react-builtins/jsx-key.ts
|
|
10634
10706
|
const ITERATOR_METHOD_NAMES = new Set([
|
|
10635
10707
|
"map",
|
|
@@ -10668,6 +10740,7 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10668
10740
|
const arrayParent = parent.parent;
|
|
10669
10741
|
if (arrayParent && isNodeOfType(arrayParent, "Property")) return null;
|
|
10670
10742
|
if (arrayParent && isNodeOfType(arrayParent, "ArrayExpression")) return null;
|
|
10743
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10671
10744
|
return { kind: "array" };
|
|
10672
10745
|
} else if (isNodeOfType(parent, "CallExpression")) {
|
|
10673
10746
|
const callee = parent.callee;
|
|
@@ -10680,10 +10753,13 @@ const findEnclosingIteratorContext = (jsxNode) => {
|
|
|
10680
10753
|
if (!targetArg) return null;
|
|
10681
10754
|
let walker = current;
|
|
10682
10755
|
while (walker && walker !== parent) {
|
|
10683
|
-
if (walker === targetArg)
|
|
10684
|
-
|
|
10685
|
-
|
|
10686
|
-
|
|
10756
|
+
if (walker === targetArg) {
|
|
10757
|
+
if (isNonChildrenJsxAttributeValue(parent)) return null;
|
|
10758
|
+
return {
|
|
10759
|
+
kind: "iterator",
|
|
10760
|
+
callExpression: parent
|
|
10761
|
+
};
|
|
10762
|
+
}
|
|
10687
10763
|
walker = walker.parent ?? null;
|
|
10688
10764
|
}
|
|
10689
10765
|
return null;
|
|
@@ -10922,7 +10998,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10922
10998
|
});
|
|
10923
10999
|
//#endregion
|
|
10924
11000
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10925
|
-
const MESSAGE$
|
|
11001
|
+
const MESSAGE$47 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10926
11002
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10927
11003
|
"code",
|
|
10928
11004
|
"pre",
|
|
@@ -10958,7 +11034,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10958
11034
|
if (isInsideLiteralTextTag(node)) return;
|
|
10959
11035
|
context.report({
|
|
10960
11036
|
node,
|
|
10961
|
-
message: MESSAGE$
|
|
11037
|
+
message: MESSAGE$47
|
|
10962
11038
|
});
|
|
10963
11039
|
} })
|
|
10964
11040
|
});
|
|
@@ -10989,7 +11065,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10989
11065
|
};
|
|
10990
11066
|
//#endregion
|
|
10991
11067
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10992
|
-
const MESSAGE$
|
|
11068
|
+
const MESSAGE$46 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10993
11069
|
const CONTEXT_MODULES$1 = [
|
|
10994
11070
|
"react",
|
|
10995
11071
|
"use-context-selector",
|
|
@@ -11087,7 +11163,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11087
11163
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11088
11164
|
context.report({
|
|
11089
11165
|
node: attribute,
|
|
11090
|
-
message: MESSAGE$
|
|
11166
|
+
message: MESSAGE$46
|
|
11091
11167
|
});
|
|
11092
11168
|
}
|
|
11093
11169
|
}
|
|
@@ -11173,7 +11249,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11173
11249
|
};
|
|
11174
11250
|
//#endregion
|
|
11175
11251
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11176
|
-
const MESSAGE$
|
|
11252
|
+
const MESSAGE$45 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11177
11253
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11178
11254
|
"icon",
|
|
11179
11255
|
"Icon",
|
|
@@ -11442,7 +11518,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11442
11518
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11443
11519
|
context.report({
|
|
11444
11520
|
node,
|
|
11445
|
-
message: MESSAGE$
|
|
11521
|
+
message: MESSAGE$45
|
|
11446
11522
|
});
|
|
11447
11523
|
}
|
|
11448
11524
|
};
|
|
@@ -11730,7 +11806,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11730
11806
|
];
|
|
11731
11807
|
//#endregion
|
|
11732
11808
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11733
|
-
const MESSAGE$
|
|
11809
|
+
const MESSAGE$44 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11734
11810
|
const isDataArrayPropName = (propName) => {
|
|
11735
11811
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11736
11812
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11814,7 +11890,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11814
11890
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11815
11891
|
context.report({
|
|
11816
11892
|
node,
|
|
11817
|
-
message: MESSAGE$
|
|
11893
|
+
message: MESSAGE$44
|
|
11818
11894
|
});
|
|
11819
11895
|
}
|
|
11820
11896
|
};
|
|
@@ -12072,7 +12148,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12072
12148
|
]);
|
|
12073
12149
|
//#endregion
|
|
12074
12150
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12075
|
-
const MESSAGE$
|
|
12151
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12076
12152
|
const isAccessorPredicateName = (propName) => {
|
|
12077
12153
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12078
12154
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12278,7 +12354,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12278
12354
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12279
12355
|
context.report({
|
|
12280
12356
|
node,
|
|
12281
|
-
message: MESSAGE$
|
|
12357
|
+
message: MESSAGE$43
|
|
12282
12358
|
});
|
|
12283
12359
|
}
|
|
12284
12360
|
};
|
|
@@ -12498,7 +12574,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12498
12574
|
];
|
|
12499
12575
|
//#endregion
|
|
12500
12576
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12501
|
-
const MESSAGE$
|
|
12577
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12502
12578
|
const isConfigObjectPropName = (propName) => {
|
|
12503
12579
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12504
12580
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12586,7 +12662,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12586
12662
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12587
12663
|
context.report({
|
|
12588
12664
|
node,
|
|
12589
|
-
message: MESSAGE$
|
|
12665
|
+
message: MESSAGE$42
|
|
12590
12666
|
});
|
|
12591
12667
|
}
|
|
12592
12668
|
};
|
|
@@ -12594,7 +12670,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12594
12670
|
});
|
|
12595
12671
|
//#endregion
|
|
12596
12672
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12597
|
-
const MESSAGE$
|
|
12673
|
+
const MESSAGE$41 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12598
12674
|
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;
|
|
12599
12675
|
const resolveSettings$28 = (settings) => {
|
|
12600
12676
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12635,7 +12711,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12635
12711
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12636
12712
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12637
12713
|
node: attribute,
|
|
12638
|
-
message: MESSAGE$
|
|
12714
|
+
message: MESSAGE$41
|
|
12639
12715
|
});
|
|
12640
12716
|
}
|
|
12641
12717
|
} };
|
|
@@ -12950,7 +13026,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12950
13026
|
});
|
|
12951
13027
|
//#endregion
|
|
12952
13028
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12953
|
-
const MESSAGE$
|
|
13029
|
+
const MESSAGE$40 = "You can't tell what props reach this element when you spread them.";
|
|
12954
13030
|
const resolveSettings$25 = (settings) => {
|
|
12955
13031
|
const reactDoctor = settings?.["react-doctor"];
|
|
12956
13032
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12991,7 +13067,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12991
13067
|
}
|
|
12992
13068
|
context.report({
|
|
12993
13069
|
node: attribute,
|
|
12994
|
-
message: MESSAGE$
|
|
13070
|
+
message: MESSAGE$40
|
|
12995
13071
|
});
|
|
12996
13072
|
}
|
|
12997
13073
|
} };
|
|
@@ -13219,7 +13295,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13219
13295
|
});
|
|
13220
13296
|
//#endregion
|
|
13221
13297
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13222
|
-
const MESSAGE$
|
|
13298
|
+
const MESSAGE$39 = "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`.";
|
|
13223
13299
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13224
13300
|
"aa",
|
|
13225
13301
|
"ab",
|
|
@@ -13431,7 +13507,7 @@ const lang = defineRule({
|
|
|
13431
13507
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13432
13508
|
context.report({
|
|
13433
13509
|
node: langAttr,
|
|
13434
|
-
message: MESSAGE$
|
|
13510
|
+
message: MESSAGE$39
|
|
13435
13511
|
});
|
|
13436
13512
|
return;
|
|
13437
13513
|
}
|
|
@@ -13440,7 +13516,7 @@ const lang = defineRule({
|
|
|
13440
13516
|
if (value === null) return;
|
|
13441
13517
|
if (!isValidLangTag(value)) context.report({
|
|
13442
13518
|
node: langAttr,
|
|
13443
|
-
message: MESSAGE$
|
|
13519
|
+
message: MESSAGE$39
|
|
13444
13520
|
});
|
|
13445
13521
|
} })
|
|
13446
13522
|
});
|
|
@@ -13466,6 +13542,7 @@ const mcpToolCapabilityRisk = defineRule({
|
|
|
13466
13542
|
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13467
13543
|
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13468
13544
|
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13545
|
+
ignoreStringLiterals: true,
|
|
13469
13546
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13470
13547
|
})
|
|
13471
13548
|
});
|
|
@@ -13484,7 +13561,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13484
13561
|
});
|
|
13485
13562
|
//#endregion
|
|
13486
13563
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13487
|
-
const MESSAGE$
|
|
13564
|
+
const MESSAGE$38 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13488
13565
|
const DEFAULT_AUDIO = ["audio"];
|
|
13489
13566
|
const DEFAULT_VIDEO = ["video"];
|
|
13490
13567
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13525,7 +13602,7 @@ const mediaHasCaption = defineRule({
|
|
|
13525
13602
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13526
13603
|
context.report({
|
|
13527
13604
|
node: node.name,
|
|
13528
|
-
message: MESSAGE$
|
|
13605
|
+
message: MESSAGE$38
|
|
13529
13606
|
});
|
|
13530
13607
|
return;
|
|
13531
13608
|
}
|
|
@@ -13542,7 +13619,7 @@ const mediaHasCaption = defineRule({
|
|
|
13542
13619
|
return kindValue.value.toLowerCase() === "captions";
|
|
13543
13620
|
})) context.report({
|
|
13544
13621
|
node: node.name,
|
|
13545
|
-
message: MESSAGE$
|
|
13622
|
+
message: MESSAGE$38
|
|
13546
13623
|
});
|
|
13547
13624
|
} };
|
|
13548
13625
|
}
|
|
@@ -15343,7 +15420,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15343
15420
|
});
|
|
15344
15421
|
//#endregion
|
|
15345
15422
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15346
|
-
const MESSAGE$
|
|
15423
|
+
const MESSAGE$37 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15347
15424
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15348
15425
|
const noAccessKey = defineRule({
|
|
15349
15426
|
id: "no-access-key",
|
|
@@ -15360,7 +15437,7 @@ const noAccessKey = defineRule({
|
|
|
15360
15437
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15361
15438
|
context.report({
|
|
15362
15439
|
node: accessKey,
|
|
15363
|
-
message: MESSAGE$
|
|
15440
|
+
message: MESSAGE$37
|
|
15364
15441
|
});
|
|
15365
15442
|
return;
|
|
15366
15443
|
}
|
|
@@ -15370,7 +15447,7 @@ const noAccessKey = defineRule({
|
|
|
15370
15447
|
if (isUndefinedIdentifier(expression)) return;
|
|
15371
15448
|
context.report({
|
|
15372
15449
|
node: accessKey,
|
|
15373
|
-
message: MESSAGE$
|
|
15450
|
+
message: MESSAGE$37
|
|
15374
15451
|
});
|
|
15375
15452
|
}
|
|
15376
15453
|
} })
|
|
@@ -15853,7 +15930,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15853
15930
|
});
|
|
15854
15931
|
//#endregion
|
|
15855
15932
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15856
|
-
const MESSAGE$
|
|
15933
|
+
const MESSAGE$36 = "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.";
|
|
15857
15934
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15858
15935
|
id: "no-aria-hidden-on-focusable",
|
|
15859
15936
|
title: "aria-hidden on focusable element",
|
|
@@ -15880,7 +15957,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15880
15957
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15881
15958
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15882
15959
|
node: ariaHidden,
|
|
15883
|
-
message: MESSAGE$
|
|
15960
|
+
message: MESSAGE$36
|
|
15884
15961
|
});
|
|
15885
15962
|
} })
|
|
15886
15963
|
});
|
|
@@ -16248,7 +16325,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16248
16325
|
});
|
|
16249
16326
|
//#endregion
|
|
16250
16327
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16251
|
-
const MESSAGE$
|
|
16328
|
+
const MESSAGE$35 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16252
16329
|
const SECOND_INDEX_METHODS = new Set([
|
|
16253
16330
|
"every",
|
|
16254
16331
|
"filter",
|
|
@@ -16452,7 +16529,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16452
16529
|
}
|
|
16453
16530
|
context.report({
|
|
16454
16531
|
node: keyAttribute,
|
|
16455
|
-
message: MESSAGE$
|
|
16532
|
+
message: MESSAGE$35
|
|
16456
16533
|
});
|
|
16457
16534
|
},
|
|
16458
16535
|
CallExpression(node) {
|
|
@@ -16472,7 +16549,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16472
16549
|
if (propName !== "key") continue;
|
|
16473
16550
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16474
16551
|
node: property,
|
|
16475
|
-
message: MESSAGE$
|
|
16552
|
+
message: MESSAGE$35
|
|
16476
16553
|
});
|
|
16477
16554
|
}
|
|
16478
16555
|
}
|
|
@@ -16480,7 +16557,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16480
16557
|
});
|
|
16481
16558
|
//#endregion
|
|
16482
16559
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16483
|
-
const MESSAGE$
|
|
16560
|
+
const MESSAGE$34 = "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.";
|
|
16484
16561
|
const noAsyncEffectCallback = defineRule({
|
|
16485
16562
|
id: "no-async-effect-callback",
|
|
16486
16563
|
title: "Async effect callback",
|
|
@@ -16494,13 +16571,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16494
16571
|
if (!callback.async) return;
|
|
16495
16572
|
context.report({
|
|
16496
16573
|
node: callback,
|
|
16497
|
-
message: MESSAGE$
|
|
16574
|
+
message: MESSAGE$34
|
|
16498
16575
|
});
|
|
16499
16576
|
} })
|
|
16500
16577
|
});
|
|
16501
16578
|
//#endregion
|
|
16502
16579
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16503
|
-
const MESSAGE$
|
|
16580
|
+
const MESSAGE$33 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16504
16581
|
const resolveSettings$21 = (settings) => {
|
|
16505
16582
|
const reactDoctor = settings?.["react-doctor"];
|
|
16506
16583
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16556,7 +16633,7 @@ const noAutofocus = defineRule({
|
|
|
16556
16633
|
}
|
|
16557
16634
|
context.report({
|
|
16558
16635
|
node: autoFocusAttribute,
|
|
16559
|
-
message: MESSAGE$
|
|
16636
|
+
message: MESSAGE$33
|
|
16560
16637
|
});
|
|
16561
16638
|
} };
|
|
16562
16639
|
}
|
|
@@ -17060,7 +17137,7 @@ const noChainStateUpdates = defineRule({
|
|
|
17060
17137
|
});
|
|
17061
17138
|
//#endregion
|
|
17062
17139
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
17063
|
-
const MESSAGE$
|
|
17140
|
+
const MESSAGE$32 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
17064
17141
|
const noChildrenProp = defineRule({
|
|
17065
17142
|
id: "no-children-prop",
|
|
17066
17143
|
title: "Children passed as a prop",
|
|
@@ -17072,7 +17149,7 @@ const noChildrenProp = defineRule({
|
|
|
17072
17149
|
if (node.name.name !== "children") return;
|
|
17073
17150
|
context.report({
|
|
17074
17151
|
node: node.name,
|
|
17075
|
-
message: MESSAGE$
|
|
17152
|
+
message: MESSAGE$32
|
|
17076
17153
|
});
|
|
17077
17154
|
},
|
|
17078
17155
|
CallExpression(node) {
|
|
@@ -17085,7 +17162,7 @@ const noChildrenProp = defineRule({
|
|
|
17085
17162
|
const propertyKey = property.key;
|
|
17086
17163
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17087
17164
|
node: propertyKey,
|
|
17088
|
-
message: MESSAGE$
|
|
17165
|
+
message: MESSAGE$32
|
|
17089
17166
|
});
|
|
17090
17167
|
}
|
|
17091
17168
|
}
|
|
@@ -17093,7 +17170,7 @@ const noChildrenProp = defineRule({
|
|
|
17093
17170
|
});
|
|
17094
17171
|
//#endregion
|
|
17095
17172
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17096
|
-
const MESSAGE$
|
|
17173
|
+
const MESSAGE$31 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17097
17174
|
const noCloneElement = defineRule({
|
|
17098
17175
|
id: "no-clone-element",
|
|
17099
17176
|
title: "cloneElement makes child props fragile",
|
|
@@ -17106,7 +17183,7 @@ const noCloneElement = defineRule({
|
|
|
17106
17183
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17107
17184
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17108
17185
|
node: callee,
|
|
17109
|
-
message: MESSAGE$
|
|
17186
|
+
message: MESSAGE$31
|
|
17110
17187
|
});
|
|
17111
17188
|
return;
|
|
17112
17189
|
}
|
|
@@ -17119,7 +17196,7 @@ const noCloneElement = defineRule({
|
|
|
17119
17196
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17120
17197
|
context.report({
|
|
17121
17198
|
node: callee,
|
|
17122
|
-
message: MESSAGE$
|
|
17199
|
+
message: MESSAGE$31
|
|
17123
17200
|
});
|
|
17124
17201
|
}
|
|
17125
17202
|
} })
|
|
@@ -17168,7 +17245,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17168
17245
|
};
|
|
17169
17246
|
//#endregion
|
|
17170
17247
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17171
|
-
const MESSAGE$
|
|
17248
|
+
const MESSAGE$30 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17172
17249
|
const CONTEXT_MODULES = [
|
|
17173
17250
|
"react",
|
|
17174
17251
|
"use-context-selector",
|
|
@@ -17204,13 +17281,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17204
17281
|
if (!componentOrHookName) return;
|
|
17205
17282
|
context.report({
|
|
17206
17283
|
node,
|
|
17207
|
-
message: `${MESSAGE$
|
|
17284
|
+
message: `${MESSAGE$30} (called inside "${componentOrHookName}")`
|
|
17208
17285
|
});
|
|
17209
17286
|
} })
|
|
17210
17287
|
});
|
|
17211
17288
|
//#endregion
|
|
17212
17289
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17213
|
-
const MESSAGE$
|
|
17290
|
+
const MESSAGE$29 = "`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.";
|
|
17214
17291
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17215
17292
|
id: "no-create-ref-in-function-component",
|
|
17216
17293
|
title: "createRef in function component",
|
|
@@ -17229,7 +17306,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17229
17306
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17230
17307
|
context.report({
|
|
17231
17308
|
node,
|
|
17232
|
-
message: MESSAGE$
|
|
17309
|
+
message: MESSAGE$29
|
|
17233
17310
|
});
|
|
17234
17311
|
} })
|
|
17235
17312
|
});
|
|
@@ -17369,7 +17446,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17369
17446
|
});
|
|
17370
17447
|
//#endregion
|
|
17371
17448
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17372
|
-
const MESSAGE$
|
|
17449
|
+
const MESSAGE$28 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17373
17450
|
const noDanger = defineRule({
|
|
17374
17451
|
id: "no-danger",
|
|
17375
17452
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17382,7 +17459,7 @@ const noDanger = defineRule({
|
|
|
17382
17459
|
if (!propAttribute) return;
|
|
17383
17460
|
context.report({
|
|
17384
17461
|
node: propAttribute.name,
|
|
17385
|
-
message: MESSAGE$
|
|
17462
|
+
message: MESSAGE$28
|
|
17386
17463
|
});
|
|
17387
17464
|
},
|
|
17388
17465
|
CallExpression(node) {
|
|
@@ -17394,7 +17471,7 @@ const noDanger = defineRule({
|
|
|
17394
17471
|
const propertyKey = property.key;
|
|
17395
17472
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17396
17473
|
node: propertyKey,
|
|
17397
|
-
message: MESSAGE$
|
|
17474
|
+
message: MESSAGE$28
|
|
17398
17475
|
});
|
|
17399
17476
|
}
|
|
17400
17477
|
}
|
|
@@ -17402,7 +17479,7 @@ const noDanger = defineRule({
|
|
|
17402
17479
|
});
|
|
17403
17480
|
//#endregion
|
|
17404
17481
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17405
|
-
const MESSAGE$
|
|
17482
|
+
const MESSAGE$27 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17406
17483
|
const isLineBreak = (child) => {
|
|
17407
17484
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17408
17485
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17472,7 +17549,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17472
17549
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17473
17550
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17474
17551
|
node: opening,
|
|
17475
|
-
message: MESSAGE$
|
|
17552
|
+
message: MESSAGE$27
|
|
17476
17553
|
});
|
|
17477
17554
|
},
|
|
17478
17555
|
CallExpression(node) {
|
|
@@ -17484,7 +17561,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17484
17561
|
if (!propsShape.hasDangerously) return;
|
|
17485
17562
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17486
17563
|
node,
|
|
17487
|
-
message: MESSAGE$
|
|
17564
|
+
message: MESSAGE$27
|
|
17488
17565
|
});
|
|
17489
17566
|
}
|
|
17490
17567
|
})
|
|
@@ -18061,7 +18138,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
18061
18138
|
//#endregion
|
|
18062
18139
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
18063
18140
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
18064
|
-
const MESSAGE$
|
|
18141
|
+
const MESSAGE$26 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
18065
18142
|
const resolveSettings$20 = (settings) => {
|
|
18066
18143
|
const reactDoctor = settings?.["react-doctor"];
|
|
18067
18144
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18080,7 +18157,7 @@ const noDidMountSetState = defineRule({
|
|
|
18080
18157
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18081
18158
|
context.report({
|
|
18082
18159
|
node: node.callee,
|
|
18083
|
-
message: MESSAGE$
|
|
18160
|
+
message: MESSAGE$26
|
|
18084
18161
|
});
|
|
18085
18162
|
} };
|
|
18086
18163
|
}
|
|
@@ -18088,7 +18165,7 @@ const noDidMountSetState = defineRule({
|
|
|
18088
18165
|
//#endregion
|
|
18089
18166
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18090
18167
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18091
|
-
const MESSAGE$
|
|
18168
|
+
const MESSAGE$25 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18092
18169
|
const resolveSettings$19 = (settings) => {
|
|
18093
18170
|
const reactDoctor = settings?.["react-doctor"];
|
|
18094
18171
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18107,7 +18184,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18107
18184
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18108
18185
|
context.report({
|
|
18109
18186
|
node: node.callee,
|
|
18110
|
-
message: MESSAGE$
|
|
18187
|
+
message: MESSAGE$25
|
|
18111
18188
|
});
|
|
18112
18189
|
} };
|
|
18113
18190
|
}
|
|
@@ -18130,7 +18207,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18130
18207
|
};
|
|
18131
18208
|
//#endregion
|
|
18132
18209
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18133
|
-
const MESSAGE$
|
|
18210
|
+
const MESSAGE$24 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18134
18211
|
const shouldIgnoreMutation = (node) => {
|
|
18135
18212
|
let isConstructor = false;
|
|
18136
18213
|
let isInsideCallExpression = false;
|
|
@@ -18152,7 +18229,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18152
18229
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18153
18230
|
context.report({
|
|
18154
18231
|
node: reportNode,
|
|
18155
|
-
message: MESSAGE$
|
|
18232
|
+
message: MESSAGE$24
|
|
18156
18233
|
});
|
|
18157
18234
|
};
|
|
18158
18235
|
const noDirectMutationState = defineRule({
|
|
@@ -18362,6 +18439,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18362
18439
|
} })
|
|
18363
18440
|
});
|
|
18364
18441
|
//#endregion
|
|
18442
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18443
|
+
const MESSAGE$23 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
18444
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18445
|
+
const noDocumentWrite = defineRule({
|
|
18446
|
+
id: "no-document-write",
|
|
18447
|
+
title: "document.write/writeln",
|
|
18448
|
+
severity: "warn",
|
|
18449
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18450
|
+
create: (context) => ({ CallExpression(node) {
|
|
18451
|
+
const callee = node.callee;
|
|
18452
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18453
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18454
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18455
|
+
context.report({
|
|
18456
|
+
node,
|
|
18457
|
+
message: MESSAGE$23
|
|
18458
|
+
});
|
|
18459
|
+
} })
|
|
18460
|
+
});
|
|
18461
|
+
//#endregion
|
|
18365
18462
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18366
18463
|
const noDynamicImportPath = defineRule({
|
|
18367
18464
|
id: "no-dynamic-import-path",
|
|
@@ -19740,7 +19837,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19740
19837
|
"ReactDOM",
|
|
19741
19838
|
"ReactDom"
|
|
19742
19839
|
]);
|
|
19743
|
-
const MESSAGE$
|
|
19840
|
+
const MESSAGE$22 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19744
19841
|
const noFindDomNode = defineRule({
|
|
19745
19842
|
id: "no-find-dom-node",
|
|
19746
19843
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19751,7 +19848,7 @@ const noFindDomNode = defineRule({
|
|
|
19751
19848
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19752
19849
|
context.report({
|
|
19753
19850
|
node: callee,
|
|
19754
|
-
message: MESSAGE$
|
|
19851
|
+
message: MESSAGE$22
|
|
19755
19852
|
});
|
|
19756
19853
|
return;
|
|
19757
19854
|
}
|
|
@@ -19762,7 +19859,7 @@ const noFindDomNode = defineRule({
|
|
|
19762
19859
|
if (callee.property.name !== "findDOMNode") return;
|
|
19763
19860
|
context.report({
|
|
19764
19861
|
node: callee.property,
|
|
19765
|
-
message: MESSAGE$
|
|
19862
|
+
message: MESSAGE$22
|
|
19766
19863
|
});
|
|
19767
19864
|
}
|
|
19768
19865
|
} })
|
|
@@ -20004,7 +20101,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
20004
20101
|
});
|
|
20005
20102
|
//#endregion
|
|
20006
20103
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20007
|
-
const MESSAGE$
|
|
20104
|
+
const MESSAGE$21 = "`<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.";
|
|
20008
20105
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20009
20106
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
20010
20107
|
title: "Lazy image with high fetchPriority",
|
|
@@ -20018,7 +20115,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
20018
20115
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20019
20116
|
context.report({
|
|
20020
20117
|
node: node.name,
|
|
20021
|
-
message: MESSAGE$
|
|
20118
|
+
message: MESSAGE$21
|
|
20022
20119
|
});
|
|
20023
20120
|
} })
|
|
20024
20121
|
});
|
|
@@ -20115,15 +20212,20 @@ const noInlineExhaustiveStyle = defineRule({
|
|
|
20115
20212
|
severity: "warn",
|
|
20116
20213
|
tags: ["test-noise", "react-jsx-only"],
|
|
20117
20214
|
recommendation: "Move the styles to a CSS class, CSS module, Tailwind utilities, or a styled component. Big inline objects are hard to read and rebuild on every update.",
|
|
20118
|
-
create: (context) =>
|
|
20119
|
-
|
|
20120
|
-
|
|
20121
|
-
|
|
20122
|
-
|
|
20123
|
-
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20215
|
+
create: (context) => {
|
|
20216
|
+
if (isGeneratedImageRenderContext(context)) return {};
|
|
20217
|
+
return { JSXAttribute(node) {
|
|
20218
|
+
const expression = getInlineStyleExpression(node);
|
|
20219
|
+
if (!expression) return;
|
|
20220
|
+
const propertyCount = expression.properties?.filter((property) => isNodeOfType(property, "Property")).length ?? 0;
|
|
20221
|
+
if (propertyCount < 8) return;
|
|
20222
|
+
if (isGeneratedImageRenderContext(context, node.parent ?? void 0)) return;
|
|
20223
|
+
context.report({
|
|
20224
|
+
node: expression,
|
|
20225
|
+
message: `This inline style has ${propertyCount} properties, which is hard to read & rebuilds every render. Move it to a CSS class, CSS module, or styled component.`
|
|
20226
|
+
});
|
|
20227
|
+
} };
|
|
20228
|
+
}
|
|
20127
20229
|
});
|
|
20128
20230
|
//#endregion
|
|
20129
20231
|
//#region src/plugin/rules/performance/no-inline-prop-on-memo-component.ts
|
|
@@ -20253,7 +20355,7 @@ const noIsMounted = defineRule({
|
|
|
20253
20355
|
});
|
|
20254
20356
|
//#endregion
|
|
20255
20357
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20256
|
-
const MESSAGE$
|
|
20358
|
+
const MESSAGE$20 = "`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)`.";
|
|
20257
20359
|
const isJsonMethodCall = (node, method) => {
|
|
20258
20360
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20259
20361
|
const callee = node.callee;
|
|
@@ -20270,13 +20372,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20270
20372
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20271
20373
|
context.report({
|
|
20272
20374
|
node,
|
|
20273
|
-
message: MESSAGE$
|
|
20375
|
+
message: MESSAGE$20
|
|
20274
20376
|
});
|
|
20275
20377
|
} })
|
|
20276
20378
|
});
|
|
20277
20379
|
//#endregion
|
|
20278
20380
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20279
|
-
const MESSAGE$
|
|
20381
|
+
const MESSAGE$19 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20280
20382
|
const isJsxElementTypeReference = (node) => {
|
|
20281
20383
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20282
20384
|
const typeName = node.typeName;
|
|
@@ -20293,7 +20395,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20293
20395
|
if (!typeAnnotation) return;
|
|
20294
20396
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20295
20397
|
node: typeAnnotation,
|
|
20296
|
-
message: MESSAGE$
|
|
20398
|
+
message: MESSAGE$19
|
|
20297
20399
|
});
|
|
20298
20400
|
};
|
|
20299
20401
|
const noJsxElementType = defineRule({
|
|
@@ -20748,7 +20850,7 @@ const noMoment = defineRule({
|
|
|
20748
20850
|
});
|
|
20749
20851
|
//#endregion
|
|
20750
20852
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20751
|
-
const MESSAGE$
|
|
20853
|
+
const MESSAGE$18 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20752
20854
|
const resolveSettings$16 = (settings) => {
|
|
20753
20855
|
const reactDoctor = settings?.["react-doctor"];
|
|
20754
20856
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21070,7 +21172,7 @@ const noMultiComp = defineRule({
|
|
|
21070
21172
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21071
21173
|
for (const component of flagged.slice(1)) context.report({
|
|
21072
21174
|
node: component.reportNode,
|
|
21073
|
-
message: MESSAGE$
|
|
21175
|
+
message: MESSAGE$18
|
|
21074
21176
|
});
|
|
21075
21177
|
} };
|
|
21076
21178
|
}
|
|
@@ -21238,7 +21340,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21238
21340
|
};
|
|
21239
21341
|
//#endregion
|
|
21240
21342
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21241
|
-
const MESSAGE$
|
|
21343
|
+
const MESSAGE$17 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21242
21344
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21243
21345
|
"copyWithin",
|
|
21244
21346
|
"fill",
|
|
@@ -21448,7 +21550,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21448
21550
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21449
21551
|
context.report({
|
|
21450
21552
|
node: options.crossFileConsumerCallSite,
|
|
21451
|
-
message: `${MESSAGE$
|
|
21553
|
+
message: `${MESSAGE$17} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21452
21554
|
});
|
|
21453
21555
|
return;
|
|
21454
21556
|
}
|
|
@@ -21457,7 +21559,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21457
21559
|
reportedNodes.add(mutation.node);
|
|
21458
21560
|
context.report({
|
|
21459
21561
|
node: mutation.node,
|
|
21460
|
-
message: MESSAGE$
|
|
21562
|
+
message: MESSAGE$17
|
|
21461
21563
|
});
|
|
21462
21564
|
}
|
|
21463
21565
|
};
|
|
@@ -21729,7 +21831,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21729
21831
|
});
|
|
21730
21832
|
//#endregion
|
|
21731
21833
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21732
|
-
const MESSAGE$
|
|
21834
|
+
const MESSAGE$16 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21733
21835
|
const resolveSettings$14 = (settings) => {
|
|
21734
21836
|
const reactDoctor = settings?.["react-doctor"];
|
|
21735
21837
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21757,7 +21859,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21757
21859
|
if (numeric === null) {
|
|
21758
21860
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21759
21861
|
node: tabIndex,
|
|
21760
|
-
message: MESSAGE$
|
|
21862
|
+
message: MESSAGE$16
|
|
21761
21863
|
});
|
|
21762
21864
|
return;
|
|
21763
21865
|
}
|
|
@@ -21770,7 +21872,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21770
21872
|
if (!roleAttribute) {
|
|
21771
21873
|
context.report({
|
|
21772
21874
|
node: tabIndex,
|
|
21773
|
-
message: MESSAGE$
|
|
21875
|
+
message: MESSAGE$16
|
|
21774
21876
|
});
|
|
21775
21877
|
return;
|
|
21776
21878
|
}
|
|
@@ -21784,7 +21886,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21784
21886
|
}
|
|
21785
21887
|
context.report({
|
|
21786
21888
|
node: tabIndex,
|
|
21787
|
-
message: MESSAGE$
|
|
21889
|
+
message: MESSAGE$16
|
|
21788
21890
|
});
|
|
21789
21891
|
} };
|
|
21790
21892
|
}
|
|
@@ -22475,7 +22577,7 @@ const noRandomKey = defineRule({
|
|
|
22475
22577
|
});
|
|
22476
22578
|
//#endregion
|
|
22477
22579
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22478
|
-
const MESSAGE$
|
|
22580
|
+
const MESSAGE$15 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22479
22581
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22480
22582
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22481
22583
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22501,13 +22603,13 @@ const noReactChildren = defineRule({
|
|
|
22501
22603
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22502
22604
|
context.report({
|
|
22503
22605
|
node: calleeOuter,
|
|
22504
|
-
message: MESSAGE$
|
|
22606
|
+
message: MESSAGE$15
|
|
22505
22607
|
});
|
|
22506
22608
|
return;
|
|
22507
22609
|
}
|
|
22508
22610
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22509
22611
|
node: calleeOuter,
|
|
22510
|
-
message: MESSAGE$
|
|
22612
|
+
message: MESSAGE$15
|
|
22511
22613
|
});
|
|
22512
22614
|
} })
|
|
22513
22615
|
});
|
|
@@ -22830,7 +22932,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22830
22932
|
});
|
|
22831
22933
|
//#endregion
|
|
22832
22934
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22833
|
-
const MESSAGE$
|
|
22935
|
+
const MESSAGE$14 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22834
22936
|
const isReactDomRenderCall = (node) => {
|
|
22835
22937
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22836
22938
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22854,7 +22956,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22854
22956
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22855
22957
|
context.report({
|
|
22856
22958
|
node: node.callee,
|
|
22857
|
-
message: MESSAGE$
|
|
22959
|
+
message: MESSAGE$14
|
|
22858
22960
|
});
|
|
22859
22961
|
} })
|
|
22860
22962
|
});
|
|
@@ -23552,7 +23654,7 @@ const getParentComponent = (node) => {
|
|
|
23552
23654
|
};
|
|
23553
23655
|
//#endregion
|
|
23554
23656
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23555
|
-
const MESSAGE$
|
|
23657
|
+
const MESSAGE$13 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23556
23658
|
const noSetState = defineRule({
|
|
23557
23659
|
id: "no-set-state",
|
|
23558
23660
|
title: "Local class state forbidden",
|
|
@@ -23567,7 +23669,7 @@ const noSetState = defineRule({
|
|
|
23567
23669
|
if (!getParentComponent(node)) return;
|
|
23568
23670
|
context.report({
|
|
23569
23671
|
node: node.callee,
|
|
23570
|
-
message: MESSAGE$
|
|
23672
|
+
message: MESSAGE$13
|
|
23571
23673
|
});
|
|
23572
23674
|
} })
|
|
23573
23675
|
});
|
|
@@ -23729,7 +23831,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23729
23831
|
};
|
|
23730
23832
|
//#endregion
|
|
23731
23833
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23732
|
-
const MESSAGE$
|
|
23834
|
+
const MESSAGE$12 = "Screen reader users can't tell this click handler is interactive because it has no `role`, so add a `role` or use a button or link.";
|
|
23733
23835
|
const DEFAULT_HANDLERS = [
|
|
23734
23836
|
"onClick",
|
|
23735
23837
|
"onMouseDown",
|
|
@@ -23789,7 +23891,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23789
23891
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23790
23892
|
context.report({
|
|
23791
23893
|
node: node.name,
|
|
23792
|
-
message: MESSAGE$
|
|
23894
|
+
message: MESSAGE$12
|
|
23793
23895
|
});
|
|
23794
23896
|
return;
|
|
23795
23897
|
}
|
|
@@ -23799,19 +23901,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23799
23901
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23800
23902
|
context.report({
|
|
23801
23903
|
node: node.name,
|
|
23802
|
-
message: MESSAGE$
|
|
23904
|
+
message: MESSAGE$12
|
|
23803
23905
|
});
|
|
23804
23906
|
return;
|
|
23805
23907
|
}
|
|
23806
23908
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23807
23909
|
context.report({
|
|
23808
23910
|
node: node.name,
|
|
23809
|
-
message: MESSAGE$
|
|
23911
|
+
message: MESSAGE$12
|
|
23810
23912
|
});
|
|
23811
23913
|
} };
|
|
23812
23914
|
}
|
|
23813
23915
|
});
|
|
23814
23916
|
//#endregion
|
|
23917
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
23918
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
23919
|
+
"disabled",
|
|
23920
|
+
"checked",
|
|
23921
|
+
"readonly",
|
|
23922
|
+
"required",
|
|
23923
|
+
"selected",
|
|
23924
|
+
"multiple",
|
|
23925
|
+
"autofocus",
|
|
23926
|
+
"autoplay",
|
|
23927
|
+
"controls",
|
|
23928
|
+
"loop",
|
|
23929
|
+
"muted",
|
|
23930
|
+
"open",
|
|
23931
|
+
"reversed",
|
|
23932
|
+
"default",
|
|
23933
|
+
"novalidate",
|
|
23934
|
+
"formnovalidate",
|
|
23935
|
+
"playsinline",
|
|
23936
|
+
"itemscope",
|
|
23937
|
+
"allowfullscreen"
|
|
23938
|
+
]);
|
|
23939
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
23940
|
+
id: "no-string-false-on-boolean-attribute",
|
|
23941
|
+
title: "String true/false on a boolean attribute",
|
|
23942
|
+
severity: "warn",
|
|
23943
|
+
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
23944
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
23945
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
23946
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
23947
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
23948
|
+
for (const attribute of node.attributes) {
|
|
23949
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23950
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23951
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
23952
|
+
const value = getJsxPropStringValue(attribute);
|
|
23953
|
+
if (value !== "false" && value !== "true") continue;
|
|
23954
|
+
const attributeName = attribute.name.name;
|
|
23955
|
+
const guidance = value === "false" ? `which React treats as truthy, so the attribute is applied even though you wrote "false". Use \`${attributeName}={false}\` (or omit the attribute) to keep it off` : `but a boolean attribute takes a boolean, not the string "true". Use \`${attributeName}\` or \`${attributeName}={true}\``;
|
|
23956
|
+
context.report({
|
|
23957
|
+
node: attribute,
|
|
23958
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
23959
|
+
});
|
|
23960
|
+
}
|
|
23961
|
+
} })
|
|
23962
|
+
});
|
|
23963
|
+
//#endregion
|
|
23815
23964
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23816
23965
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23817
23966
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23862,6 +24011,27 @@ const noStringRefs = defineRule({
|
|
|
23862
24011
|
}
|
|
23863
24012
|
});
|
|
23864
24013
|
//#endregion
|
|
24014
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24015
|
+
const MESSAGE$11 = "A synchronous `XMLHttpRequest` (`.open(method, url, false)`) freezes the main thread until the request finishes, blocking all rendering and input. Use `fetch()` or an async XHR (`open(method, url, true)`).";
|
|
24016
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24017
|
+
const noSyncXhr = defineRule({
|
|
24018
|
+
id: "no-sync-xhr",
|
|
24019
|
+
title: "Synchronous XMLHttpRequest",
|
|
24020
|
+
severity: "warn",
|
|
24021
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24022
|
+
create: (context) => ({ CallExpression(node) {
|
|
24023
|
+
const callee = node.callee;
|
|
24024
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24025
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24026
|
+
const asyncArgument = node.arguments?.[2];
|
|
24027
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24028
|
+
context.report({
|
|
24029
|
+
node,
|
|
24030
|
+
message: MESSAGE$11
|
|
24031
|
+
});
|
|
24032
|
+
} })
|
|
24033
|
+
});
|
|
24034
|
+
//#endregion
|
|
23865
24035
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23866
24036
|
const MESSAGE$10 = "This value is `undefined` because function components have no `this`.";
|
|
23867
24037
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
@@ -35437,13 +35607,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35437
35607
|
const collectDeclaredNames = (declaration) => {
|
|
35438
35608
|
const names = /* @__PURE__ */ new Set();
|
|
35439
35609
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35440
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35441
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35442
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35443
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35444
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35445
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35446
|
-
}
|
|
35610
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35447
35611
|
return names;
|
|
35448
35612
|
};
|
|
35449
35613
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35453,11 +35617,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35453
35617
|
};
|
|
35454
35618
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35455
35619
|
if (names.size === 0) return false;
|
|
35620
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35456
35621
|
let didRead = false;
|
|
35457
|
-
|
|
35458
|
-
if (
|
|
35459
|
-
|
|
35460
|
-
|
|
35622
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
35623
|
+
if (!declarator.init) continue;
|
|
35624
|
+
walkAst(declarator.init, (child) => {
|
|
35625
|
+
if (didRead) return;
|
|
35626
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
35627
|
+
});
|
|
35628
|
+
}
|
|
35461
35629
|
return didRead;
|
|
35462
35630
|
};
|
|
35463
35631
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36717,7 +36885,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36717
36885
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36718
36886
|
scan: scanByPattern({
|
|
36719
36887
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36720
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36888
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36721
36889
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36722
36890
|
})
|
|
36723
36891
|
});
|
|
@@ -39173,6 +39341,17 @@ const reactDoctorRules = [
|
|
|
39173
39341
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39174
39342
|
}
|
|
39175
39343
|
},
|
|
39344
|
+
{
|
|
39345
|
+
key: "react-doctor/no-document-write",
|
|
39346
|
+
id: "no-document-write",
|
|
39347
|
+
source: "react-doctor",
|
|
39348
|
+
originallyExternal: false,
|
|
39349
|
+
rule: {
|
|
39350
|
+
...noDocumentWrite,
|
|
39351
|
+
framework: "global",
|
|
39352
|
+
category: "Performance"
|
|
39353
|
+
}
|
|
39354
|
+
},
|
|
39176
39355
|
{
|
|
39177
39356
|
key: "react-doctor/no-dynamic-import-path",
|
|
39178
39357
|
id: "no-dynamic-import-path",
|
|
@@ -39982,6 +40161,18 @@ const reactDoctorRules = [
|
|
|
39982
40161
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39983
40162
|
}
|
|
39984
40163
|
},
|
|
40164
|
+
{
|
|
40165
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40166
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40167
|
+
source: "react-doctor",
|
|
40168
|
+
originallyExternal: false,
|
|
40169
|
+
rule: {
|
|
40170
|
+
...noStringFalseOnBooleanAttribute,
|
|
40171
|
+
framework: "global",
|
|
40172
|
+
category: "Bugs",
|
|
40173
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40174
|
+
}
|
|
40175
|
+
},
|
|
39985
40176
|
{
|
|
39986
40177
|
key: "react-doctor/no-string-refs",
|
|
39987
40178
|
id: "no-string-refs",
|
|
@@ -39994,6 +40185,17 @@ const reactDoctorRules = [
|
|
|
39994
40185
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39995
40186
|
}
|
|
39996
40187
|
},
|
|
40188
|
+
{
|
|
40189
|
+
key: "react-doctor/no-sync-xhr",
|
|
40190
|
+
id: "no-sync-xhr",
|
|
40191
|
+
source: "react-doctor",
|
|
40192
|
+
originallyExternal: false,
|
|
40193
|
+
rule: {
|
|
40194
|
+
...noSyncXhr,
|
|
40195
|
+
framework: "global",
|
|
40196
|
+
category: "Performance"
|
|
40197
|
+
}
|
|
40198
|
+
},
|
|
39997
40199
|
{
|
|
39998
40200
|
key: "react-doctor/no-this-in-sfc",
|
|
39999
40201
|
id: "no-this-in-sfc",
|