oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.b08ca1c
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 +294 -0
- package/dist/index.js +561 -144
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1861,7 +1861,7 @@ const anchorAmbiguousText = defineRule({
|
|
|
1861
1861
|
});
|
|
1862
1862
|
//#endregion
|
|
1863
1863
|
//#region src/plugin/rules/a11y/anchor-has-content.ts
|
|
1864
|
-
const MESSAGE$
|
|
1864
|
+
const MESSAGE$57 = "Blind users can't follow this link because screen readers announce nothing, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
1865
1865
|
const anchorHasContent = defineRule({
|
|
1866
1866
|
id: "anchor-has-content",
|
|
1867
1867
|
title: "Anchor has no content",
|
|
@@ -1877,7 +1877,7 @@ const anchorHasContent = defineRule({
|
|
|
1877
1877
|
for (const attribute of ["title", "aria-label"]) if (hasJsxPropIgnoreCase(opening.attributes, attribute)) return;
|
|
1878
1878
|
context.report({
|
|
1879
1879
|
node: opening.name,
|
|
1880
|
-
message: MESSAGE$
|
|
1880
|
+
message: MESSAGE$57
|
|
1881
1881
|
});
|
|
1882
1882
|
} })
|
|
1883
1883
|
});
|
|
@@ -2271,7 +2271,7 @@ const parseJsxValue = (value) => {
|
|
|
2271
2271
|
};
|
|
2272
2272
|
//#endregion
|
|
2273
2273
|
//#region src/plugin/rules/a11y/aria-activedescendant-has-tabindex.ts
|
|
2274
|
-
const MESSAGE$
|
|
2274
|
+
const MESSAGE$56 = "Keyboard users can't focus this element with `aria-activedescendant` because it isn't tabbable, so add `tabIndex={0}`.";
|
|
2275
2275
|
const ariaActivedescendantHasTabindex = defineRule({
|
|
2276
2276
|
id: "aria-activedescendant-has-tabindex",
|
|
2277
2277
|
title: "aria-activedescendant missing tabindex",
|
|
@@ -2289,14 +2289,14 @@ const ariaActivedescendantHasTabindex = defineRule({
|
|
|
2289
2289
|
if (tabIndexValue === null || tabIndexValue >= -1) return;
|
|
2290
2290
|
context.report({
|
|
2291
2291
|
node: node.name,
|
|
2292
|
-
message: MESSAGE$
|
|
2292
|
+
message: MESSAGE$56
|
|
2293
2293
|
});
|
|
2294
2294
|
return;
|
|
2295
2295
|
}
|
|
2296
2296
|
if (isInteractiveElement(tag, node)) return;
|
|
2297
2297
|
context.report({
|
|
2298
2298
|
node: node.name,
|
|
2299
|
-
message: MESSAGE$
|
|
2299
|
+
message: MESSAGE$56
|
|
2300
2300
|
});
|
|
2301
2301
|
} })
|
|
2302
2302
|
});
|
|
@@ -3104,6 +3104,76 @@ const AUTH_FUNCTION_NAMES = new Set([
|
|
|
3104
3104
|
"getAuth",
|
|
3105
3105
|
"validateSession"
|
|
3106
3106
|
]);
|
|
3107
|
+
const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
|
|
3108
|
+
const AUTH_STANDALONE_NOUN_TOKENS = new Set([
|
|
3109
|
+
"signedin",
|
|
3110
|
+
"loggedin",
|
|
3111
|
+
"signin"
|
|
3112
|
+
]);
|
|
3113
|
+
const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
|
|
3114
|
+
"require",
|
|
3115
|
+
"ensure",
|
|
3116
|
+
"assert",
|
|
3117
|
+
"verify",
|
|
3118
|
+
"validate",
|
|
3119
|
+
"check",
|
|
3120
|
+
"protect",
|
|
3121
|
+
"enforce",
|
|
3122
|
+
"guard",
|
|
3123
|
+
"gate",
|
|
3124
|
+
"restrict",
|
|
3125
|
+
"is",
|
|
3126
|
+
"has",
|
|
3127
|
+
"can",
|
|
3128
|
+
"must"
|
|
3129
|
+
]);
|
|
3130
|
+
const AUTH_GETTER_VERB_TOKENS = new Set([
|
|
3131
|
+
"get",
|
|
3132
|
+
"fetch",
|
|
3133
|
+
"load",
|
|
3134
|
+
"read",
|
|
3135
|
+
"resolve",
|
|
3136
|
+
"retrieve",
|
|
3137
|
+
"use"
|
|
3138
|
+
]);
|
|
3139
|
+
const AUTH_QUALIFIER_TOKENS = new Set([
|
|
3140
|
+
"current",
|
|
3141
|
+
"my",
|
|
3142
|
+
"own"
|
|
3143
|
+
]);
|
|
3144
|
+
const AUTH_STRONG_NOUN_TOKENS = new Set([
|
|
3145
|
+
"session",
|
|
3146
|
+
"sessions",
|
|
3147
|
+
"login",
|
|
3148
|
+
"admin",
|
|
3149
|
+
"admins",
|
|
3150
|
+
"superadmin",
|
|
3151
|
+
"superuser",
|
|
3152
|
+
"role",
|
|
3153
|
+
"roles",
|
|
3154
|
+
"permission",
|
|
3155
|
+
"permissions",
|
|
3156
|
+
"jwt",
|
|
3157
|
+
"identity",
|
|
3158
|
+
"principal",
|
|
3159
|
+
"credential",
|
|
3160
|
+
"credentials"
|
|
3161
|
+
]);
|
|
3162
|
+
const AUTH_WEAK_NOUN_TOKENS = new Set([
|
|
3163
|
+
"user",
|
|
3164
|
+
"users",
|
|
3165
|
+
"account",
|
|
3166
|
+
"accounts",
|
|
3167
|
+
"token",
|
|
3168
|
+
"tokens",
|
|
3169
|
+
"access",
|
|
3170
|
+
"me",
|
|
3171
|
+
"viewer",
|
|
3172
|
+
"caller",
|
|
3173
|
+
"subject",
|
|
3174
|
+
"scope",
|
|
3175
|
+
"scopes"
|
|
3176
|
+
]);
|
|
3107
3177
|
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3108
3178
|
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3109
3179
|
const SECRET_PATTERNS = [
|
|
@@ -4201,6 +4271,58 @@ const asyncParallel = defineRule({
|
|
|
4201
4271
|
}
|
|
4202
4272
|
});
|
|
4203
4273
|
//#endregion
|
|
4274
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4275
|
+
const MESSAGE$55 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4276
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
|
+
const STORAGE_GLOBALS = new Set([
|
|
4278
|
+
"window",
|
|
4279
|
+
"globalThis",
|
|
4280
|
+
"self"
|
|
4281
|
+
]);
|
|
4282
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4283
|
+
const isWebStorageObject = (node) => {
|
|
4284
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4285
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4286
|
+
return false;
|
|
4287
|
+
};
|
|
4288
|
+
const staticMemberName = (member) => {
|
|
4289
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4290
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4291
|
+
return null;
|
|
4292
|
+
};
|
|
4293
|
+
const authTokenInWebStorage = defineRule({
|
|
4294
|
+
id: "auth-token-in-web-storage",
|
|
4295
|
+
title: "Auth token in web storage",
|
|
4296
|
+
severity: "warn",
|
|
4297
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4298
|
+
create: (context) => ({
|
|
4299
|
+
CallExpression(node) {
|
|
4300
|
+
const callee = node.callee;
|
|
4301
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4302
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4303
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4304
|
+
const keyArgument = node.arguments?.[0];
|
|
4305
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4306
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
|
+
context.report({
|
|
4308
|
+
node,
|
|
4309
|
+
message: MESSAGE$55
|
|
4310
|
+
});
|
|
4311
|
+
},
|
|
4312
|
+
AssignmentExpression(node) {
|
|
4313
|
+
const target = node.left;
|
|
4314
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4315
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4316
|
+
const propertyName = staticMemberName(target);
|
|
4317
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
|
+
context.report({
|
|
4319
|
+
node: target,
|
|
4320
|
+
message: MESSAGE$55
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
})
|
|
4324
|
+
});
|
|
4325
|
+
//#endregion
|
|
4204
4326
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4205
4327
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4206
4328
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4572,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4572
4694
|
//#endregion
|
|
4573
4695
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
4696
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4575
|
-
const MESSAGE$
|
|
4697
|
+
const MESSAGE$54 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4576
4698
|
const KEY_HANDLERS = [
|
|
4577
4699
|
"onKeyUp",
|
|
4578
4700
|
"onKeyDown",
|
|
@@ -4604,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4604
4726
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4727
|
context.report({
|
|
4606
4728
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4729
|
+
message: MESSAGE$54
|
|
4608
4730
|
});
|
|
4609
4731
|
} };
|
|
4610
4732
|
}
|
|
@@ -4719,7 +4841,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4841
|
};
|
|
4720
4842
|
//#endregion
|
|
4721
4843
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
const MESSAGE$
|
|
4844
|
+
const MESSAGE$53 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4723
4845
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4846
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4847
|
"alt",
|
|
@@ -4880,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
5002
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
5003
|
context.report({
|
|
4882
5004
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
5005
|
+
message: MESSAGE$53
|
|
4884
5006
|
});
|
|
4885
5007
|
} };
|
|
4886
5008
|
}
|
|
@@ -5306,6 +5428,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5428
|
} })
|
|
5307
5429
|
});
|
|
5308
5430
|
//#endregion
|
|
5431
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5432
|
+
const hasJsxSpreadAttribute$1 = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5433
|
+
//#endregion
|
|
5434
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5435
|
+
const MESSAGE$52 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5436
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5437
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5438
|
+
"aria-label",
|
|
5439
|
+
"aria-labelledby",
|
|
5440
|
+
"title"
|
|
5441
|
+
];
|
|
5442
|
+
const dialogHasAccessibleName = defineRule({
|
|
5443
|
+
id: "dialog-has-accessible-name",
|
|
5444
|
+
title: "Dialog without accessible name",
|
|
5445
|
+
severity: "warn",
|
|
5446
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5447
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5448
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5449
|
+
const tagName = node.name.name;
|
|
5450
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5451
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5452
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5453
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5454
|
+
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
5455
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5456
|
+
context.report({
|
|
5457
|
+
node: node.name,
|
|
5458
|
+
message: MESSAGE$52
|
|
5459
|
+
});
|
|
5460
|
+
} })
|
|
5461
|
+
});
|
|
5462
|
+
//#endregion
|
|
5309
5463
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5464
|
const PRAGMA$2 = "React";
|
|
5311
5465
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5494,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5494
|
};
|
|
5341
5495
|
//#endregion
|
|
5342
5496
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5497
|
+
const MESSAGE$51 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5498
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5499
|
"observer",
|
|
5346
5500
|
"lazy",
|
|
@@ -5543,7 +5697,7 @@ const displayName = defineRule({
|
|
|
5543
5697
|
const reportAt = (node) => {
|
|
5544
5698
|
context.report({
|
|
5545
5699
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5700
|
+
message: MESSAGE$51
|
|
5547
5701
|
});
|
|
5548
5702
|
};
|
|
5549
5703
|
return {
|
|
@@ -7691,7 +7845,7 @@ const forbidElements = defineRule({
|
|
|
7691
7845
|
});
|
|
7692
7846
|
//#endregion
|
|
7693
7847
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7848
|
+
const MESSAGE$50 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7849
|
const forwardRefUsesRef = defineRule({
|
|
7696
7850
|
id: "forward-ref-uses-ref",
|
|
7697
7851
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7865,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7865
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7866
|
context.report({
|
|
7713
7867
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7868
|
+
message: MESSAGE$50
|
|
7715
7869
|
});
|
|
7716
7870
|
} })
|
|
7717
7871
|
});
|
|
@@ -7748,7 +7902,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7902
|
});
|
|
7749
7903
|
//#endregion
|
|
7750
7904
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
const MESSAGE$
|
|
7905
|
+
const MESSAGE$49 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7752
7906
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7907
|
"h1",
|
|
7754
7908
|
"h2",
|
|
@@ -7781,7 +7935,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7935
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7936
|
context.report({
|
|
7783
7937
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7938
|
+
message: MESSAGE$49
|
|
7785
7939
|
});
|
|
7786
7940
|
} };
|
|
7787
7941
|
}
|
|
@@ -7919,7 +8073,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8073
|
});
|
|
7920
8074
|
//#endregion
|
|
7921
8075
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
const MESSAGE$
|
|
8076
|
+
const MESSAGE$48 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
7923
8077
|
const resolveSettings$38 = (settings) => {
|
|
7924
8078
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8079
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7967,7 +8121,7 @@ const htmlHasLang = defineRule({
|
|
|
7967
8121
|
if (!lang) {
|
|
7968
8122
|
context.report({
|
|
7969
8123
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8124
|
+
message: MESSAGE$48
|
|
7971
8125
|
});
|
|
7972
8126
|
return;
|
|
7973
8127
|
}
|
|
@@ -7975,13 +8129,13 @@ const htmlHasLang = defineRule({
|
|
|
7975
8129
|
if (verdict === "missing" || verdict === "empty") {
|
|
7976
8130
|
context.report({
|
|
7977
8131
|
node: lang,
|
|
7978
|
-
message: MESSAGE$
|
|
8132
|
+
message: MESSAGE$48
|
|
7979
8133
|
});
|
|
7980
8134
|
return;
|
|
7981
8135
|
}
|
|
7982
8136
|
if (hasSpread && !lang) context.report({
|
|
7983
8137
|
node: node.name,
|
|
7984
|
-
message: MESSAGE$
|
|
8138
|
+
message: MESSAGE$48
|
|
7985
8139
|
});
|
|
7986
8140
|
} };
|
|
7987
8141
|
}
|
|
@@ -8195,7 +8349,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8349
|
});
|
|
8196
8350
|
//#endregion
|
|
8197
8351
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
const MESSAGE$
|
|
8352
|
+
const MESSAGE$47 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8199
8353
|
const evaluateTitleValue = (value) => {
|
|
8200
8354
|
if (!value) return "missing";
|
|
8201
8355
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8389,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8389
|
if (!titleAttr) {
|
|
8236
8390
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8391
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8392
|
+
message: MESSAGE$47
|
|
8239
8393
|
});
|
|
8240
8394
|
return;
|
|
8241
8395
|
}
|
|
8242
8396
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8397
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8398
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8399
|
+
message: MESSAGE$47
|
|
8246
8400
|
});
|
|
8247
8401
|
} })
|
|
8248
8402
|
});
|
|
@@ -8346,7 +8500,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8500
|
});
|
|
8347
8501
|
//#endregion
|
|
8348
8502
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
const MESSAGE$
|
|
8503
|
+
const MESSAGE$46 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8350
8504
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8505
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8506
|
"image",
|
|
@@ -8411,7 +8565,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8565
|
if (!altAttribute) return;
|
|
8412
8566
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8567
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8568
|
+
message: MESSAGE$46
|
|
8415
8569
|
});
|
|
8416
8570
|
} };
|
|
8417
8571
|
}
|
|
@@ -10768,7 +10922,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10922
|
});
|
|
10769
10923
|
//#endregion
|
|
10770
10924
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10925
|
+
const MESSAGE$45 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10926
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10927
|
"code",
|
|
10774
10928
|
"pre",
|
|
@@ -10804,7 +10958,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
10958
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
10959
|
context.report({
|
|
10806
10960
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
10961
|
+
message: MESSAGE$45
|
|
10808
10962
|
});
|
|
10809
10963
|
} })
|
|
10810
10964
|
});
|
|
@@ -10835,7 +10989,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
10989
|
};
|
|
10836
10990
|
//#endregion
|
|
10837
10991
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
10992
|
+
const MESSAGE$44 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
10993
|
const CONTEXT_MODULES$1 = [
|
|
10840
10994
|
"react",
|
|
10841
10995
|
"use-context-selector",
|
|
@@ -10933,7 +11087,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11087
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11088
|
context.report({
|
|
10935
11089
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11090
|
+
message: MESSAGE$44
|
|
10937
11091
|
});
|
|
10938
11092
|
}
|
|
10939
11093
|
}
|
|
@@ -11019,7 +11173,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11173
|
};
|
|
11020
11174
|
//#endregion
|
|
11021
11175
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11176
|
+
const MESSAGE$43 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11177
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11178
|
"icon",
|
|
11025
11179
|
"Icon",
|
|
@@ -11288,7 +11442,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11442
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11443
|
context.report({
|
|
11290
11444
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11445
|
+
message: MESSAGE$43
|
|
11292
11446
|
});
|
|
11293
11447
|
}
|
|
11294
11448
|
};
|
|
@@ -11576,7 +11730,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11730
|
];
|
|
11577
11731
|
//#endregion
|
|
11578
11732
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11733
|
+
const MESSAGE$42 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11734
|
const isDataArrayPropName = (propName) => {
|
|
11581
11735
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11736
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11814,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11814
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11815
|
context.report({
|
|
11662
11816
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11817
|
+
message: MESSAGE$42
|
|
11664
11818
|
});
|
|
11665
11819
|
}
|
|
11666
11820
|
};
|
|
@@ -11918,7 +12072,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12072
|
]);
|
|
11919
12073
|
//#endregion
|
|
11920
12074
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12075
|
+
const MESSAGE$41 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12076
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12077
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12078
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12278,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12278
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12279
|
context.report({
|
|
12126
12280
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12281
|
+
message: MESSAGE$41
|
|
12128
12282
|
});
|
|
12129
12283
|
}
|
|
12130
12284
|
};
|
|
@@ -12344,7 +12498,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12498
|
];
|
|
12345
12499
|
//#endregion
|
|
12346
12500
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12501
|
+
const MESSAGE$40 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12502
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12503
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12504
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12586,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12586
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12587
|
context.report({
|
|
12434
12588
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12589
|
+
message: MESSAGE$40
|
|
12436
12590
|
});
|
|
12437
12591
|
}
|
|
12438
12592
|
};
|
|
@@ -12440,7 +12594,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12594
|
});
|
|
12441
12595
|
//#endregion
|
|
12442
12596
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12597
|
+
const MESSAGE$39 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
12598
|
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;
|
|
12445
12599
|
const resolveSettings$28 = (settings) => {
|
|
12446
12600
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12635,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12635
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12636
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12637
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12638
|
+
message: MESSAGE$39
|
|
12485
12639
|
});
|
|
12486
12640
|
}
|
|
12487
12641
|
} };
|
|
@@ -12796,7 +12950,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
12950
|
});
|
|
12797
12951
|
//#endregion
|
|
12798
12952
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
12953
|
+
const MESSAGE$38 = "You can't tell what props reach this element when you spread them.";
|
|
12800
12954
|
const resolveSettings$25 = (settings) => {
|
|
12801
12955
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
12956
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +12991,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
12991
|
}
|
|
12838
12992
|
context.report({
|
|
12839
12993
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
12994
|
+
message: MESSAGE$38
|
|
12841
12995
|
});
|
|
12842
12996
|
}
|
|
12843
12997
|
} };
|
|
@@ -13065,7 +13219,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13219
|
});
|
|
13066
13220
|
//#endregion
|
|
13067
13221
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
const MESSAGE$
|
|
13222
|
+
const MESSAGE$37 = "Screen readers can't pick the right voice because this `lang` isn't a real language code, so use a valid one like `en` or `en-US`.";
|
|
13069
13223
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13224
|
"aa",
|
|
13071
13225
|
"ab",
|
|
@@ -13277,7 +13431,7 @@ const lang = defineRule({
|
|
|
13277
13431
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13432
|
context.report({
|
|
13279
13433
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13434
|
+
message: MESSAGE$37
|
|
13281
13435
|
});
|
|
13282
13436
|
return;
|
|
13283
13437
|
}
|
|
@@ -13286,7 +13440,7 @@ const lang = defineRule({
|
|
|
13286
13440
|
if (value === null) return;
|
|
13287
13441
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13442
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13443
|
+
message: MESSAGE$37
|
|
13290
13444
|
});
|
|
13291
13445
|
} })
|
|
13292
13446
|
});
|
|
@@ -13330,7 +13484,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13484
|
});
|
|
13331
13485
|
//#endregion
|
|
13332
13486
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
const MESSAGE$
|
|
13487
|
+
const MESSAGE$36 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13334
13488
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13489
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13490
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13525,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13525
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13526
|
context.report({
|
|
13373
13527
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13528
|
+
message: MESSAGE$36
|
|
13375
13529
|
});
|
|
13376
13530
|
return;
|
|
13377
13531
|
}
|
|
@@ -13388,7 +13542,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13542
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13543
|
})) context.report({
|
|
13390
13544
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13545
|
+
message: MESSAGE$36
|
|
13392
13546
|
});
|
|
13393
13547
|
} };
|
|
13394
13548
|
}
|
|
@@ -15189,7 +15343,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15343
|
});
|
|
15190
15344
|
//#endregion
|
|
15191
15345
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15346
|
+
const MESSAGE$35 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15347
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15348
|
const noAccessKey = defineRule({
|
|
15195
15349
|
id: "no-access-key",
|
|
@@ -15206,7 +15360,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15360
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15361
|
context.report({
|
|
15208
15362
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15363
|
+
message: MESSAGE$35
|
|
15210
15364
|
});
|
|
15211
15365
|
return;
|
|
15212
15366
|
}
|
|
@@ -15216,7 +15370,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15370
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15371
|
context.report({
|
|
15218
15372
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15373
|
+
message: MESSAGE$35
|
|
15220
15374
|
});
|
|
15221
15375
|
}
|
|
15222
15376
|
} })
|
|
@@ -15699,7 +15853,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15699
15853
|
});
|
|
15700
15854
|
//#endregion
|
|
15701
15855
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
const MESSAGE$
|
|
15856
|
+
const MESSAGE$34 = "Screen reader users tab to this focusable element but hear nothing because `aria-hidden` skips it, so remove `aria-hidden` or stop it being focusable.";
|
|
15703
15857
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15858
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15859
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15880,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15880
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15881
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15882
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15883
|
+
message: MESSAGE$34
|
|
15730
15884
|
});
|
|
15731
15885
|
} })
|
|
15732
15886
|
});
|
|
@@ -16094,7 +16248,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16248
|
});
|
|
16095
16249
|
//#endregion
|
|
16096
16250
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16251
|
+
const MESSAGE$33 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16252
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16253
|
"every",
|
|
16100
16254
|
"filter",
|
|
@@ -16298,7 +16452,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16452
|
}
|
|
16299
16453
|
context.report({
|
|
16300
16454
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16455
|
+
message: MESSAGE$33
|
|
16302
16456
|
});
|
|
16303
16457
|
},
|
|
16304
16458
|
CallExpression(node) {
|
|
@@ -16318,15 +16472,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16472
|
if (propName !== "key") continue;
|
|
16319
16473
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16474
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16475
|
+
message: MESSAGE$33
|
|
16322
16476
|
});
|
|
16323
16477
|
}
|
|
16324
16478
|
}
|
|
16325
16479
|
})
|
|
16326
16480
|
});
|
|
16327
16481
|
//#endregion
|
|
16482
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16483
|
+
const MESSAGE$32 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16484
|
+
const noAsyncEffectCallback = defineRule({
|
|
16485
|
+
id: "no-async-effect-callback",
|
|
16486
|
+
title: "Async effect callback",
|
|
16487
|
+
severity: "warn",
|
|
16488
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16489
|
+
create: (context) => ({ CallExpression(node) {
|
|
16490
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16491
|
+
const callback = getEffectCallback(node);
|
|
16492
|
+
if (!callback) return;
|
|
16493
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16494
|
+
if (!callback.async) return;
|
|
16495
|
+
context.report({
|
|
16496
|
+
node: callback,
|
|
16497
|
+
message: MESSAGE$32
|
|
16498
|
+
});
|
|
16499
|
+
} })
|
|
16500
|
+
});
|
|
16501
|
+
//#endregion
|
|
16328
16502
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
const MESSAGE$
|
|
16503
|
+
const MESSAGE$31 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16330
16504
|
const resolveSettings$21 = (settings) => {
|
|
16331
16505
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16506
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,7 +16556,7 @@ const noAutofocus = defineRule({
|
|
|
16382
16556
|
}
|
|
16383
16557
|
context.report({
|
|
16384
16558
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16559
|
+
message: MESSAGE$31
|
|
16386
16560
|
});
|
|
16387
16561
|
} };
|
|
16388
16562
|
}
|
|
@@ -16632,6 +16806,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16806
|
}
|
|
16633
16807
|
});
|
|
16634
16808
|
//#endregion
|
|
16809
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16810
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16811
|
+
"FunctionDeclaration",
|
|
16812
|
+
"FunctionExpression",
|
|
16813
|
+
"ArrowFunctionExpression",
|
|
16814
|
+
"ClassDeclaration",
|
|
16815
|
+
"ClassExpression"
|
|
16816
|
+
]);
|
|
16817
|
+
const isReactImport$1 = (symbol) => {
|
|
16818
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16819
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16820
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16821
|
+
return importDeclaration.source.value === "react";
|
|
16822
|
+
};
|
|
16823
|
+
const getImportedName = (symbol) => {
|
|
16824
|
+
if (symbol.kind !== "import") return null;
|
|
16825
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16826
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16827
|
+
};
|
|
16828
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16829
|
+
if (symbol.kind !== "import") return false;
|
|
16830
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16831
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16832
|
+
};
|
|
16833
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16834
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16835
|
+
const symbol = scopes.symbolFor(callee);
|
|
16836
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16837
|
+
};
|
|
16838
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16839
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16840
|
+
if (callee.computed) return false;
|
|
16841
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16842
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16843
|
+
if (callee.property.name !== "createElement") return false;
|
|
16844
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16845
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16846
|
+
};
|
|
16847
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16848
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16849
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16850
|
+
};
|
|
16851
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16852
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16853
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16854
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16855
|
+
const nodeRecord = node;
|
|
16856
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16857
|
+
if (key === "parent") continue;
|
|
16858
|
+
const child = nodeRecord[key];
|
|
16859
|
+
if (Array.isArray(child)) {
|
|
16860
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16861
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16862
|
+
}
|
|
16863
|
+
return false;
|
|
16864
|
+
};
|
|
16865
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16866
|
+
//#endregion
|
|
16867
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16868
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16869
|
+
//#endregion
|
|
16870
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16871
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16872
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16873
|
+
const declaration = symbol.declarationNode;
|
|
16874
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16875
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16876
|
+
return false;
|
|
16877
|
+
};
|
|
16878
|
+
const noCallComponentAsFunction = defineRule({
|
|
16879
|
+
id: "no-call-component-as-function",
|
|
16880
|
+
title: "Component called as a function",
|
|
16881
|
+
severity: "warn",
|
|
16882
|
+
tags: ["test-noise"],
|
|
16883
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16884
|
+
create: (context) => {
|
|
16885
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16886
|
+
const candidateCalls = [];
|
|
16887
|
+
return {
|
|
16888
|
+
JSXOpeningElement(node) {
|
|
16889
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16890
|
+
},
|
|
16891
|
+
CallExpression(node) {
|
|
16892
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16893
|
+
node,
|
|
16894
|
+
callee: node.callee,
|
|
16895
|
+
name: node.callee.name
|
|
16896
|
+
});
|
|
16897
|
+
},
|
|
16898
|
+
"Program:exit"() {
|
|
16899
|
+
for (const candidate of candidateCalls) {
|
|
16900
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16901
|
+
if (!symbol) continue;
|
|
16902
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16903
|
+
node: candidate.node,
|
|
16904
|
+
message: message(candidate.name)
|
|
16905
|
+
});
|
|
16906
|
+
}
|
|
16907
|
+
}
|
|
16908
|
+
};
|
|
16909
|
+
}
|
|
16910
|
+
});
|
|
16911
|
+
//#endregion
|
|
16635
16912
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
16913
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
16914
|
//#endregion
|
|
@@ -16783,7 +17060,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
17060
|
});
|
|
16784
17061
|
//#endregion
|
|
16785
17062
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
const MESSAGE$
|
|
17063
|
+
const MESSAGE$30 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16787
17064
|
const noChildrenProp = defineRule({
|
|
16788
17065
|
id: "no-children-prop",
|
|
16789
17066
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17072,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17072
|
if (node.name.name !== "children") return;
|
|
16796
17073
|
context.report({
|
|
16797
17074
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17075
|
+
message: MESSAGE$30
|
|
16799
17076
|
});
|
|
16800
17077
|
},
|
|
16801
17078
|
CallExpression(node) {
|
|
@@ -16808,7 +17085,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17085
|
const propertyKey = property.key;
|
|
16809
17086
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17087
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17088
|
+
message: MESSAGE$30
|
|
16812
17089
|
});
|
|
16813
17090
|
}
|
|
16814
17091
|
}
|
|
@@ -16816,7 +17093,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17093
|
});
|
|
16817
17094
|
//#endregion
|
|
16818
17095
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
const MESSAGE$
|
|
17096
|
+
const MESSAGE$29 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
16820
17097
|
const noCloneElement = defineRule({
|
|
16821
17098
|
id: "no-clone-element",
|
|
16822
17099
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17106,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17106
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17107
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17108
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17109
|
+
message: MESSAGE$29
|
|
16833
17110
|
});
|
|
16834
17111
|
return;
|
|
16835
17112
|
}
|
|
@@ -16842,7 +17119,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17119
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17120
|
context.report({
|
|
16844
17121
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17122
|
+
message: MESSAGE$29
|
|
16846
17123
|
});
|
|
16847
17124
|
}
|
|
16848
17125
|
} })
|
|
@@ -16891,7 +17168,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17168
|
};
|
|
16892
17169
|
//#endregion
|
|
16893
17170
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17171
|
+
const MESSAGE$28 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17172
|
const CONTEXT_MODULES = [
|
|
16896
17173
|
"react",
|
|
16897
17174
|
"use-context-selector",
|
|
@@ -16927,7 +17204,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17204
|
if (!componentOrHookName) return;
|
|
16928
17205
|
context.report({
|
|
16929
17206
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17207
|
+
message: `${MESSAGE$28} (called inside "${componentOrHookName}")`
|
|
17208
|
+
});
|
|
17209
|
+
} })
|
|
17210
|
+
});
|
|
17211
|
+
//#endregion
|
|
17212
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17213
|
+
const MESSAGE$27 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17214
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17215
|
+
id: "no-create-ref-in-function-component",
|
|
17216
|
+
title: "createRef in function component",
|
|
17217
|
+
severity: "warn",
|
|
17218
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17219
|
+
create: (context) => ({ CallExpression(node) {
|
|
17220
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17221
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17222
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17223
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17224
|
+
}
|
|
17225
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17226
|
+
if (!enclosingFunction) return;
|
|
17227
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17228
|
+
if (!displayName) return;
|
|
17229
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17230
|
+
context.report({
|
|
17231
|
+
node,
|
|
17232
|
+
message: MESSAGE$27
|
|
16931
17233
|
});
|
|
16932
17234
|
} })
|
|
16933
17235
|
});
|
|
@@ -17067,7 +17369,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17369
|
});
|
|
17068
17370
|
//#endregion
|
|
17069
17371
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17372
|
+
const MESSAGE$26 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17373
|
const noDanger = defineRule({
|
|
17072
17374
|
id: "no-danger",
|
|
17073
17375
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17080,7 +17382,7 @@ const noDanger = defineRule({
|
|
|
17080
17382
|
if (!propAttribute) return;
|
|
17081
17383
|
context.report({
|
|
17082
17384
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17385
|
+
message: MESSAGE$26
|
|
17084
17386
|
});
|
|
17085
17387
|
},
|
|
17086
17388
|
CallExpression(node) {
|
|
@@ -17092,7 +17394,7 @@ const noDanger = defineRule({
|
|
|
17092
17394
|
const propertyKey = property.key;
|
|
17093
17395
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17396
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17397
|
+
message: MESSAGE$26
|
|
17096
17398
|
});
|
|
17097
17399
|
}
|
|
17098
17400
|
}
|
|
@@ -17100,7 +17402,7 @@ const noDanger = defineRule({
|
|
|
17100
17402
|
});
|
|
17101
17403
|
//#endregion
|
|
17102
17404
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17405
|
+
const MESSAGE$25 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17406
|
const isLineBreak = (child) => {
|
|
17105
17407
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17408
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17472,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17472
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17473
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17474
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17475
|
+
message: MESSAGE$25
|
|
17174
17476
|
});
|
|
17175
17477
|
},
|
|
17176
17478
|
CallExpression(node) {
|
|
@@ -17182,7 +17484,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17484
|
if (!propsShape.hasDangerously) return;
|
|
17183
17485
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17486
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17487
|
+
message: MESSAGE$25
|
|
17186
17488
|
});
|
|
17187
17489
|
}
|
|
17188
17490
|
})
|
|
@@ -17759,7 +18061,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
18061
|
//#endregion
|
|
17760
18062
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
18063
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
18064
|
+
const MESSAGE$24 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
18065
|
const resolveSettings$20 = (settings) => {
|
|
17764
18066
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
18067
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18080,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18080
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18081
|
context.report({
|
|
17780
18082
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18083
|
+
message: MESSAGE$24
|
|
17782
18084
|
});
|
|
17783
18085
|
} };
|
|
17784
18086
|
}
|
|
@@ -17786,7 +18088,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18088
|
//#endregion
|
|
17787
18089
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18090
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18091
|
+
const MESSAGE$23 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18092
|
const resolveSettings$19 = (settings) => {
|
|
17791
18093
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18094
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18107,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18107
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18108
|
context.report({
|
|
17807
18109
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18110
|
+
message: MESSAGE$23
|
|
17809
18111
|
});
|
|
17810
18112
|
} };
|
|
17811
18113
|
}
|
|
@@ -17828,7 +18130,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18130
|
};
|
|
17829
18131
|
//#endregion
|
|
17830
18132
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18133
|
+
const MESSAGE$22 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18134
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18135
|
let isConstructor = false;
|
|
17834
18136
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18152,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18152
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18153
|
context.report({
|
|
17852
18154
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18155
|
+
message: MESSAGE$22
|
|
17854
18156
|
});
|
|
17855
18157
|
};
|
|
17856
18158
|
const noDirectMutationState = defineRule({
|
|
@@ -19438,7 +19740,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19740
|
"ReactDOM",
|
|
19439
19741
|
"ReactDom"
|
|
19440
19742
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19743
|
+
const MESSAGE$21 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19744
|
const noFindDomNode = defineRule({
|
|
19443
19745
|
id: "no-find-dom-node",
|
|
19444
19746
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19751,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19751
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19752
|
context.report({
|
|
19451
19753
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19754
|
+
message: MESSAGE$21
|
|
19453
19755
|
});
|
|
19454
19756
|
return;
|
|
19455
19757
|
}
|
|
@@ -19460,7 +19762,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19762
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19763
|
context.report({
|
|
19462
19764
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19765
|
+
message: MESSAGE$21
|
|
19464
19766
|
});
|
|
19465
19767
|
}
|
|
19466
19768
|
} })
|
|
@@ -19523,64 +19825,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
19825
|
} })
|
|
19524
19826
|
});
|
|
19525
19827
|
//#endregion
|
|
19526
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19527
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19528
|
-
"FunctionDeclaration",
|
|
19529
|
-
"FunctionExpression",
|
|
19530
|
-
"ArrowFunctionExpression",
|
|
19531
|
-
"ClassDeclaration",
|
|
19532
|
-
"ClassExpression"
|
|
19533
|
-
]);
|
|
19534
|
-
const isReactImport$1 = (symbol) => {
|
|
19535
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19536
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19537
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19538
|
-
return importDeclaration.source.value === "react";
|
|
19539
|
-
};
|
|
19540
|
-
const getImportedName = (symbol) => {
|
|
19541
|
-
if (symbol.kind !== "import") return null;
|
|
19542
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19543
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19544
|
-
};
|
|
19545
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19546
|
-
if (symbol.kind !== "import") return false;
|
|
19547
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19548
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19549
|
-
};
|
|
19550
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19551
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19552
|
-
const symbol = scopes.symbolFor(callee);
|
|
19553
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19554
|
-
};
|
|
19555
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19556
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19557
|
-
if (callee.computed) return false;
|
|
19558
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19559
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19560
|
-
if (callee.property.name !== "createElement") return false;
|
|
19561
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19562
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19563
|
-
};
|
|
19564
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19565
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19566
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19567
|
-
};
|
|
19568
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19569
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19570
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19571
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19572
|
-
const nodeRecord = node;
|
|
19573
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19574
|
-
if (key === "parent") continue;
|
|
19575
|
-
const child = nodeRecord[key];
|
|
19576
|
-
if (Array.isArray(child)) {
|
|
19577
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19578
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19579
|
-
}
|
|
19580
|
-
return false;
|
|
19581
|
-
};
|
|
19582
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19583
|
-
//#endregion
|
|
19584
19828
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
19829
|
const noGiantComponent = defineRule({
|
|
19586
19830
|
id: "no-giant-component",
|
|
@@ -19759,6 +20003,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
20003
|
} })
|
|
19760
20004
|
});
|
|
19761
20005
|
//#endregion
|
|
20006
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
20007
|
+
const MESSAGE$20 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
20008
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20009
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20010
|
+
title: "Lazy image with high fetchPriority",
|
|
20011
|
+
severity: "warn",
|
|
20012
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
20013
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20014
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20015
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20016
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20017
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20018
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20019
|
+
context.report({
|
|
20020
|
+
node: node.name,
|
|
20021
|
+
message: MESSAGE$20
|
|
20022
|
+
});
|
|
20023
|
+
} })
|
|
20024
|
+
});
|
|
20025
|
+
//#endregion
|
|
19762
20026
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
20027
|
const noInitializeState = defineRule({
|
|
19764
20028
|
id: "no-initialize-state",
|
|
@@ -19988,6 +20252,29 @@ const noIsMounted = defineRule({
|
|
|
19988
20252
|
} })
|
|
19989
20253
|
});
|
|
19990
20254
|
//#endregion
|
|
20255
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20256
|
+
const MESSAGE$19 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20257
|
+
const isJsonMethodCall = (node, method) => {
|
|
20258
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20259
|
+
const callee = node.callee;
|
|
20260
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20261
|
+
};
|
|
20262
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20263
|
+
id: "no-json-parse-stringify-clone",
|
|
20264
|
+
title: "JSON parse/stringify deep clone",
|
|
20265
|
+
severity: "warn",
|
|
20266
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20267
|
+
create: (context) => ({ CallExpression(node) {
|
|
20268
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20269
|
+
const firstArgument = node.arguments?.[0];
|
|
20270
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20271
|
+
context.report({
|
|
20272
|
+
node,
|
|
20273
|
+
message: MESSAGE$19
|
|
20274
|
+
});
|
|
20275
|
+
} })
|
|
20276
|
+
});
|
|
20277
|
+
//#endregion
|
|
19991
20278
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
20279
|
const MESSAGE$18 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
19993
20280
|
const isJsxElementTypeReference = (node) => {
|
|
@@ -20310,9 +20597,6 @@ const noLongTransitionDuration = defineRule({
|
|
|
20310
20597
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20598
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20599
|
//#endregion
|
|
20313
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20314
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20315
|
-
//#endregion
|
|
20316
20600
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20601
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20602
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -22730,11 +23014,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22730
23014
|
return "unknown";
|
|
22731
23015
|
};
|
|
22732
23016
|
//#endregion
|
|
22733
|
-
//#region src/plugin/utils/
|
|
22734
|
-
const
|
|
22735
|
-
|
|
23017
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23018
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23019
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23020
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23021
|
+
if (!words) return [];
|
|
23022
|
+
return words.map((word) => word.toLowerCase());
|
|
22736
23023
|
};
|
|
22737
23024
|
//#endregion
|
|
23025
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23026
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23027
|
+
//#endregion
|
|
22738
23028
|
//#region src/plugin/constants/tanstack.ts
|
|
22739
23029
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22740
23030
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -34681,6 +34971,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34681
34971
|
}
|
|
34682
34972
|
});
|
|
34683
34973
|
//#endregion
|
|
34974
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
34975
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
34976
|
+
"signed",
|
|
34977
|
+
"logged",
|
|
34978
|
+
"sign"
|
|
34979
|
+
]);
|
|
34980
|
+
const mergeSignedInTokens = (tokens) => {
|
|
34981
|
+
const mergedTokens = [];
|
|
34982
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
34983
|
+
const currentToken = tokens[tokenIndex];
|
|
34984
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
34985
|
+
mergedTokens.push(`${currentToken}in`);
|
|
34986
|
+
tokenIndex += 1;
|
|
34987
|
+
continue;
|
|
34988
|
+
}
|
|
34989
|
+
mergedTokens.push(currentToken);
|
|
34990
|
+
}
|
|
34991
|
+
return mergedTokens;
|
|
34992
|
+
};
|
|
34993
|
+
const isAuthGuardName = (calleeName) => {
|
|
34994
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
34995
|
+
if (tokens.length === 0) return false;
|
|
34996
|
+
let hasAssertiveVerb = false;
|
|
34997
|
+
let hasGetterVerb = false;
|
|
34998
|
+
let hasQualifier = false;
|
|
34999
|
+
let hasStrongNoun = false;
|
|
35000
|
+
let hasWeakNoun = false;
|
|
35001
|
+
for (const token of tokens) {
|
|
35002
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35003
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35004
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35005
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35006
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35007
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35008
|
+
}
|
|
35009
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35010
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35011
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35012
|
+
return false;
|
|
35013
|
+
};
|
|
35014
|
+
//#endregion
|
|
34684
35015
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34685
35016
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34686
35017
|
if (!node) return false;
|
|
@@ -34723,9 +35054,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34723
35054
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34724
35055
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34725
35056
|
if (!calleeNode) return null;
|
|
34726
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35057
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35058
|
+
const calleeName = calleeNode.name;
|
|
35059
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35060
|
+
}
|
|
34727
35061
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34728
35062
|
const methodName = calleeNode.property.name;
|
|
35063
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34729
35064
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34730
35065
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34731
35066
|
return methodName;
|
|
@@ -37144,6 +37479,17 @@ const reactDoctorRules = [
|
|
|
37144
37479
|
category: "Performance"
|
|
37145
37480
|
}
|
|
37146
37481
|
},
|
|
37482
|
+
{
|
|
37483
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
37484
|
+
id: "auth-token-in-web-storage",
|
|
37485
|
+
source: "react-doctor",
|
|
37486
|
+
originallyExternal: false,
|
|
37487
|
+
rule: {
|
|
37488
|
+
...authTokenInWebStorage,
|
|
37489
|
+
framework: "global",
|
|
37490
|
+
category: "Security"
|
|
37491
|
+
}
|
|
37492
|
+
},
|
|
37147
37493
|
{
|
|
37148
37494
|
key: "react-doctor/autocomplete-valid",
|
|
37149
37495
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +37706,18 @@ const reactDoctorRules = [
|
|
|
37360
37706
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
37707
|
}
|
|
37362
37708
|
},
|
|
37709
|
+
{
|
|
37710
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
37711
|
+
id: "dialog-has-accessible-name",
|
|
37712
|
+
source: "react-doctor",
|
|
37713
|
+
originallyExternal: false,
|
|
37714
|
+
rule: {
|
|
37715
|
+
...dialogHasAccessibleName,
|
|
37716
|
+
framework: "global",
|
|
37717
|
+
category: "Accessibility",
|
|
37718
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
37719
|
+
}
|
|
37720
|
+
},
|
|
37363
37721
|
{
|
|
37364
37722
|
key: "react-doctor/display-name",
|
|
37365
37723
|
id: "display-name",
|
|
@@ -38519,6 +38877,18 @@ const reactDoctorRules = [
|
|
|
38519
38877
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
38878
|
}
|
|
38521
38879
|
},
|
|
38880
|
+
{
|
|
38881
|
+
key: "react-doctor/no-async-effect-callback",
|
|
38882
|
+
id: "no-async-effect-callback",
|
|
38883
|
+
source: "react-doctor",
|
|
38884
|
+
originallyExternal: false,
|
|
38885
|
+
rule: {
|
|
38886
|
+
...noAsyncEffectCallback,
|
|
38887
|
+
framework: "global",
|
|
38888
|
+
category: "Bugs",
|
|
38889
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
38890
|
+
}
|
|
38891
|
+
},
|
|
38522
38892
|
{
|
|
38523
38893
|
key: "react-doctor/no-autofocus",
|
|
38524
38894
|
id: "no-autofocus",
|
|
@@ -38542,6 +38912,18 @@ const reactDoctorRules = [
|
|
|
38542
38912
|
category: "Performance"
|
|
38543
38913
|
}
|
|
38544
38914
|
},
|
|
38915
|
+
{
|
|
38916
|
+
key: "react-doctor/no-call-component-as-function",
|
|
38917
|
+
id: "no-call-component-as-function",
|
|
38918
|
+
source: "react-doctor",
|
|
38919
|
+
originallyExternal: false,
|
|
38920
|
+
rule: {
|
|
38921
|
+
...noCallComponentAsFunction,
|
|
38922
|
+
framework: "global",
|
|
38923
|
+
category: "Bugs",
|
|
38924
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
38925
|
+
}
|
|
38926
|
+
},
|
|
38545
38927
|
{
|
|
38546
38928
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
38929
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +38984,18 @@ const reactDoctorRules = [
|
|
|
38602
38984
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
38985
|
}
|
|
38604
38986
|
},
|
|
38987
|
+
{
|
|
38988
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
38989
|
+
id: "no-create-ref-in-function-component",
|
|
38990
|
+
source: "react-doctor",
|
|
38991
|
+
originallyExternal: false,
|
|
38992
|
+
rule: {
|
|
38993
|
+
...noCreateRefInFunctionComponent,
|
|
38994
|
+
framework: "global",
|
|
38995
|
+
category: "Bugs",
|
|
38996
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
38997
|
+
}
|
|
38998
|
+
},
|
|
38605
38999
|
{
|
|
38606
39000
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
39001
|
id: "no-create-store-in-render",
|
|
@@ -38976,6 +39370,18 @@ const reactDoctorRules = [
|
|
|
38976
39370
|
category: "Accessibility"
|
|
38977
39371
|
}
|
|
38978
39372
|
},
|
|
39373
|
+
{
|
|
39374
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
39375
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
39376
|
+
source: "react-doctor",
|
|
39377
|
+
originallyExternal: false,
|
|
39378
|
+
rule: {
|
|
39379
|
+
...noImgLazyWithHighFetchpriority,
|
|
39380
|
+
framework: "global",
|
|
39381
|
+
category: "Performance",
|
|
39382
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
39383
|
+
}
|
|
39384
|
+
},
|
|
38979
39385
|
{
|
|
38980
39386
|
key: "react-doctor/no-initialize-state",
|
|
38981
39387
|
id: "no-initialize-state",
|
|
@@ -39046,6 +39452,17 @@ const reactDoctorRules = [
|
|
|
39046
39452
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
39453
|
}
|
|
39048
39454
|
},
|
|
39455
|
+
{
|
|
39456
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
39457
|
+
id: "no-json-parse-stringify-clone",
|
|
39458
|
+
source: "react-doctor",
|
|
39459
|
+
originallyExternal: false,
|
|
39460
|
+
rule: {
|
|
39461
|
+
...noJsonParseStringifyClone,
|
|
39462
|
+
framework: "global",
|
|
39463
|
+
category: "Performance"
|
|
39464
|
+
}
|
|
39465
|
+
},
|
|
39049
39466
|
{
|
|
39050
39467
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
39468
|
id: "no-jsx-element-type",
|