oxlint-plugin-react-doctor 0.5.6-dev.8908f98 → 0.5.6-dev.eafac9d
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 +635 -5
- package/dist/index.js +1085 -160
- 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$64 = "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$64
|
|
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$63 = "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$63
|
|
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$63
|
|
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 = [
|
|
@@ -4202,7 +4272,7 @@ const asyncParallel = defineRule({
|
|
|
4202
4272
|
});
|
|
4203
4273
|
//#endregion
|
|
4204
4274
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4205
|
-
const MESSAGE$
|
|
4275
|
+
const MESSAGE$62 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
4206
4276
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4207
4277
|
const STORAGE_GLOBALS = new Set([
|
|
4208
4278
|
"window",
|
|
@@ -4236,7 +4306,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4236
4306
|
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4237
4307
|
context.report({
|
|
4238
4308
|
node,
|
|
4239
|
-
message: MESSAGE$
|
|
4309
|
+
message: MESSAGE$62
|
|
4240
4310
|
});
|
|
4241
4311
|
},
|
|
4242
4312
|
AssignmentExpression(node) {
|
|
@@ -4247,7 +4317,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4247
4317
|
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4248
4318
|
context.report({
|
|
4249
4319
|
node: target,
|
|
4250
|
-
message: MESSAGE$
|
|
4320
|
+
message: MESSAGE$62
|
|
4251
4321
|
});
|
|
4252
4322
|
}
|
|
4253
4323
|
})
|
|
@@ -4624,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4624
4694
|
//#endregion
|
|
4625
4695
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4626
4696
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4627
|
-
const MESSAGE$
|
|
4697
|
+
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4628
4698
|
const KEY_HANDLERS = [
|
|
4629
4699
|
"onKeyUp",
|
|
4630
4700
|
"onKeyDown",
|
|
@@ -4656,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4656
4726
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4657
4727
|
context.report({
|
|
4658
4728
|
node: node.name,
|
|
4659
|
-
message: MESSAGE$
|
|
4729
|
+
message: MESSAGE$61
|
|
4660
4730
|
});
|
|
4661
4731
|
} };
|
|
4662
4732
|
}
|
|
@@ -4771,7 +4841,7 @@ const isReactComponentName = (name) => {
|
|
|
4771
4841
|
};
|
|
4772
4842
|
//#endregion
|
|
4773
4843
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4774
|
-
const MESSAGE$
|
|
4844
|
+
const MESSAGE$60 = "Blind users can't tell what this control does because screen readers find no label, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
4775
4845
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4776
4846
|
const DEFAULT_LABELLING_PROPS = [
|
|
4777
4847
|
"alt",
|
|
@@ -4932,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4932
5002
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4933
5003
|
context.report({
|
|
4934
5004
|
node: opening,
|
|
4935
|
-
message: MESSAGE$
|
|
5005
|
+
message: MESSAGE$60
|
|
4936
5006
|
});
|
|
4937
5007
|
} };
|
|
4938
5008
|
}
|
|
@@ -5061,6 +5131,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
5061
5131
|
return findings;
|
|
5062
5132
|
}
|
|
5063
5133
|
});
|
|
5134
|
+
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5064
5135
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5065
5136
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5066
5137
|
"continue",
|
|
@@ -5359,10 +5430,10 @@ const noVagueButtonLabel = defineRule({
|
|
|
5359
5430
|
});
|
|
5360
5431
|
//#endregion
|
|
5361
5432
|
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5362
|
-
const hasJsxSpreadAttribute
|
|
5433
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5363
5434
|
//#endregion
|
|
5364
5435
|
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
5365
|
-
const MESSAGE$
|
|
5436
|
+
const MESSAGE$59 = "This dialog has no accessible name, so screen readers announce it as just “dialog.” Add `aria-label` or point `aria-labelledby` at its heading.";
|
|
5366
5437
|
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5367
5438
|
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5368
5439
|
"aria-label",
|
|
@@ -5381,11 +5452,11 @@ const dialogHasAccessibleName = defineRule({
|
|
|
5381
5452
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5382
5453
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5383
5454
|
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5384
|
-
if (hasJsxSpreadAttribute
|
|
5455
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5385
5456
|
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5386
5457
|
context.report({
|
|
5387
5458
|
node: node.name,
|
|
5388
|
-
message: MESSAGE$
|
|
5459
|
+
message: MESSAGE$59
|
|
5389
5460
|
});
|
|
5390
5461
|
} })
|
|
5391
5462
|
});
|
|
@@ -5424,7 +5495,7 @@ const isEs6Component = (node) => {
|
|
|
5424
5495
|
};
|
|
5425
5496
|
//#endregion
|
|
5426
5497
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5427
|
-
const MESSAGE$
|
|
5498
|
+
const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5428
5499
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5429
5500
|
"observer",
|
|
5430
5501
|
"lazy",
|
|
@@ -5627,7 +5698,7 @@ const displayName = defineRule({
|
|
|
5627
5698
|
const reportAt = (node) => {
|
|
5628
5699
|
context.report({
|
|
5629
5700
|
node,
|
|
5630
|
-
message: MESSAGE$
|
|
5701
|
+
message: MESSAGE$58
|
|
5631
5702
|
});
|
|
5632
5703
|
};
|
|
5633
5704
|
return {
|
|
@@ -7775,7 +7846,7 @@ const forbidElements = defineRule({
|
|
|
7775
7846
|
});
|
|
7776
7847
|
//#endregion
|
|
7777
7848
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7778
|
-
const MESSAGE$
|
|
7849
|
+
const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7779
7850
|
const forwardRefUsesRef = defineRule({
|
|
7780
7851
|
id: "forward-ref-uses-ref",
|
|
7781
7852
|
title: "forwardRef without ref parameter",
|
|
@@ -7795,7 +7866,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7795
7866
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7796
7867
|
context.report({
|
|
7797
7868
|
node: inner,
|
|
7798
|
-
message: MESSAGE$
|
|
7869
|
+
message: MESSAGE$57
|
|
7799
7870
|
});
|
|
7800
7871
|
} })
|
|
7801
7872
|
});
|
|
@@ -7832,7 +7903,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7832
7903
|
});
|
|
7833
7904
|
//#endregion
|
|
7834
7905
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7835
|
-
const MESSAGE$
|
|
7906
|
+
const MESSAGE$56 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7836
7907
|
const DEFAULT_HEADING_TAGS = [
|
|
7837
7908
|
"h1",
|
|
7838
7909
|
"h2",
|
|
@@ -7865,7 +7936,7 @@ const headingHasContent = defineRule({
|
|
|
7865
7936
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7866
7937
|
context.report({
|
|
7867
7938
|
node,
|
|
7868
|
-
message: MESSAGE$
|
|
7939
|
+
message: MESSAGE$56
|
|
7869
7940
|
});
|
|
7870
7941
|
} };
|
|
7871
7942
|
}
|
|
@@ -8003,7 +8074,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
8003
8074
|
});
|
|
8004
8075
|
//#endregion
|
|
8005
8076
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
8006
|
-
const MESSAGE$
|
|
8077
|
+
const MESSAGE$55 = "Screen readers may mispronounce this page because it doesn't declare a language, so add a `lang` attribute like `en`.";
|
|
8007
8078
|
const resolveSettings$38 = (settings) => {
|
|
8008
8079
|
const reactDoctor = settings?.["react-doctor"];
|
|
8009
8080
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -8051,7 +8122,7 @@ const htmlHasLang = defineRule({
|
|
|
8051
8122
|
if (!lang) {
|
|
8052
8123
|
context.report({
|
|
8053
8124
|
node: node.name,
|
|
8054
|
-
message: MESSAGE$
|
|
8125
|
+
message: MESSAGE$55
|
|
8055
8126
|
});
|
|
8056
8127
|
return;
|
|
8057
8128
|
}
|
|
@@ -8059,13 +8130,13 @@ const htmlHasLang = defineRule({
|
|
|
8059
8130
|
if (verdict === "missing" || verdict === "empty") {
|
|
8060
8131
|
context.report({
|
|
8061
8132
|
node: lang,
|
|
8062
|
-
message: MESSAGE$
|
|
8133
|
+
message: MESSAGE$55
|
|
8063
8134
|
});
|
|
8064
8135
|
return;
|
|
8065
8136
|
}
|
|
8066
8137
|
if (hasSpread && !lang) context.report({
|
|
8067
8138
|
node: node.name,
|
|
8068
|
-
message: MESSAGE$
|
|
8139
|
+
message: MESSAGE$55
|
|
8069
8140
|
});
|
|
8070
8141
|
} };
|
|
8071
8142
|
}
|
|
@@ -8279,7 +8350,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8279
8350
|
});
|
|
8280
8351
|
//#endregion
|
|
8281
8352
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8282
|
-
const MESSAGE$
|
|
8353
|
+
const MESSAGE$54 = "Screen reader users cannot identify this `<iframe>` because it has no title. Add a `title` that describes its content.";
|
|
8283
8354
|
const evaluateTitleValue = (value) => {
|
|
8284
8355
|
if (!value) return "missing";
|
|
8285
8356
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8319,14 +8390,14 @@ const iframeHasTitle = defineRule({
|
|
|
8319
8390
|
if (!titleAttr) {
|
|
8320
8391
|
if (hasSpread || tag === "iframe") context.report({
|
|
8321
8392
|
node: node.name,
|
|
8322
|
-
message: MESSAGE$
|
|
8393
|
+
message: MESSAGE$54
|
|
8323
8394
|
});
|
|
8324
8395
|
return;
|
|
8325
8396
|
}
|
|
8326
8397
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8327
8398
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8328
8399
|
node: titleAttr,
|
|
8329
|
-
message: MESSAGE$
|
|
8400
|
+
message: MESSAGE$54
|
|
8330
8401
|
});
|
|
8331
8402
|
} })
|
|
8332
8403
|
});
|
|
@@ -8430,7 +8501,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8430
8501
|
});
|
|
8431
8502
|
//#endregion
|
|
8432
8503
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8433
|
-
const MESSAGE$
|
|
8504
|
+
const MESSAGE$53 = "Screen reader users hear \"image\" or \"photo\" twice because they already announce it, so describe what the image shows instead.";
|
|
8434
8505
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8435
8506
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8436
8507
|
"image",
|
|
@@ -8495,7 +8566,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8495
8566
|
if (!altAttribute) return;
|
|
8496
8567
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8497
8568
|
node: altAttribute,
|
|
8498
|
-
message: MESSAGE$
|
|
8569
|
+
message: MESSAGE$53
|
|
8499
8570
|
});
|
|
8500
8571
|
} };
|
|
8501
8572
|
}
|
|
@@ -10852,7 +10923,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10852
10923
|
});
|
|
10853
10924
|
//#endregion
|
|
10854
10925
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10855
|
-
const MESSAGE$
|
|
10926
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10856
10927
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10857
10928
|
"code",
|
|
10858
10929
|
"pre",
|
|
@@ -10888,7 +10959,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10888
10959
|
if (isInsideLiteralTextTag(node)) return;
|
|
10889
10960
|
context.report({
|
|
10890
10961
|
node,
|
|
10891
|
-
message: MESSAGE$
|
|
10962
|
+
message: MESSAGE$52
|
|
10892
10963
|
});
|
|
10893
10964
|
} })
|
|
10894
10965
|
});
|
|
@@ -10919,7 +10990,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10919
10990
|
};
|
|
10920
10991
|
//#endregion
|
|
10921
10992
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10922
|
-
const MESSAGE$
|
|
10993
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10923
10994
|
const CONTEXT_MODULES$1 = [
|
|
10924
10995
|
"react",
|
|
10925
10996
|
"use-context-selector",
|
|
@@ -11017,7 +11088,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
11017
11088
|
if (!isConstructedValue(innerExpression)) continue;
|
|
11018
11089
|
context.report({
|
|
11019
11090
|
node: attribute,
|
|
11020
|
-
message: MESSAGE$
|
|
11091
|
+
message: MESSAGE$51
|
|
11021
11092
|
});
|
|
11022
11093
|
}
|
|
11023
11094
|
}
|
|
@@ -11103,7 +11174,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11103
11174
|
};
|
|
11104
11175
|
//#endregion
|
|
11105
11176
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11106
|
-
const MESSAGE$
|
|
11177
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11107
11178
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11108
11179
|
"icon",
|
|
11109
11180
|
"Icon",
|
|
@@ -11372,7 +11443,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11372
11443
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11373
11444
|
context.report({
|
|
11374
11445
|
node,
|
|
11375
|
-
message: MESSAGE$
|
|
11446
|
+
message: MESSAGE$50
|
|
11376
11447
|
});
|
|
11377
11448
|
}
|
|
11378
11449
|
};
|
|
@@ -11660,7 +11731,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11660
11731
|
];
|
|
11661
11732
|
//#endregion
|
|
11662
11733
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11663
|
-
const MESSAGE$
|
|
11734
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11664
11735
|
const isDataArrayPropName = (propName) => {
|
|
11665
11736
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11666
11737
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11744,7 +11815,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11744
11815
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11745
11816
|
context.report({
|
|
11746
11817
|
node,
|
|
11747
|
-
message: MESSAGE$
|
|
11818
|
+
message: MESSAGE$49
|
|
11748
11819
|
});
|
|
11749
11820
|
}
|
|
11750
11821
|
};
|
|
@@ -12002,7 +12073,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
12002
12073
|
]);
|
|
12003
12074
|
//#endregion
|
|
12004
12075
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
12005
|
-
const MESSAGE$
|
|
12076
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
12006
12077
|
const isAccessorPredicateName = (propName) => {
|
|
12007
12078
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
12008
12079
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12208,7 +12279,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12208
12279
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12209
12280
|
context.report({
|
|
12210
12281
|
node,
|
|
12211
|
-
message: MESSAGE$
|
|
12282
|
+
message: MESSAGE$48
|
|
12212
12283
|
});
|
|
12213
12284
|
}
|
|
12214
12285
|
};
|
|
@@ -12428,7 +12499,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12428
12499
|
];
|
|
12429
12500
|
//#endregion
|
|
12430
12501
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12431
|
-
const MESSAGE$
|
|
12502
|
+
const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12432
12503
|
const isConfigObjectPropName = (propName) => {
|
|
12433
12504
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12434
12505
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12516,7 +12587,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12516
12587
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12517
12588
|
context.report({
|
|
12518
12589
|
node,
|
|
12519
|
-
message: MESSAGE$
|
|
12590
|
+
message: MESSAGE$47
|
|
12520
12591
|
});
|
|
12521
12592
|
}
|
|
12522
12593
|
};
|
|
@@ -12524,7 +12595,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12524
12595
|
});
|
|
12525
12596
|
//#endregion
|
|
12526
12597
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12527
|
-
const MESSAGE$
|
|
12598
|
+
const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12528
12599
|
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;
|
|
12529
12600
|
const resolveSettings$28 = (settings) => {
|
|
12530
12601
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12565,7 +12636,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12565
12636
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12566
12637
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12567
12638
|
node: attribute,
|
|
12568
|
-
message: MESSAGE$
|
|
12639
|
+
message: MESSAGE$46
|
|
12569
12640
|
});
|
|
12570
12641
|
}
|
|
12571
12642
|
} };
|
|
@@ -12880,7 +12951,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12880
12951
|
});
|
|
12881
12952
|
//#endregion
|
|
12882
12953
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12883
|
-
const MESSAGE$
|
|
12954
|
+
const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
|
|
12884
12955
|
const resolveSettings$25 = (settings) => {
|
|
12885
12956
|
const reactDoctor = settings?.["react-doctor"];
|
|
12886
12957
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12921,7 +12992,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12921
12992
|
}
|
|
12922
12993
|
context.report({
|
|
12923
12994
|
node: attribute,
|
|
12924
|
-
message: MESSAGE$
|
|
12995
|
+
message: MESSAGE$45
|
|
12925
12996
|
});
|
|
12926
12997
|
}
|
|
12927
12998
|
} };
|
|
@@ -13149,7 +13220,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13149
13220
|
});
|
|
13150
13221
|
//#endregion
|
|
13151
13222
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13152
|
-
const MESSAGE$
|
|
13223
|
+
const MESSAGE$44 = "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`.";
|
|
13153
13224
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13154
13225
|
"aa",
|
|
13155
13226
|
"ab",
|
|
@@ -13361,7 +13432,7 @@ const lang = defineRule({
|
|
|
13361
13432
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13362
13433
|
context.report({
|
|
13363
13434
|
node: langAttr,
|
|
13364
|
-
message: MESSAGE$
|
|
13435
|
+
message: MESSAGE$44
|
|
13365
13436
|
});
|
|
13366
13437
|
return;
|
|
13367
13438
|
}
|
|
@@ -13370,7 +13441,7 @@ const lang = defineRule({
|
|
|
13370
13441
|
if (value === null) return;
|
|
13371
13442
|
if (!isValidLangTag(value)) context.report({
|
|
13372
13443
|
node: langAttr,
|
|
13373
|
-
message: MESSAGE$
|
|
13444
|
+
message: MESSAGE$44
|
|
13374
13445
|
});
|
|
13375
13446
|
} })
|
|
13376
13447
|
});
|
|
@@ -13414,7 +13485,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13414
13485
|
});
|
|
13415
13486
|
//#endregion
|
|
13416
13487
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13417
|
-
const MESSAGE$
|
|
13488
|
+
const MESSAGE$43 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
13418
13489
|
const DEFAULT_AUDIO = ["audio"];
|
|
13419
13490
|
const DEFAULT_VIDEO = ["video"];
|
|
13420
13491
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13455,7 +13526,7 @@ const mediaHasCaption = defineRule({
|
|
|
13455
13526
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13456
13527
|
context.report({
|
|
13457
13528
|
node: node.name,
|
|
13458
|
-
message: MESSAGE$
|
|
13529
|
+
message: MESSAGE$43
|
|
13459
13530
|
});
|
|
13460
13531
|
return;
|
|
13461
13532
|
}
|
|
@@ -13472,7 +13543,7 @@ const mediaHasCaption = defineRule({
|
|
|
13472
13543
|
return kindValue.value.toLowerCase() === "captions";
|
|
13473
13544
|
})) context.report({
|
|
13474
13545
|
node: node.name,
|
|
13475
|
-
message: MESSAGE$
|
|
13546
|
+
message: MESSAGE$43
|
|
13476
13547
|
});
|
|
13477
13548
|
} };
|
|
13478
13549
|
}
|
|
@@ -15273,7 +15344,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15273
15344
|
});
|
|
15274
15345
|
//#endregion
|
|
15275
15346
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15276
|
-
const MESSAGE$
|
|
15347
|
+
const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15277
15348
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15278
15349
|
const noAccessKey = defineRule({
|
|
15279
15350
|
id: "no-access-key",
|
|
@@ -15290,7 +15361,7 @@ const noAccessKey = defineRule({
|
|
|
15290
15361
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15291
15362
|
context.report({
|
|
15292
15363
|
node: accessKey,
|
|
15293
|
-
message: MESSAGE$
|
|
15364
|
+
message: MESSAGE$42
|
|
15294
15365
|
});
|
|
15295
15366
|
return;
|
|
15296
15367
|
}
|
|
@@ -15300,7 +15371,7 @@ const noAccessKey = defineRule({
|
|
|
15300
15371
|
if (isUndefinedIdentifier(expression)) return;
|
|
15301
15372
|
context.report({
|
|
15302
15373
|
node: accessKey,
|
|
15303
|
-
message: MESSAGE$
|
|
15374
|
+
message: MESSAGE$42
|
|
15304
15375
|
});
|
|
15305
15376
|
}
|
|
15306
15377
|
} })
|
|
@@ -15782,8 +15853,41 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15782
15853
|
} })
|
|
15783
15854
|
});
|
|
15784
15855
|
//#endregion
|
|
15856
|
+
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
15857
|
+
const getStringFromClassNameAttr = (node) => {
|
|
15858
|
+
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
15859
|
+
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
15860
|
+
if (!classAttr?.value) return null;
|
|
15861
|
+
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
15862
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
15863
|
+
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
15864
|
+
return null;
|
|
15865
|
+
};
|
|
15866
|
+
//#endregion
|
|
15867
|
+
//#region src/plugin/rules/design/no-arbitrary-px-font-size.ts
|
|
15868
|
+
const ARBITRARY_PX_FONT_SIZE = /(?:^|\s)(?:\w+:)*text-\[(\d+(?:\.\d+)?)px\]/g;
|
|
15869
|
+
const noArbitraryPxFontSize = defineRule({
|
|
15870
|
+
id: "no-arbitrary-px-font-size",
|
|
15871
|
+
title: "Pixel arbitrary font size",
|
|
15872
|
+
tags: ["design", "test-noise"],
|
|
15873
|
+
severity: "warn",
|
|
15874
|
+
category: "Accessibility",
|
|
15875
|
+
recommendation: "Use `rem` for arbitrary font sizes (`text-[0.8125rem]`, not `text-[13px]`) so text scales with the user's root font-size preference. Pixels stay fine for `border-*` / `outline-*`.",
|
|
15876
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
15877
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
15878
|
+
if (!classNameValue) return;
|
|
15879
|
+
for (const match of classNameValue.matchAll(ARBITRARY_PX_FONT_SIZE)) {
|
|
15880
|
+
const rem = parseFloat(match[1]) / 16;
|
|
15881
|
+
context.report({
|
|
15882
|
+
node,
|
|
15883
|
+
message: `\`text-[${match[1]}px]\` doesn't scale with the user's font-size preference — use rem, e.g. \`text-[${rem}rem]\`.`
|
|
15884
|
+
});
|
|
15885
|
+
}
|
|
15886
|
+
} })
|
|
15887
|
+
});
|
|
15888
|
+
//#endregion
|
|
15785
15889
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15786
|
-
const MESSAGE$
|
|
15890
|
+
const MESSAGE$41 = "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.";
|
|
15787
15891
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15788
15892
|
id: "no-aria-hidden-on-focusable",
|
|
15789
15893
|
title: "aria-hidden on focusable element",
|
|
@@ -15810,7 +15914,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15810
15914
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15811
15915
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15812
15916
|
node: ariaHidden,
|
|
15813
|
-
message: MESSAGE$
|
|
15917
|
+
message: MESSAGE$41
|
|
15814
15918
|
});
|
|
15815
15919
|
} })
|
|
15816
15920
|
});
|
|
@@ -16178,7 +16282,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16178
16282
|
});
|
|
16179
16283
|
//#endregion
|
|
16180
16284
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16181
|
-
const MESSAGE$
|
|
16285
|
+
const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16182
16286
|
const SECOND_INDEX_METHODS = new Set([
|
|
16183
16287
|
"every",
|
|
16184
16288
|
"filter",
|
|
@@ -16382,7 +16486,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16382
16486
|
}
|
|
16383
16487
|
context.report({
|
|
16384
16488
|
node: keyAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16489
|
+
message: MESSAGE$40
|
|
16386
16490
|
});
|
|
16387
16491
|
},
|
|
16388
16492
|
CallExpression(node) {
|
|
@@ -16402,7 +16506,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16402
16506
|
if (propName !== "key") continue;
|
|
16403
16507
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16404
16508
|
node: property,
|
|
16405
|
-
message: MESSAGE$
|
|
16509
|
+
message: MESSAGE$40
|
|
16406
16510
|
});
|
|
16407
16511
|
}
|
|
16408
16512
|
}
|
|
@@ -16410,7 +16514,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16410
16514
|
});
|
|
16411
16515
|
//#endregion
|
|
16412
16516
|
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
16413
|
-
const MESSAGE$
|
|
16517
|
+
const MESSAGE$39 = "The `useEffect` callback is `async`, so it returns a Promise instead of a cleanup function. React calls that Promise as cleanup (a no-op) and the effect can race on unmount. Put the async work in an inner function and call it.";
|
|
16414
16518
|
const noAsyncEffectCallback = defineRule({
|
|
16415
16519
|
id: "no-async-effect-callback",
|
|
16416
16520
|
title: "Async effect callback",
|
|
@@ -16424,13 +16528,13 @@ const noAsyncEffectCallback = defineRule({
|
|
|
16424
16528
|
if (!callback.async) return;
|
|
16425
16529
|
context.report({
|
|
16426
16530
|
node: callback,
|
|
16427
|
-
message: MESSAGE$
|
|
16531
|
+
message: MESSAGE$39
|
|
16428
16532
|
});
|
|
16429
16533
|
} })
|
|
16430
16534
|
});
|
|
16431
16535
|
//#endregion
|
|
16432
16536
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16433
|
-
const MESSAGE$
|
|
16537
|
+
const MESSAGE$38 = "`autoFocus` moves focus on load, which can disrupt screen reader and keyboard users. Remove it and let users choose where to focus.";
|
|
16434
16538
|
const resolveSettings$21 = (settings) => {
|
|
16435
16539
|
const reactDoctor = settings?.["react-doctor"];
|
|
16436
16540
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16486,12 +16590,45 @@ const noAutofocus = defineRule({
|
|
|
16486
16590
|
}
|
|
16487
16591
|
context.report({
|
|
16488
16592
|
node: autoFocusAttribute,
|
|
16489
|
-
message: MESSAGE$
|
|
16593
|
+
message: MESSAGE$38
|
|
16490
16594
|
});
|
|
16491
16595
|
} };
|
|
16492
16596
|
}
|
|
16493
16597
|
});
|
|
16494
16598
|
//#endregion
|
|
16599
|
+
//#region src/plugin/rules/a11y/no-autoplay-without-muted.ts
|
|
16600
|
+
const MESSAGE$37 = "Autoplaying media with sound is hostile to your users (and browsers block it). Add `muted` (with `playsInline`) to the autoplaying `<video>` / `<audio>`, or drop `autoPlay`.";
|
|
16601
|
+
const resolveStaticBoolean = (attribute) => {
|
|
16602
|
+
const value = attribute.value;
|
|
16603
|
+
if (!value) return true;
|
|
16604
|
+
const literal = isNodeOfType(value, "JSXExpressionContainer") ? value.expression : value;
|
|
16605
|
+
if (isNodeOfType(literal, "Literal")) {
|
|
16606
|
+
if (literal.value === true || literal.value === "true") return true;
|
|
16607
|
+
if (literal.value === false || literal.value === "false") return false;
|
|
16608
|
+
}
|
|
16609
|
+
return null;
|
|
16610
|
+
};
|
|
16611
|
+
const noAutoplayWithoutMuted = defineRule({
|
|
16612
|
+
id: "no-autoplay-without-muted",
|
|
16613
|
+
title: "Autoplaying media without muted",
|
|
16614
|
+
severity: "warn",
|
|
16615
|
+
recommendation: "Always pair `autoPlay` with `muted` (and `playsInline`): `<video autoPlay muted loop playsInline />`. If the sound matters, drop `autoPlay` and let users start it.",
|
|
16616
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
16617
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
16618
|
+
const tagName = node.name.name;
|
|
16619
|
+
if (tagName !== "video" && tagName !== "audio") return;
|
|
16620
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
16621
|
+
const autoPlay = hasJsxPropIgnoreCase(node.attributes, "autoplay");
|
|
16622
|
+
if (!autoPlay || resolveStaticBoolean(autoPlay) !== true) return;
|
|
16623
|
+
const muted = hasJsxPropIgnoreCase(node.attributes, "muted");
|
|
16624
|
+
if (muted && resolveStaticBoolean(muted) !== false) return;
|
|
16625
|
+
context.report({
|
|
16626
|
+
node: node.name,
|
|
16627
|
+
message: MESSAGE$37
|
|
16628
|
+
});
|
|
16629
|
+
} })
|
|
16630
|
+
});
|
|
16631
|
+
//#endregion
|
|
16495
16632
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16496
16633
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16497
16634
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -16990,7 +17127,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16990
17127
|
});
|
|
16991
17128
|
//#endregion
|
|
16992
17129
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16993
|
-
const MESSAGE$
|
|
17130
|
+
const MESSAGE$36 = "A `children` prop can override or hide nested children, so the component may render different content than the JSX shows.";
|
|
16994
17131
|
const noChildrenProp = defineRule({
|
|
16995
17132
|
id: "no-children-prop",
|
|
16996
17133
|
title: "Children passed as a prop",
|
|
@@ -17002,7 +17139,7 @@ const noChildrenProp = defineRule({
|
|
|
17002
17139
|
if (node.name.name !== "children") return;
|
|
17003
17140
|
context.report({
|
|
17004
17141
|
node: node.name,
|
|
17005
|
-
message: MESSAGE$
|
|
17142
|
+
message: MESSAGE$36
|
|
17006
17143
|
});
|
|
17007
17144
|
},
|
|
17008
17145
|
CallExpression(node) {
|
|
@@ -17015,7 +17152,7 @@ const noChildrenProp = defineRule({
|
|
|
17015
17152
|
const propertyKey = property.key;
|
|
17016
17153
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
17017
17154
|
node: propertyKey,
|
|
17018
|
-
message: MESSAGE$
|
|
17155
|
+
message: MESSAGE$36
|
|
17019
17156
|
});
|
|
17020
17157
|
}
|
|
17021
17158
|
}
|
|
@@ -17023,7 +17160,7 @@ const noChildrenProp = defineRule({
|
|
|
17023
17160
|
});
|
|
17024
17161
|
//#endregion
|
|
17025
17162
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
17026
|
-
const MESSAGE$
|
|
17163
|
+
const MESSAGE$35 = "`React.cloneElement` couples the parent to the child's prop shape, so child prop changes can silently break injected behavior.";
|
|
17027
17164
|
const noCloneElement = defineRule({
|
|
17028
17165
|
id: "no-clone-element",
|
|
17029
17166
|
title: "cloneElement makes child props fragile",
|
|
@@ -17036,7 +17173,7 @@ const noCloneElement = defineRule({
|
|
|
17036
17173
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
17037
17174
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
17038
17175
|
node: callee,
|
|
17039
|
-
message: MESSAGE$
|
|
17176
|
+
message: MESSAGE$35
|
|
17040
17177
|
});
|
|
17041
17178
|
return;
|
|
17042
17179
|
}
|
|
@@ -17049,7 +17186,7 @@ const noCloneElement = defineRule({
|
|
|
17049
17186
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
17050
17187
|
context.report({
|
|
17051
17188
|
node: callee,
|
|
17052
|
-
message: MESSAGE$
|
|
17189
|
+
message: MESSAGE$35
|
|
17053
17190
|
});
|
|
17054
17191
|
}
|
|
17055
17192
|
} })
|
|
@@ -17098,7 +17235,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
17098
17235
|
};
|
|
17099
17236
|
//#endregion
|
|
17100
17237
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
17101
|
-
const MESSAGE$
|
|
17238
|
+
const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
17102
17239
|
const CONTEXT_MODULES = [
|
|
17103
17240
|
"react",
|
|
17104
17241
|
"use-context-selector",
|
|
@@ -17134,13 +17271,13 @@ const noCreateContextInRender = defineRule({
|
|
|
17134
17271
|
if (!componentOrHookName) return;
|
|
17135
17272
|
context.report({
|
|
17136
17273
|
node,
|
|
17137
|
-
message: `${MESSAGE$
|
|
17274
|
+
message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
|
|
17138
17275
|
});
|
|
17139
17276
|
} })
|
|
17140
17277
|
});
|
|
17141
17278
|
//#endregion
|
|
17142
17279
|
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
17143
|
-
const MESSAGE$
|
|
17280
|
+
const MESSAGE$33 = "`createRef()` in a function component allocates a brand-new ref on every render, so it never holds a value between renders. Use the `useRef()` hook instead.";
|
|
17144
17281
|
const noCreateRefInFunctionComponent = defineRule({
|
|
17145
17282
|
id: "no-create-ref-in-function-component",
|
|
17146
17283
|
title: "createRef in function component",
|
|
@@ -17159,7 +17296,7 @@ const noCreateRefInFunctionComponent = defineRule({
|
|
|
17159
17296
|
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17160
17297
|
context.report({
|
|
17161
17298
|
node,
|
|
17162
|
-
message: MESSAGE$
|
|
17299
|
+
message: MESSAGE$33
|
|
17163
17300
|
});
|
|
17164
17301
|
} })
|
|
17165
17302
|
});
|
|
@@ -17299,7 +17436,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17299
17436
|
});
|
|
17300
17437
|
//#endregion
|
|
17301
17438
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17302
|
-
const MESSAGE$
|
|
17439
|
+
const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17303
17440
|
const noDanger = defineRule({
|
|
17304
17441
|
id: "no-danger",
|
|
17305
17442
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17312,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17312
17449
|
if (!propAttribute) return;
|
|
17313
17450
|
context.report({
|
|
17314
17451
|
node: propAttribute.name,
|
|
17315
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$32
|
|
17316
17453
|
});
|
|
17317
17454
|
},
|
|
17318
17455
|
CallExpression(node) {
|
|
@@ -17324,7 +17461,7 @@ const noDanger = defineRule({
|
|
|
17324
17461
|
const propertyKey = property.key;
|
|
17325
17462
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17326
17463
|
node: propertyKey,
|
|
17327
|
-
message: MESSAGE$
|
|
17464
|
+
message: MESSAGE$32
|
|
17328
17465
|
});
|
|
17329
17466
|
}
|
|
17330
17467
|
}
|
|
@@ -17332,7 +17469,7 @@ const noDanger = defineRule({
|
|
|
17332
17469
|
});
|
|
17333
17470
|
//#endregion
|
|
17334
17471
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17335
|
-
const MESSAGE$
|
|
17472
|
+
const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17336
17473
|
const isLineBreak = (child) => {
|
|
17337
17474
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17338
17475
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17402,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17402
17539
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17403
17540
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17404
17541
|
node: opening,
|
|
17405
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$31
|
|
17406
17543
|
});
|
|
17407
17544
|
},
|
|
17408
17545
|
CallExpression(node) {
|
|
@@ -17414,7 +17551,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17414
17551
|
if (!propsShape.hasDangerously) return;
|
|
17415
17552
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17416
17553
|
node,
|
|
17417
|
-
message: MESSAGE$
|
|
17554
|
+
message: MESSAGE$31
|
|
17418
17555
|
});
|
|
17419
17556
|
}
|
|
17420
17557
|
})
|
|
@@ -17579,6 +17716,37 @@ const noDefaultProps = defineRule({
|
|
|
17579
17716
|
} })
|
|
17580
17717
|
});
|
|
17581
17718
|
//#endregion
|
|
17719
|
+
//#region src/plugin/utils/get-class-name-tokens.ts
|
|
17720
|
+
const getClassNameTokens = (classNameValue) => classNameValue.split(/\s+/).filter((token) => token.length > 0).map((token) => token.split(":").pop() ?? token);
|
|
17721
|
+
//#endregion
|
|
17722
|
+
//#region src/plugin/rules/design/no-deprecated-tailwind-class.ts
|
|
17723
|
+
const renameDeprecatedToken = (token) => {
|
|
17724
|
+
if (token === "overflow-ellipsis") return "text-ellipsis";
|
|
17725
|
+
if (token.startsWith("flex-shrink")) return token.replace("flex-shrink", "shrink");
|
|
17726
|
+
if (token.startsWith("flex-grow")) return token.replace("flex-grow", "grow");
|
|
17727
|
+
if (token.startsWith("bg-gradient-to-")) return token.replace("bg-gradient-to-", "bg-linear-to-");
|
|
17728
|
+
return null;
|
|
17729
|
+
};
|
|
17730
|
+
const noDeprecatedTailwindClass = defineRule({
|
|
17731
|
+
id: "no-deprecated-tailwind-class",
|
|
17732
|
+
title: "Deprecated Tailwind v4 utility",
|
|
17733
|
+
tags: ["design", "test-noise"],
|
|
17734
|
+
severity: "warn",
|
|
17735
|
+
requires: ["tailwind:4"],
|
|
17736
|
+
recommendation: "Tailwind v4 renamed these utilities: `bg-gradient-*` → `bg-linear-*`, `flex-shrink-*` → `shrink-*`, `flex-grow-*` → `grow-*`, `overflow-ellipsis` → `text-ellipsis`. Use the new names.",
|
|
17737
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
17738
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
17739
|
+
if (!classNameValue) return;
|
|
17740
|
+
for (const token of getClassNameTokens(classNameValue)) {
|
|
17741
|
+
const replacement = renameDeprecatedToken(token);
|
|
17742
|
+
if (replacement) context.report({
|
|
17743
|
+
node,
|
|
17744
|
+
message: `\`${token}\` was renamed in Tailwind v4 and no longer applies — use \`${replacement}\`.`
|
|
17745
|
+
});
|
|
17746
|
+
}
|
|
17747
|
+
} })
|
|
17748
|
+
});
|
|
17749
|
+
//#endregion
|
|
17582
17750
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17583
17751
|
const isInitialOnlyPropName = (propName) => {
|
|
17584
17752
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -17991,7 +18159,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17991
18159
|
//#endregion
|
|
17992
18160
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17993
18161
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17994
|
-
const MESSAGE$
|
|
18162
|
+
const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17995
18163
|
const resolveSettings$20 = (settings) => {
|
|
17996
18164
|
const reactDoctor = settings?.["react-doctor"];
|
|
17997
18165
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18010,7 +18178,7 @@ const noDidMountSetState = defineRule({
|
|
|
18010
18178
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18011
18179
|
context.report({
|
|
18012
18180
|
node: node.callee,
|
|
18013
|
-
message: MESSAGE$
|
|
18181
|
+
message: MESSAGE$30
|
|
18014
18182
|
});
|
|
18015
18183
|
} };
|
|
18016
18184
|
}
|
|
@@ -18018,7 +18186,7 @@ const noDidMountSetState = defineRule({
|
|
|
18018
18186
|
//#endregion
|
|
18019
18187
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
18020
18188
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
18021
|
-
const MESSAGE$
|
|
18189
|
+
const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
18022
18190
|
const resolveSettings$19 = (settings) => {
|
|
18023
18191
|
const reactDoctor = settings?.["react-doctor"];
|
|
18024
18192
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -18037,7 +18205,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
18037
18205
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
18038
18206
|
context.report({
|
|
18039
18207
|
node: node.callee,
|
|
18040
|
-
message: MESSAGE$
|
|
18208
|
+
message: MESSAGE$29
|
|
18041
18209
|
});
|
|
18042
18210
|
} };
|
|
18043
18211
|
}
|
|
@@ -18060,7 +18228,7 @@ const isStateMemberExpression = (node) => {
|
|
|
18060
18228
|
};
|
|
18061
18229
|
//#endregion
|
|
18062
18230
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
18063
|
-
const MESSAGE$
|
|
18231
|
+
const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
18064
18232
|
const shouldIgnoreMutation = (node) => {
|
|
18065
18233
|
let isConstructor = false;
|
|
18066
18234
|
let isInsideCallExpression = false;
|
|
@@ -18082,7 +18250,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
18082
18250
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
18083
18251
|
context.report({
|
|
18084
18252
|
node: reportNode,
|
|
18085
|
-
message: MESSAGE$
|
|
18253
|
+
message: MESSAGE$28
|
|
18086
18254
|
});
|
|
18087
18255
|
};
|
|
18088
18256
|
const noDirectMutationState = defineRule({
|
|
@@ -18292,6 +18460,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18292
18460
|
} })
|
|
18293
18461
|
});
|
|
18294
18462
|
//#endregion
|
|
18463
|
+
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
18464
|
+
const MESSAGE$27 = "`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.";
|
|
18465
|
+
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
18466
|
+
const noDocumentWrite = defineRule({
|
|
18467
|
+
id: "no-document-write",
|
|
18468
|
+
title: "document.write/writeln",
|
|
18469
|
+
severity: "warn",
|
|
18470
|
+
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
18471
|
+
create: (context) => ({ CallExpression(node) {
|
|
18472
|
+
const callee = node.callee;
|
|
18473
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
18474
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== "document") return;
|
|
18475
|
+
if (!isNodeOfType(callee.property, "Identifier") || !WRITE_METHODS.has(callee.property.name)) return;
|
|
18476
|
+
context.report({
|
|
18477
|
+
node,
|
|
18478
|
+
message: MESSAGE$27
|
|
18479
|
+
});
|
|
18480
|
+
} })
|
|
18481
|
+
});
|
|
18482
|
+
//#endregion
|
|
18295
18483
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18296
18484
|
const noDynamicImportPath = defineRule({
|
|
18297
18485
|
id: "no-dynamic-import-path",
|
|
@@ -19670,7 +19858,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19670
19858
|
"ReactDOM",
|
|
19671
19859
|
"ReactDom"
|
|
19672
19860
|
]);
|
|
19673
|
-
const MESSAGE$
|
|
19861
|
+
const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19674
19862
|
const noFindDomNode = defineRule({
|
|
19675
19863
|
id: "no-find-dom-node",
|
|
19676
19864
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19681,7 +19869,7 @@ const noFindDomNode = defineRule({
|
|
|
19681
19869
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19682
19870
|
context.report({
|
|
19683
19871
|
node: callee,
|
|
19684
|
-
message: MESSAGE$
|
|
19872
|
+
message: MESSAGE$26
|
|
19685
19873
|
});
|
|
19686
19874
|
return;
|
|
19687
19875
|
}
|
|
@@ -19692,7 +19880,7 @@ const noFindDomNode = defineRule({
|
|
|
19692
19880
|
if (callee.property.name !== "findDOMNode") return;
|
|
19693
19881
|
context.report({
|
|
19694
19882
|
node: callee.property,
|
|
19695
|
-
message: MESSAGE$
|
|
19883
|
+
message: MESSAGE$26
|
|
19696
19884
|
});
|
|
19697
19885
|
}
|
|
19698
19886
|
} })
|
|
@@ -19733,6 +19921,41 @@ const noFullLodashImport = defineRule({
|
|
|
19733
19921
|
} })
|
|
19734
19922
|
});
|
|
19735
19923
|
//#endregion
|
|
19924
|
+
//#region src/plugin/rules/design/no-full-viewport-width.ts
|
|
19925
|
+
const FULL_VIEWPORT_WIDTH_CLASS = /(?:^|\s)(?:min-)?w-(?:screen|\[100vw\])(?:$|\s)/;
|
|
19926
|
+
const WIDTH_KEYS = new Set(["width", "minWidth"]);
|
|
19927
|
+
const MESSAGE$25 = "`100vw` is wider than the viewport whenever a scrollbar is visible, so it triggers horizontal scroll on most desktops. Use `w-full` / `width: 100%` (with the parent's padding) for a full-bleed element.";
|
|
19928
|
+
const noFullViewportWidth = defineRule({
|
|
19929
|
+
id: "no-full-viewport-width",
|
|
19930
|
+
title: "Full viewport width causes overflow",
|
|
19931
|
+
tags: ["design", "test-noise"],
|
|
19932
|
+
severity: "warn",
|
|
19933
|
+
recommendation: "Prefer `w-full` (`width: 100%`) over `w-screen` / `100vw`. `100vw` ignores the scrollbar gutter and overflows horizontally.",
|
|
19934
|
+
create: (context) => ({
|
|
19935
|
+
JSXAttribute(node) {
|
|
19936
|
+
const expression = getInlineStyleExpression(node);
|
|
19937
|
+
if (!expression) return;
|
|
19938
|
+
for (const property of expression.properties ?? []) {
|
|
19939
|
+
const key = getStylePropertyKey(property);
|
|
19940
|
+
if (!key || !WIDTH_KEYS.has(key)) continue;
|
|
19941
|
+
const value = getStylePropertyStringValue(property);
|
|
19942
|
+
if (value && value.trim().toLowerCase() === "100vw") context.report({
|
|
19943
|
+
node: property,
|
|
19944
|
+
message: MESSAGE$25
|
|
19945
|
+
});
|
|
19946
|
+
}
|
|
19947
|
+
},
|
|
19948
|
+
JSXOpeningElement(node) {
|
|
19949
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
19950
|
+
if (!classNameValue) return;
|
|
19951
|
+
if (FULL_VIEWPORT_WIDTH_CLASS.test(classNameValue)) context.report({
|
|
19952
|
+
node,
|
|
19953
|
+
message: MESSAGE$25
|
|
19954
|
+
});
|
|
19955
|
+
}
|
|
19956
|
+
})
|
|
19957
|
+
});
|
|
19958
|
+
//#endregion
|
|
19736
19959
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19737
19960
|
const noGenericHandlerNames = defineRule({
|
|
19738
19961
|
id: "no-generic-handler-names",
|
|
@@ -19795,7 +20018,7 @@ const noGiantComponent = defineRule({
|
|
|
19795
20018
|
});
|
|
19796
20019
|
//#endregion
|
|
19797
20020
|
//#region src/plugin/constants/style.ts
|
|
19798
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
20021
|
+
const LAYOUT_PROPERTIES$1 = new Set([
|
|
19799
20022
|
"width",
|
|
19800
20023
|
"height",
|
|
19801
20024
|
"top",
|
|
@@ -19865,17 +20088,6 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
19865
20088
|
} })
|
|
19866
20089
|
});
|
|
19867
20090
|
//#endregion
|
|
19868
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
19869
|
-
const getStringFromClassNameAttr = (node) => {
|
|
19870
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
19871
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
19872
|
-
if (!classAttr?.value) return null;
|
|
19873
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
19874
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
19875
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "TemplateLiteral") && classAttr.value.expression.quasis?.length === 1) return classAttr.value.expression.quasis[0].value?.raw ?? null;
|
|
19876
|
-
return null;
|
|
19877
|
-
};
|
|
19878
|
-
//#endregion
|
|
19879
20091
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
19880
20092
|
const noGradientText = defineRule({
|
|
19881
20093
|
id: "no-gradient-text",
|
|
@@ -19934,7 +20146,7 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19934
20146
|
});
|
|
19935
20147
|
//#endregion
|
|
19936
20148
|
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
19937
|
-
const MESSAGE$
|
|
20149
|
+
const MESSAGE$24 = "`<img loading=\"lazy\">` defers the request while `fetchPriority=\"high\"` asks the browser to rush it, so the two directives contradict each other. Drop one: keep `fetchPriority=\"high\"` (and eager loading) for an LCP image, or `loading=\"lazy\"` for a below-the-fold one.";
|
|
19938
20150
|
const noImgLazyWithHighFetchpriority = defineRule({
|
|
19939
20151
|
id: "no-img-lazy-with-high-fetchpriority",
|
|
19940
20152
|
title: "Lazy image with high fetchPriority",
|
|
@@ -19948,7 +20160,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
19948
20160
|
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
19949
20161
|
context.report({
|
|
19950
20162
|
node: node.name,
|
|
19951
|
-
message: MESSAGE$
|
|
20163
|
+
message: MESSAGE$24
|
|
19952
20164
|
});
|
|
19953
20165
|
} })
|
|
19954
20166
|
});
|
|
@@ -20183,7 +20395,7 @@ const noIsMounted = defineRule({
|
|
|
20183
20395
|
});
|
|
20184
20396
|
//#endregion
|
|
20185
20397
|
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
20186
|
-
const MESSAGE$
|
|
20398
|
+
const MESSAGE$23 = "`JSON.parse(JSON.stringify(x))` deep-clones by re-serializing: it is slow on large objects and silently drops `undefined`, functions, `Date`/`Map`/`Set`, and cyclic references. Use `structuredClone(x)`.";
|
|
20187
20399
|
const isJsonMethodCall = (node, method) => {
|
|
20188
20400
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20189
20401
|
const callee = node.callee;
|
|
@@ -20200,13 +20412,13 @@ const noJsonParseStringifyClone = defineRule({
|
|
|
20200
20412
|
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20201
20413
|
context.report({
|
|
20202
20414
|
node,
|
|
20203
|
-
message: MESSAGE$
|
|
20415
|
+
message: MESSAGE$23
|
|
20204
20416
|
});
|
|
20205
20417
|
} })
|
|
20206
20418
|
});
|
|
20207
20419
|
//#endregion
|
|
20208
20420
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
20209
|
-
const MESSAGE$
|
|
20421
|
+
const MESSAGE$22 = "`JSX.Element` is too narrow: it excludes `null`, strings, numbers, and fragments that components commonly return. Use `React.ReactNode` instead.";
|
|
20210
20422
|
const isJsxElementTypeReference = (node) => {
|
|
20211
20423
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
20212
20424
|
const typeName = node.typeName;
|
|
@@ -20223,7 +20435,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20223
20435
|
if (!typeAnnotation) return;
|
|
20224
20436
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20225
20437
|
node: typeAnnotation,
|
|
20226
|
-
message: MESSAGE$
|
|
20438
|
+
message: MESSAGE$22
|
|
20227
20439
|
});
|
|
20228
20440
|
};
|
|
20229
20441
|
const noJsxElementType = defineRule({
|
|
@@ -20333,7 +20545,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20333
20545
|
let propertyName = null;
|
|
20334
20546
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20335
20547
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20336
|
-
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20548
|
+
if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
|
|
20337
20549
|
node: property,
|
|
20338
20550
|
message: `This stutters because animating "${propertyName}" makes the browser redo page layout every frame, so animate transform or scale instead, or use the layout prop`
|
|
20339
20551
|
});
|
|
@@ -20523,6 +20735,134 @@ const noLongTransitionDuration = defineRule({
|
|
|
20523
20735
|
} })
|
|
20524
20736
|
});
|
|
20525
20737
|
//#endregion
|
|
20738
|
+
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
20739
|
+
const getStylePropertyNumberValue = (property) => {
|
|
20740
|
+
if (!isNodeOfType(property, "Property")) return null;
|
|
20741
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
20742
|
+
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
20743
|
+
return null;
|
|
20744
|
+
};
|
|
20745
|
+
//#endregion
|
|
20746
|
+
//#region src/plugin/rules/design/utils/get-wcag-contrast-ratio.ts
|
|
20747
|
+
const linearizeChannel = (channel) => {
|
|
20748
|
+
const normalized = channel / 255;
|
|
20749
|
+
return normalized <= .03928 ? normalized / 12.92 : Math.pow((normalized + .055) / 1.055, 2.4);
|
|
20750
|
+
};
|
|
20751
|
+
const relativeLuminance = (color) => .2126 * linearizeChannel(color.red) + .7152 * linearizeChannel(color.green) + .0722 * linearizeChannel(color.blue);
|
|
20752
|
+
const getWcagContrastRatio = (foreground, background) => {
|
|
20753
|
+
const foregroundLuminance = relativeLuminance(foreground);
|
|
20754
|
+
const backgroundLuminance = relativeLuminance(background);
|
|
20755
|
+
const lighter = Math.max(foregroundLuminance, backgroundLuminance);
|
|
20756
|
+
const darker = Math.min(foregroundLuminance, backgroundLuminance);
|
|
20757
|
+
return (lighter + .05) / (darker + .05);
|
|
20758
|
+
};
|
|
20759
|
+
//#endregion
|
|
20760
|
+
//#region src/plugin/rules/design/no-low-contrast-inline-style.ts
|
|
20761
|
+
const UNRESOLVABLE = new Set([
|
|
20762
|
+
"transparent",
|
|
20763
|
+
"currentcolor",
|
|
20764
|
+
"inherit",
|
|
20765
|
+
"initial",
|
|
20766
|
+
"unset",
|
|
20767
|
+
"revert",
|
|
20768
|
+
"none"
|
|
20769
|
+
]);
|
|
20770
|
+
const resolveOpaqueColor = (raw) => {
|
|
20771
|
+
const value = raw.trim().toLowerCase();
|
|
20772
|
+
if (UNRESOLVABLE.has(value)) return null;
|
|
20773
|
+
if (value === "white") return {
|
|
20774
|
+
red: 255,
|
|
20775
|
+
green: 255,
|
|
20776
|
+
blue: 255
|
|
20777
|
+
};
|
|
20778
|
+
if (value === "black") return {
|
|
20779
|
+
red: 0,
|
|
20780
|
+
green: 0,
|
|
20781
|
+
blue: 0
|
|
20782
|
+
};
|
|
20783
|
+
if (value.startsWith("var(")) return null;
|
|
20784
|
+
if (/^#(?:[0-9a-f]{4}|[0-9a-f]{8})$/.test(value)) return null;
|
|
20785
|
+
if (value.startsWith("hsl") || value.startsWith("oklch")) return null;
|
|
20786
|
+
if (value.startsWith("rgb")) {
|
|
20787
|
+
const inner = value.slice(value.indexOf("(") + 1, value.lastIndexOf(")"));
|
|
20788
|
+
if (inner.includes("/") || inner.split(",").length >= 4) return null;
|
|
20789
|
+
}
|
|
20790
|
+
return parseColorToRgb(value);
|
|
20791
|
+
};
|
|
20792
|
+
const toPx = (property) => {
|
|
20793
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20794
|
+
if (numberValue !== null) return numberValue;
|
|
20795
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20796
|
+
if (stringValue === null) return null;
|
|
20797
|
+
const pxMatch = stringValue.match(/^([\d.]+)px$/);
|
|
20798
|
+
if (pxMatch) return parseFloat(pxMatch[1]);
|
|
20799
|
+
const remMatch = stringValue.match(/^([\d.]+)rem$/);
|
|
20800
|
+
if (remMatch) return parseFloat(remMatch[1]) * 16;
|
|
20801
|
+
return null;
|
|
20802
|
+
};
|
|
20803
|
+
const isBoldWeight = (property) => {
|
|
20804
|
+
const numberValue = getStylePropertyNumberValue(property);
|
|
20805
|
+
if (numberValue !== null) return numberValue >= 700;
|
|
20806
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20807
|
+
if (stringValue === null) return false;
|
|
20808
|
+
if (stringValue === "bold" || stringValue === "bolder") return true;
|
|
20809
|
+
const numericWeight = Number(stringValue);
|
|
20810
|
+
return Number.isFinite(numericWeight) && numericWeight >= 700;
|
|
20811
|
+
};
|
|
20812
|
+
const noLowContrastInlineStyle = defineRule({
|
|
20813
|
+
id: "no-low-contrast-inline-style",
|
|
20814
|
+
title: "Low-contrast text in inline style",
|
|
20815
|
+
tags: ["test-noise"],
|
|
20816
|
+
severity: "warn",
|
|
20817
|
+
category: "Accessibility",
|
|
20818
|
+
recommendation: "Text needs a WCAG contrast ratio of at least 4.5:1 (3:1 for large/bold text) against its background. Darken or lighten one of the colors until it passes.",
|
|
20819
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
20820
|
+
const expression = getInlineStyleExpression(node);
|
|
20821
|
+
if (!expression) return;
|
|
20822
|
+
const properties = expression.properties ?? [];
|
|
20823
|
+
if (properties.some((property) => property.type === "SpreadElement")) return;
|
|
20824
|
+
let foreground = null;
|
|
20825
|
+
let backgroundColorRaw = null;
|
|
20826
|
+
let backgroundShorthandRaw = null;
|
|
20827
|
+
let backgroundIsUnknown = false;
|
|
20828
|
+
let fontSizePx = null;
|
|
20829
|
+
let isBold = false;
|
|
20830
|
+
for (const property of properties) {
|
|
20831
|
+
const key = getStylePropertyKey(property);
|
|
20832
|
+
if (!key) continue;
|
|
20833
|
+
if (key === "backgroundImage") {
|
|
20834
|
+
backgroundIsUnknown = true;
|
|
20835
|
+
continue;
|
|
20836
|
+
}
|
|
20837
|
+
if (key === "fontSize" && property.type === "Property") {
|
|
20838
|
+
fontSizePx = toPx(property);
|
|
20839
|
+
continue;
|
|
20840
|
+
}
|
|
20841
|
+
if (key === "fontWeight" && property.type === "Property") {
|
|
20842
|
+
isBold = isBoldWeight(property);
|
|
20843
|
+
continue;
|
|
20844
|
+
}
|
|
20845
|
+
const stringValue = getStylePropertyStringValue(property);
|
|
20846
|
+
if (key === "color") {
|
|
20847
|
+
if (stringValue !== null) foreground = resolveOpaqueColor(stringValue);
|
|
20848
|
+
} else if (key === "backgroundColor") backgroundColorRaw = stringValue;
|
|
20849
|
+
else if (key === "background") if (stringValue === null) backgroundIsUnknown = true;
|
|
20850
|
+
else backgroundShorthandRaw = stringValue;
|
|
20851
|
+
}
|
|
20852
|
+
if (backgroundIsUnknown) return;
|
|
20853
|
+
if (backgroundColorRaw !== null && backgroundShorthandRaw !== null) return;
|
|
20854
|
+
const backgroundRaw = backgroundColorRaw ?? backgroundShorthandRaw;
|
|
20855
|
+
const background = backgroundRaw === null ? null : resolveOpaqueColor(backgroundRaw);
|
|
20856
|
+
if (!foreground || !background) return;
|
|
20857
|
+
const threshold = fontSizePx === null || fontSizePx >= 24 || isBold && fontSizePx >= 18.66 ? 3 : WCAG_CONTRAST_NORMAL_MIN;
|
|
20858
|
+
const ratio = getWcagContrastRatio(foreground, background);
|
|
20859
|
+
if (ratio < threshold) context.report({
|
|
20860
|
+
node,
|
|
20861
|
+
message: `Your users struggle to read this text: its contrast against the background is ${ratio.toFixed(2)}:1, below the ${threshold}:1 WCAG minimum, so darken or lighten one of the colors.`
|
|
20862
|
+
});
|
|
20863
|
+
} })
|
|
20864
|
+
});
|
|
20865
|
+
//#endregion
|
|
20526
20866
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20527
20867
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20528
20868
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
@@ -20678,7 +21018,7 @@ const noMoment = defineRule({
|
|
|
20678
21018
|
});
|
|
20679
21019
|
//#endregion
|
|
20680
21020
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20681
|
-
const MESSAGE$
|
|
21021
|
+
const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20682
21022
|
const resolveSettings$16 = (settings) => {
|
|
20683
21023
|
const reactDoctor = settings?.["react-doctor"];
|
|
20684
21024
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -21000,7 +21340,7 @@ const noMultiComp = defineRule({
|
|
|
21000
21340
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
21001
21341
|
for (const component of flagged.slice(1)) context.report({
|
|
21002
21342
|
node: component.reportNode,
|
|
21003
|
-
message: MESSAGE$
|
|
21343
|
+
message: MESSAGE$21
|
|
21004
21344
|
});
|
|
21005
21345
|
} };
|
|
21006
21346
|
}
|
|
@@ -21168,7 +21508,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
21168
21508
|
};
|
|
21169
21509
|
//#endregion
|
|
21170
21510
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
21171
|
-
const MESSAGE$
|
|
21511
|
+
const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
|
|
21172
21512
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
21173
21513
|
"copyWithin",
|
|
21174
21514
|
"fill",
|
|
@@ -21378,7 +21718,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21378
21718
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21379
21719
|
context.report({
|
|
21380
21720
|
node: options.crossFileConsumerCallSite,
|
|
21381
|
-
message: `${MESSAGE$
|
|
21721
|
+
message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21382
21722
|
});
|
|
21383
21723
|
return;
|
|
21384
21724
|
}
|
|
@@ -21387,7 +21727,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21387
21727
|
reportedNodes.add(mutation.node);
|
|
21388
21728
|
context.report({
|
|
21389
21729
|
node: mutation.node,
|
|
21390
|
-
message: MESSAGE$
|
|
21730
|
+
message: MESSAGE$20
|
|
21391
21731
|
});
|
|
21392
21732
|
}
|
|
21393
21733
|
};
|
|
@@ -21659,7 +21999,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21659
21999
|
});
|
|
21660
22000
|
//#endregion
|
|
21661
22001
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21662
|
-
const MESSAGE$
|
|
22002
|
+
const MESSAGE$19 = "Keyboard users get stuck focusing this element they can't act on because `tabIndex` makes it tabbable, so remove it.";
|
|
21663
22003
|
const resolveSettings$14 = (settings) => {
|
|
21664
22004
|
const reactDoctor = settings?.["react-doctor"];
|
|
21665
22005
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21687,7 +22027,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21687
22027
|
if (numeric === null) {
|
|
21688
22028
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21689
22029
|
node: tabIndex,
|
|
21690
|
-
message: MESSAGE$
|
|
22030
|
+
message: MESSAGE$19
|
|
21691
22031
|
});
|
|
21692
22032
|
return;
|
|
21693
22033
|
}
|
|
@@ -21700,7 +22040,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21700
22040
|
if (!roleAttribute) {
|
|
21701
22041
|
context.report({
|
|
21702
22042
|
node: tabIndex,
|
|
21703
|
-
message: MESSAGE$
|
|
22043
|
+
message: MESSAGE$19
|
|
21704
22044
|
});
|
|
21705
22045
|
return;
|
|
21706
22046
|
}
|
|
@@ -21714,20 +22054,12 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21714
22054
|
}
|
|
21715
22055
|
context.report({
|
|
21716
22056
|
node: tabIndex,
|
|
21717
|
-
message: MESSAGE$
|
|
22057
|
+
message: MESSAGE$19
|
|
21718
22058
|
});
|
|
21719
22059
|
} };
|
|
21720
22060
|
}
|
|
21721
22061
|
});
|
|
21722
22062
|
//#endregion
|
|
21723
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21724
|
-
const getStylePropertyNumberValue = (property) => {
|
|
21725
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
21726
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21727
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21728
|
-
return null;
|
|
21729
|
-
};
|
|
21730
|
-
//#endregion
|
|
21731
22063
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
21732
22064
|
const noOutlineNone = defineRule({
|
|
21733
22065
|
id: "no-outline-none",
|
|
@@ -22405,7 +22737,7 @@ const noRandomKey = defineRule({
|
|
|
22405
22737
|
});
|
|
22406
22738
|
//#endregion
|
|
22407
22739
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22408
|
-
const MESSAGE$
|
|
22740
|
+
const MESSAGE$18 = "`React.Children` traversal depends on the runtime child shape, so wrapping or unwrapping a child can silently change what gets visited.";
|
|
22409
22741
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22410
22742
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22411
22743
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22431,13 +22763,13 @@ const noReactChildren = defineRule({
|
|
|
22431
22763
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22432
22764
|
context.report({
|
|
22433
22765
|
node: calleeOuter,
|
|
22434
|
-
message: MESSAGE$
|
|
22766
|
+
message: MESSAGE$18
|
|
22435
22767
|
});
|
|
22436
22768
|
return;
|
|
22437
22769
|
}
|
|
22438
22770
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22439
22771
|
node: calleeOuter,
|
|
22440
|
-
message: MESSAGE$
|
|
22772
|
+
message: MESSAGE$18
|
|
22441
22773
|
});
|
|
22442
22774
|
} })
|
|
22443
22775
|
});
|
|
@@ -22548,6 +22880,86 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22548
22880
|
})
|
|
22549
22881
|
});
|
|
22550
22882
|
//#endregion
|
|
22883
|
+
//#region src/plugin/rules/design/no-redundant-display-class.ts
|
|
22884
|
+
const BLOCK_DEFAULT_TAGS = new Set([
|
|
22885
|
+
"div",
|
|
22886
|
+
"p",
|
|
22887
|
+
"section",
|
|
22888
|
+
"article",
|
|
22889
|
+
"main",
|
|
22890
|
+
"header",
|
|
22891
|
+
"footer",
|
|
22892
|
+
"nav",
|
|
22893
|
+
"aside",
|
|
22894
|
+
"figure",
|
|
22895
|
+
"figcaption",
|
|
22896
|
+
"blockquote",
|
|
22897
|
+
"form",
|
|
22898
|
+
"fieldset",
|
|
22899
|
+
"address",
|
|
22900
|
+
"pre",
|
|
22901
|
+
"ul",
|
|
22902
|
+
"ol",
|
|
22903
|
+
"dl",
|
|
22904
|
+
"dt",
|
|
22905
|
+
"dd",
|
|
22906
|
+
"h1",
|
|
22907
|
+
"h2",
|
|
22908
|
+
"h3",
|
|
22909
|
+
"h4",
|
|
22910
|
+
"h5",
|
|
22911
|
+
"h6"
|
|
22912
|
+
]);
|
|
22913
|
+
const INLINE_DEFAULT_TAGS = new Set([
|
|
22914
|
+
"span",
|
|
22915
|
+
"a",
|
|
22916
|
+
"b",
|
|
22917
|
+
"i",
|
|
22918
|
+
"em",
|
|
22919
|
+
"strong",
|
|
22920
|
+
"small",
|
|
22921
|
+
"code",
|
|
22922
|
+
"abbr",
|
|
22923
|
+
"cite",
|
|
22924
|
+
"label",
|
|
22925
|
+
"mark",
|
|
22926
|
+
"q",
|
|
22927
|
+
"s",
|
|
22928
|
+
"u",
|
|
22929
|
+
"sub",
|
|
22930
|
+
"sup",
|
|
22931
|
+
"kbd",
|
|
22932
|
+
"samp",
|
|
22933
|
+
"var",
|
|
22934
|
+
"time"
|
|
22935
|
+
]);
|
|
22936
|
+
const STANDALONE_BLOCK = /(?:^|\s)block(?:$|\s)/;
|
|
22937
|
+
const STANDALONE_INLINE = /(?:^|\s)inline(?:$|\s)/;
|
|
22938
|
+
const noRedundantDisplayClass = defineRule({
|
|
22939
|
+
id: "no-redundant-display-class",
|
|
22940
|
+
title: "Redundant display utility",
|
|
22941
|
+
tags: ["design", "test-noise"],
|
|
22942
|
+
severity: "warn",
|
|
22943
|
+
recommendation: "Drop the display class that matches the element's default (`block` on a `<div>`, `inline` on a `<span>`). It is pure noise; keep only display changes like `flex`, `grid`, or `hidden`.",
|
|
22944
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
22945
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
22946
|
+
const tagName = node.name.name;
|
|
22947
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
22948
|
+
if (!classNameValue) return;
|
|
22949
|
+
if (BLOCK_DEFAULT_TAGS.has(tagName) && STANDALONE_BLOCK.test(classNameValue)) {
|
|
22950
|
+
context.report({
|
|
22951
|
+
node,
|
|
22952
|
+
message: `\`block\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22953
|
+
});
|
|
22954
|
+
return;
|
|
22955
|
+
}
|
|
22956
|
+
if (INLINE_DEFAULT_TAGS.has(tagName) && STANDALONE_INLINE.test(classNameValue)) context.report({
|
|
22957
|
+
node,
|
|
22958
|
+
message: `\`inline\` is the default display of \`<${tagName}>\`, so the class does nothing — remove it.`
|
|
22959
|
+
});
|
|
22960
|
+
} })
|
|
22961
|
+
});
|
|
22962
|
+
//#endregion
|
|
22551
22963
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22552
22964
|
const ELEMENT_ROLE_PAIRS = [
|
|
22553
22965
|
["a", "link"],
|
|
@@ -22760,7 +23172,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22760
23172
|
});
|
|
22761
23173
|
//#endregion
|
|
22762
23174
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22763
|
-
const MESSAGE$
|
|
23175
|
+
const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22764
23176
|
const isReactDomRenderCall = (node) => {
|
|
22765
23177
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22766
23178
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22784,7 +23196,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22784
23196
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22785
23197
|
context.report({
|
|
22786
23198
|
node: node.callee,
|
|
22787
|
-
message: MESSAGE$
|
|
23199
|
+
message: MESSAGE$17
|
|
22788
23200
|
});
|
|
22789
23201
|
} })
|
|
22790
23202
|
});
|
|
@@ -22944,11 +23356,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22944
23356
|
return "unknown";
|
|
22945
23357
|
};
|
|
22946
23358
|
//#endregion
|
|
22947
|
-
//#region src/plugin/utils/
|
|
22948
|
-
const
|
|
22949
|
-
|
|
23359
|
+
//#region src/plugin/utils/tokenize-identifier-words.ts
|
|
23360
|
+
const IDENTIFIER_WORD_PATTERN = /[A-Z]+(?=[A-Z][a-z]|\b)|[A-Z]?[a-z]+|\d+/g;
|
|
23361
|
+
const tokenizeIdentifierWords = (identifierName) => {
|
|
23362
|
+
const words = identifierName.match(IDENTIFIER_WORD_PATTERN);
|
|
23363
|
+
if (!words) return [];
|
|
23364
|
+
return words.map((word) => word.toLowerCase());
|
|
22950
23365
|
};
|
|
22951
23366
|
//#endregion
|
|
23367
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23368
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23369
|
+
//#endregion
|
|
22952
23370
|
//#region src/plugin/constants/tanstack.ts
|
|
22953
23371
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22954
23372
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23476,7 +23894,7 @@ const getParentComponent = (node) => {
|
|
|
23476
23894
|
};
|
|
23477
23895
|
//#endregion
|
|
23478
23896
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23479
|
-
const MESSAGE$
|
|
23897
|
+
const MESSAGE$16 = "`this.setState` keeps local class state in a project that forbids it, so state ownership becomes harder to reason about.";
|
|
23480
23898
|
const noSetState = defineRule({
|
|
23481
23899
|
id: "no-set-state",
|
|
23482
23900
|
title: "Local class state forbidden",
|
|
@@ -23491,7 +23909,7 @@ const noSetState = defineRule({
|
|
|
23491
23909
|
if (!getParentComponent(node)) return;
|
|
23492
23910
|
context.report({
|
|
23493
23911
|
node: node.callee,
|
|
23494
|
-
message: MESSAGE$
|
|
23912
|
+
message: MESSAGE$16
|
|
23495
23913
|
});
|
|
23496
23914
|
} })
|
|
23497
23915
|
});
|
|
@@ -23653,7 +24071,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23653
24071
|
};
|
|
23654
24072
|
//#endregion
|
|
23655
24073
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23656
|
-
const MESSAGE$
|
|
24074
|
+
const MESSAGE$15 = "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.";
|
|
23657
24075
|
const DEFAULT_HANDLERS = [
|
|
23658
24076
|
"onClick",
|
|
23659
24077
|
"onMouseDown",
|
|
@@ -23713,7 +24131,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23713
24131
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23714
24132
|
context.report({
|
|
23715
24133
|
node: node.name,
|
|
23716
|
-
message: MESSAGE$
|
|
24134
|
+
message: MESSAGE$15
|
|
23717
24135
|
});
|
|
23718
24136
|
return;
|
|
23719
24137
|
}
|
|
@@ -23723,19 +24141,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23723
24141
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23724
24142
|
context.report({
|
|
23725
24143
|
node: node.name,
|
|
23726
|
-
message: MESSAGE$
|
|
24144
|
+
message: MESSAGE$15
|
|
23727
24145
|
});
|
|
23728
24146
|
return;
|
|
23729
24147
|
}
|
|
23730
24148
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23731
24149
|
context.report({
|
|
23732
24150
|
node: node.name,
|
|
23733
|
-
message: MESSAGE$
|
|
24151
|
+
message: MESSAGE$15
|
|
23734
24152
|
});
|
|
23735
24153
|
} };
|
|
23736
24154
|
}
|
|
23737
24155
|
});
|
|
23738
24156
|
//#endregion
|
|
24157
|
+
//#region src/plugin/rules/react-builtins/no-string-false-on-boolean-attribute.ts
|
|
24158
|
+
const BOOLEAN_ATTRIBUTES = new Set([
|
|
24159
|
+
"disabled",
|
|
24160
|
+
"checked",
|
|
24161
|
+
"readonly",
|
|
24162
|
+
"required",
|
|
24163
|
+
"selected",
|
|
24164
|
+
"multiple",
|
|
24165
|
+
"autofocus",
|
|
24166
|
+
"autoplay",
|
|
24167
|
+
"controls",
|
|
24168
|
+
"loop",
|
|
24169
|
+
"muted",
|
|
24170
|
+
"open",
|
|
24171
|
+
"reversed",
|
|
24172
|
+
"default",
|
|
24173
|
+
"novalidate",
|
|
24174
|
+
"formnovalidate",
|
|
24175
|
+
"playsinline",
|
|
24176
|
+
"itemscope",
|
|
24177
|
+
"allowfullscreen"
|
|
24178
|
+
]);
|
|
24179
|
+
const noStringFalseOnBooleanAttribute = defineRule({
|
|
24180
|
+
id: "no-string-false-on-boolean-attribute",
|
|
24181
|
+
title: "String true/false on a boolean attribute",
|
|
24182
|
+
severity: "warn",
|
|
24183
|
+
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.",
|
|
24184
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24185
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24186
|
+
const firstCharacter = node.name.name.charCodeAt(0);
|
|
24187
|
+
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
24188
|
+
for (const attribute of node.attributes) {
|
|
24189
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
24190
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
24191
|
+
if (!BOOLEAN_ATTRIBUTES.has(attribute.name.name.toLowerCase())) continue;
|
|
24192
|
+
const value = getJsxPropStringValue(attribute);
|
|
24193
|
+
if (value !== "false" && value !== "true") continue;
|
|
24194
|
+
const attributeName = attribute.name.name;
|
|
24195
|
+
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}\``;
|
|
24196
|
+
context.report({
|
|
24197
|
+
node: attribute,
|
|
24198
|
+
message: `\`${attributeName}="${value}"\` passes the string "${value}", ${guidance}.`
|
|
24199
|
+
});
|
|
24200
|
+
}
|
|
24201
|
+
} })
|
|
24202
|
+
});
|
|
24203
|
+
//#endregion
|
|
23739
24204
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23740
24205
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23741
24206
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23786,8 +24251,154 @@ const noStringRefs = defineRule({
|
|
|
23786
24251
|
}
|
|
23787
24252
|
});
|
|
23788
24253
|
//#endregion
|
|
24254
|
+
//#region src/plugin/rules/design/no-svg-currentcolor-with-fill-class.ts
|
|
24255
|
+
const hasColorUtility = (classNameValue, prefix) => classNameValue.split(/\s+/).some((token) => {
|
|
24256
|
+
if (token.includes(":")) return false;
|
|
24257
|
+
if (!token.startsWith(prefix)) return false;
|
|
24258
|
+
const value = token.slice(prefix.length);
|
|
24259
|
+
if (value === "" || value === "current") return false;
|
|
24260
|
+
if (/^\d/.test(value) || /^\[\d/.test(value)) return false;
|
|
24261
|
+
return true;
|
|
24262
|
+
});
|
|
24263
|
+
const isCurrentColor = (attribute) => {
|
|
24264
|
+
const value = getJsxPropStringValue(attribute);
|
|
24265
|
+
return value !== null && value.trim().toLowerCase() === "currentcolor";
|
|
24266
|
+
};
|
|
24267
|
+
const noSvgCurrentcolorWithFillClass = defineRule({
|
|
24268
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
24269
|
+
title: "currentColor fights a fill/stroke class",
|
|
24270
|
+
tags: ["design", "test-noise"],
|
|
24271
|
+
severity: "warn",
|
|
24272
|
+
recommendation: "Pick one source of truth: drop the `fill=\"currentColor\"` attribute and keep the `fill-*` class, or use `fill-current` to inherit the text color. Having both means the class silently wins.",
|
|
24273
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24274
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24275
|
+
if (!classNameValue) return;
|
|
24276
|
+
for (const paint of ["fill", "stroke"]) {
|
|
24277
|
+
const attribute = findJsxAttribute(node.attributes, paint);
|
|
24278
|
+
if (attribute && isCurrentColor(attribute) && hasColorUtility(classNameValue, `${paint}-`)) {
|
|
24279
|
+
context.report({
|
|
24280
|
+
node: attribute,
|
|
24281
|
+
message: `\`${paint}="currentColor"\` and a \`${paint}-*\` color class on the same element conflict — the class wins. Remove one, or use \`${paint}-current\` to inherit the text color.`
|
|
24282
|
+
});
|
|
24283
|
+
return;
|
|
24284
|
+
}
|
|
24285
|
+
}
|
|
24286
|
+
} })
|
|
24287
|
+
});
|
|
24288
|
+
//#endregion
|
|
24289
|
+
//#region src/plugin/rules/js-performance/no-sync-xhr.ts
|
|
24290
|
+
const MESSAGE$14 = "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)`).";
|
|
24291
|
+
const isFalseLiteral = (node) => isNodeOfType(node, "Literal") && node.value === false;
|
|
24292
|
+
const noSyncXhr = defineRule({
|
|
24293
|
+
id: "no-sync-xhr",
|
|
24294
|
+
title: "Synchronous XMLHttpRequest",
|
|
24295
|
+
severity: "warn",
|
|
24296
|
+
recommendation: "Never open an XMLHttpRequest synchronously (`async` = `false`). It blocks the main thread. Use `fetch()` or pass `true` and handle the response asynchronously.",
|
|
24297
|
+
create: (context) => ({ CallExpression(node) {
|
|
24298
|
+
const callee = node.callee;
|
|
24299
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
24300
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "open") return;
|
|
24301
|
+
const asyncArgument = node.arguments?.[2];
|
|
24302
|
+
if (!asyncArgument || !isFalseLiteral(stripParenExpression(asyncArgument))) return;
|
|
24303
|
+
context.report({
|
|
24304
|
+
node,
|
|
24305
|
+
message: MESSAGE$14
|
|
24306
|
+
});
|
|
24307
|
+
} })
|
|
24308
|
+
});
|
|
24309
|
+
//#endregion
|
|
24310
|
+
//#region src/plugin/rules/design/no-tailwind-layout-transition.ts
|
|
24311
|
+
const ARBITRARY_TRANSITION_PROPERTY = /transition-\[([^\]]+)\]/g;
|
|
24312
|
+
const LAYOUT_PROPERTIES = new Set([
|
|
24313
|
+
"width",
|
|
24314
|
+
"height",
|
|
24315
|
+
"min-width",
|
|
24316
|
+
"max-width",
|
|
24317
|
+
"min-height",
|
|
24318
|
+
"max-height",
|
|
24319
|
+
"top",
|
|
24320
|
+
"left",
|
|
24321
|
+
"right",
|
|
24322
|
+
"bottom",
|
|
24323
|
+
"inset",
|
|
24324
|
+
"inset-block",
|
|
24325
|
+
"inset-inline",
|
|
24326
|
+
"margin",
|
|
24327
|
+
"margin-top",
|
|
24328
|
+
"margin-right",
|
|
24329
|
+
"margin-bottom",
|
|
24330
|
+
"margin-left",
|
|
24331
|
+
"margin-block",
|
|
24332
|
+
"margin-inline",
|
|
24333
|
+
"padding",
|
|
24334
|
+
"padding-top",
|
|
24335
|
+
"padding-right",
|
|
24336
|
+
"padding-bottom",
|
|
24337
|
+
"padding-left",
|
|
24338
|
+
"padding-block",
|
|
24339
|
+
"padding-inline"
|
|
24340
|
+
]);
|
|
24341
|
+
const noTailwindLayoutTransition = defineRule({
|
|
24342
|
+
id: "no-tailwind-layout-transition",
|
|
24343
|
+
title: "Animating a layout property",
|
|
24344
|
+
tags: ["design", "test-noise"],
|
|
24345
|
+
severity: "warn",
|
|
24346
|
+
category: "Performance",
|
|
24347
|
+
recommendation: "Animate `transform` and `opacity` instead, since they skip layout and run on the compositor. For height, animate `grid-template-rows` from `0fr` to `1fr`.",
|
|
24348
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24349
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24350
|
+
if (!classNameValue) return;
|
|
24351
|
+
for (const transitionMatch of classNameValue.matchAll(ARBITRARY_TRANSITION_PROPERTY)) {
|
|
24352
|
+
const animatedProperties = transitionMatch[1];
|
|
24353
|
+
const layoutProperty = animatedProperties.split(",").map((property) => property.trim()).find((property) => LAYOUT_PROPERTIES.has(property));
|
|
24354
|
+
if (layoutProperty) context.report({
|
|
24355
|
+
node,
|
|
24356
|
+
message: `Your users see janky animation because \`transition-[${animatedProperties}]\` animates "${layoutProperty}", a layout property the browser recomputes every frame, so animate transform & opacity instead.`
|
|
24357
|
+
});
|
|
24358
|
+
}
|
|
24359
|
+
} })
|
|
24360
|
+
});
|
|
24361
|
+
//#endregion
|
|
24362
|
+
//#region src/plugin/rules/a11y/no-target-blank-without-rel.ts
|
|
24363
|
+
const MESSAGE$13 = "`<a target=\"_blank\">` without `rel=\"noopener\"` lets the opened page script your tab via `window.opener` (reverse tabnabbing). Add `rel=\"noopener noreferrer\"`.";
|
|
24364
|
+
const targetIsBlank = (attribute) => {
|
|
24365
|
+
const stringValue = getJsxPropStringValue(attribute);
|
|
24366
|
+
if (stringValue !== null) return stringValue === "_blank";
|
|
24367
|
+
const value = attribute.value;
|
|
24368
|
+
if (value && isNodeOfType(value, "JSXExpressionContainer")) {
|
|
24369
|
+
const expression = value.expression;
|
|
24370
|
+
if (isNodeOfType(expression, "Literal") && expression.value === "_blank") return true;
|
|
24371
|
+
}
|
|
24372
|
+
return false;
|
|
24373
|
+
};
|
|
24374
|
+
const noTargetBlankWithoutRel = defineRule({
|
|
24375
|
+
id: "no-target-blank-without-rel",
|
|
24376
|
+
title: "target=_blank without rel=noopener",
|
|
24377
|
+
severity: "warn",
|
|
24378
|
+
recommendation: "Add `rel=\"noopener noreferrer\"` to every `target=\"_blank\"` link. `noopener` blocks reverse tabnabbing; `noreferrer` also strips the `Referer` header.",
|
|
24379
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24380
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
24381
|
+
const tagName = node.name.name;
|
|
24382
|
+
if (tagName !== "a" && tagName !== "area") return;
|
|
24383
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
24384
|
+
const targetAttribute = findJsxAttribute(node.attributes, "target");
|
|
24385
|
+
if (!targetAttribute || !targetIsBlank(targetAttribute)) return;
|
|
24386
|
+
const relAttribute = findJsxAttribute(node.attributes, "rel");
|
|
24387
|
+
if (relAttribute) {
|
|
24388
|
+
const relValue = getJsxPropStringValue(relAttribute);
|
|
24389
|
+
if (relValue === null) return;
|
|
24390
|
+
const tokens = relValue.toLowerCase().split(/\s+/);
|
|
24391
|
+
if (tokens.includes("noopener") || tokens.includes("noreferrer")) return;
|
|
24392
|
+
}
|
|
24393
|
+
context.report({
|
|
24394
|
+
node: node.name,
|
|
24395
|
+
message: MESSAGE$13
|
|
24396
|
+
});
|
|
24397
|
+
} })
|
|
24398
|
+
});
|
|
24399
|
+
//#endregion
|
|
23789
24400
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23790
|
-
const MESSAGE$
|
|
24401
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
23791
24402
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
23792
24403
|
let ancestor = node.parent;
|
|
23793
24404
|
while (ancestor) {
|
|
@@ -23856,7 +24467,7 @@ const noThisInSfc = defineRule({
|
|
|
23856
24467
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
23857
24468
|
context.report({
|
|
23858
24469
|
node,
|
|
23859
|
-
message: MESSAGE$
|
|
24470
|
+
message: MESSAGE$12
|
|
23860
24471
|
});
|
|
23861
24472
|
} };
|
|
23862
24473
|
}
|
|
@@ -23894,26 +24505,39 @@ const noTinyText = defineRule({
|
|
|
23894
24505
|
});
|
|
23895
24506
|
//#endregion
|
|
23896
24507
|
//#region src/plugin/rules/performance/no-transition-all.ts
|
|
24508
|
+
const hasTransitionAllClass = (classNameValue) => getClassNameTokens(classNameValue).some((token) => token === "transition-all");
|
|
24509
|
+
const TAILWIND_MESSAGE = "Your users see janky animation because `transition-all` animates every property that changes, including expensive layout ones and instant ones like focus rings. Name the properties: `transition-colors`, `transition-opacity`, or `transition-transform`.";
|
|
23897
24510
|
const noTransitionAll = defineRule({
|
|
23898
24511
|
id: "no-transition-all",
|
|
23899
24512
|
title: "transition: all animates everything",
|
|
23900
24513
|
tags: ["test-noise"],
|
|
23901
24514
|
severity: "warn",
|
|
23902
24515
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
23903
|
-
create: (context) => ({
|
|
23904
|
-
|
|
23905
|
-
|
|
23906
|
-
|
|
23907
|
-
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
24516
|
+
create: (context) => ({
|
|
24517
|
+
JSXAttribute(node) {
|
|
24518
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "style") return;
|
|
24519
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
24520
|
+
const expression = node.value.expression;
|
|
24521
|
+
if (!isNodeOfType(expression, "ObjectExpression")) return;
|
|
24522
|
+
for (const property of expression.properties ?? []) {
|
|
24523
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
24524
|
+
const key = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
24525
|
+
if (key !== "transition" && key !== "transitionProperty") continue;
|
|
24526
|
+
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "string" && property.value.value.trim().startsWith("all")) context.report({
|
|
24527
|
+
node: property,
|
|
24528
|
+
message: "This can stutter because transition: \"all\" animates every property, even slow layout ones, so list only the properties you actually change"
|
|
24529
|
+
});
|
|
24530
|
+
}
|
|
24531
|
+
},
|
|
24532
|
+
JSXOpeningElement(node) {
|
|
24533
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
24534
|
+
if (!classNameValue) return;
|
|
24535
|
+
if (hasTransitionAllClass(classNameValue)) context.report({
|
|
24536
|
+
node,
|
|
24537
|
+
message: TAILWIND_MESSAGE
|
|
23914
24538
|
});
|
|
23915
24539
|
}
|
|
23916
|
-
}
|
|
24540
|
+
})
|
|
23917
24541
|
});
|
|
23918
24542
|
//#endregion
|
|
23919
24543
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -23957,7 +24581,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
23957
24581
|
}
|
|
23958
24582
|
return stateNames;
|
|
23959
24583
|
};
|
|
23960
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
23961
24584
|
const noUncontrolledInput = defineRule({
|
|
23962
24585
|
id: "no-uncontrolled-input",
|
|
23963
24586
|
title: "Uncontrolled input value",
|
|
@@ -24061,6 +24684,38 @@ const noUnescapedEntities = defineRule({
|
|
|
24061
24684
|
} })
|
|
24062
24685
|
});
|
|
24063
24686
|
//#endregion
|
|
24687
|
+
//#region src/plugin/rules/a11y/no-uninformative-aria-label.ts
|
|
24688
|
+
const UNINFORMATIVE_LABELS = new Set([
|
|
24689
|
+
"icon",
|
|
24690
|
+
"button",
|
|
24691
|
+
"image",
|
|
24692
|
+
"img",
|
|
24693
|
+
"link",
|
|
24694
|
+
"graphic",
|
|
24695
|
+
"svg",
|
|
24696
|
+
"picture",
|
|
24697
|
+
"element",
|
|
24698
|
+
"field",
|
|
24699
|
+
"input"
|
|
24700
|
+
]);
|
|
24701
|
+
const MESSAGE$11 = "An `aria-label` should name the action or destination, not the element type — this value tells screen-reader users nothing. Use something like `aria-label=\"Search\"` or `aria-label=\"Close dialog\"`.";
|
|
24702
|
+
const noUninformativeAriaLabel = defineRule({
|
|
24703
|
+
id: "no-uninformative-aria-label",
|
|
24704
|
+
title: "Uninformative aria-label",
|
|
24705
|
+
severity: "warn",
|
|
24706
|
+
recommendation: "Name the action, not the element type: `aria-label=\"Search\"`, not `aria-label=\"icon\"` or `aria-label=\"button\"`.",
|
|
24707
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
24708
|
+
const ariaLabel = findJsxAttribute(node.attributes, "aria-label");
|
|
24709
|
+
if (!ariaLabel) return;
|
|
24710
|
+
const labelValue = getJsxPropStringValue(ariaLabel);
|
|
24711
|
+
if (labelValue === null) return;
|
|
24712
|
+
if (UNINFORMATIVE_LABELS.has(labelValue.trim().toLowerCase())) context.report({
|
|
24713
|
+
node: ariaLabel,
|
|
24714
|
+
message: MESSAGE$11
|
|
24715
|
+
});
|
|
24716
|
+
} })
|
|
24717
|
+
});
|
|
24718
|
+
//#endregion
|
|
24064
24719
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
24065
24720
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
24066
24721
|
"activedescendant",
|
|
@@ -25532,7 +26187,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
25532
26187
|
//#endregion
|
|
25533
26188
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
25534
26189
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
25535
|
-
const MESSAGE$
|
|
26190
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
25536
26191
|
const resolveSettings$7 = (settings) => {
|
|
25537
26192
|
const reactDoctor = settings?.["react-doctor"];
|
|
25538
26193
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -25566,7 +26221,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
25566
26221
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25567
26222
|
context.report({
|
|
25568
26223
|
node: node.callee,
|
|
25569
|
-
message: MESSAGE$
|
|
26224
|
+
message: MESSAGE$10
|
|
25570
26225
|
});
|
|
25571
26226
|
} };
|
|
25572
26227
|
}
|
|
@@ -26444,7 +27099,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
26444
27099
|
});
|
|
26445
27100
|
//#endregion
|
|
26446
27101
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
26447
|
-
const MESSAGE$
|
|
27102
|
+
const MESSAGE$9 = "Your users get no response from `onDoubleClick` in Preact core, where it never fires, so use `onDblClick` instead, which matches the DOM event name.";
|
|
26448
27103
|
const preactPreferOndblclick = defineRule({
|
|
26449
27104
|
id: "preact-prefer-ondblclick",
|
|
26450
27105
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -26459,7 +27114,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
26459
27114
|
if (!onDoubleClickAttribute) return;
|
|
26460
27115
|
context.report({
|
|
26461
27116
|
node: onDoubleClickAttribute,
|
|
26462
|
-
message: MESSAGE$
|
|
27117
|
+
message: MESSAGE$9
|
|
26463
27118
|
});
|
|
26464
27119
|
} })
|
|
26465
27120
|
});
|
|
@@ -26499,6 +27154,42 @@ const preactPreferOninput = defineRule({
|
|
|
26499
27154
|
} })
|
|
26500
27155
|
});
|
|
26501
27156
|
//#endregion
|
|
27157
|
+
//#region src/plugin/rules/design/prefer-dvh-over-vh.ts
|
|
27158
|
+
const FULL_VIEWPORT_HEIGHT_CLASS = /(?:^|\s)(?:\w+:)*(?:min-)?h-(?:screen|\[100vh\])(?=$|[\s])/;
|
|
27159
|
+
const HEIGHT_KEYS = new Set(["height", "minHeight"]);
|
|
27160
|
+
const MESSAGE$8 = "`100vh` is taller than the visible viewport on mobile (it ignores the browser's dynamic toolbars), so full-height layouts get clipped. Use the dynamic-viewport unit: `h-dvh` / `min-h-dvh` (or `100dvh`).";
|
|
27161
|
+
const preferDvhOverVh = defineRule({
|
|
27162
|
+
id: "prefer-dvh-over-vh",
|
|
27163
|
+
title: "Use dvh instead of vh for full height",
|
|
27164
|
+
tags: ["design", "test-noise"],
|
|
27165
|
+
severity: "warn",
|
|
27166
|
+
requires: ["tailwind:3.4"],
|
|
27167
|
+
recommendation: "Prefer `dvh` over `vh` for full-height elements. `100vh` overflows under mobile browser chrome; `100dvh` tracks the visible viewport. (`h-dvh`/`min-h-dvh` need Tailwind 3.4+.)",
|
|
27168
|
+
create: (context) => ({
|
|
27169
|
+
JSXAttribute(node) {
|
|
27170
|
+
const expression = getInlineStyleExpression(node);
|
|
27171
|
+
if (!expression) return;
|
|
27172
|
+
for (const property of expression.properties ?? []) {
|
|
27173
|
+
const key = getStylePropertyKey(property);
|
|
27174
|
+
if (!key || !HEIGHT_KEYS.has(key)) continue;
|
|
27175
|
+
const value = getStylePropertyStringValue(property);
|
|
27176
|
+
if (value && value.trim().toLowerCase() === "100vh") context.report({
|
|
27177
|
+
node: property,
|
|
27178
|
+
message: MESSAGE$8
|
|
27179
|
+
});
|
|
27180
|
+
}
|
|
27181
|
+
},
|
|
27182
|
+
JSXOpeningElement(node) {
|
|
27183
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27184
|
+
if (!classNameValue) return;
|
|
27185
|
+
if (FULL_VIEWPORT_HEIGHT_CLASS.test(classNameValue)) context.report({
|
|
27186
|
+
node,
|
|
27187
|
+
message: MESSAGE$8
|
|
27188
|
+
});
|
|
27189
|
+
}
|
|
27190
|
+
})
|
|
27191
|
+
});
|
|
27192
|
+
//#endregion
|
|
26502
27193
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
26503
27194
|
const preferDynamicImport = defineRule({
|
|
26504
27195
|
id: "prefer-dynamic-import",
|
|
@@ -27090,6 +27781,26 @@ const preferTagOverRole = defineRule({
|
|
|
27090
27781
|
} })
|
|
27091
27782
|
});
|
|
27092
27783
|
//#endregion
|
|
27784
|
+
//#region src/plugin/rules/design/prefer-truncate-shorthand.ts
|
|
27785
|
+
const HAS_OVERFLOW_HIDDEN = /(?:^|\s)overflow-hidden(?:$|\s)/;
|
|
27786
|
+
const HAS_TEXT_ELLIPSIS = /(?:^|\s)text-ellipsis(?:$|\s)/;
|
|
27787
|
+
const HAS_WHITESPACE_NOWRAP = /(?:^|\s)whitespace-nowrap(?:$|\s)/;
|
|
27788
|
+
const preferTruncateShorthand = defineRule({
|
|
27789
|
+
id: "prefer-truncate-shorthand",
|
|
27790
|
+
title: "Use truncate shorthand",
|
|
27791
|
+
tags: ["design", "test-noise"],
|
|
27792
|
+
severity: "warn",
|
|
27793
|
+
recommendation: "Replace `overflow-hidden text-ellipsis whitespace-nowrap` with the single Tailwind `truncate` utility, which sets all three.",
|
|
27794
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
27795
|
+
const classNameValue = getStringFromClassNameAttr(node);
|
|
27796
|
+
if (!classNameValue) return;
|
|
27797
|
+
if (HAS_OVERFLOW_HIDDEN.test(classNameValue) && HAS_TEXT_ELLIPSIS.test(classNameValue) && HAS_WHITESPACE_NOWRAP.test(classNameValue)) context.report({
|
|
27798
|
+
node,
|
|
27799
|
+
message: "`overflow-hidden text-ellipsis whitespace-nowrap` is exactly what the `truncate` utility does — collapse the three classes into `truncate`."
|
|
27800
|
+
});
|
|
27801
|
+
} })
|
|
27802
|
+
});
|
|
27803
|
+
//#endregion
|
|
27093
27804
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
27094
27805
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
27095
27806
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -34895,6 +35606,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34895
35606
|
}
|
|
34896
35607
|
});
|
|
34897
35608
|
//#endregion
|
|
35609
|
+
//#region src/plugin/utils/is-auth-guard-name.ts
|
|
35610
|
+
const SIGNED_IN_HEAD_TOKENS = new Set([
|
|
35611
|
+
"signed",
|
|
35612
|
+
"logged",
|
|
35613
|
+
"sign"
|
|
35614
|
+
]);
|
|
35615
|
+
const mergeSignedInTokens = (tokens) => {
|
|
35616
|
+
const mergedTokens = [];
|
|
35617
|
+
for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex += 1) {
|
|
35618
|
+
const currentToken = tokens[tokenIndex];
|
|
35619
|
+
if (SIGNED_IN_HEAD_TOKENS.has(currentToken) && tokens[tokenIndex + 1] === "in") {
|
|
35620
|
+
mergedTokens.push(`${currentToken}in`);
|
|
35621
|
+
tokenIndex += 1;
|
|
35622
|
+
continue;
|
|
35623
|
+
}
|
|
35624
|
+
mergedTokens.push(currentToken);
|
|
35625
|
+
}
|
|
35626
|
+
return mergedTokens;
|
|
35627
|
+
};
|
|
35628
|
+
const isAuthGuardName = (calleeName) => {
|
|
35629
|
+
const tokens = mergeSignedInTokens(tokenizeIdentifierWords(calleeName));
|
|
35630
|
+
if (tokens.length === 0) return false;
|
|
35631
|
+
let hasAssertiveVerb = false;
|
|
35632
|
+
let hasGetterVerb = false;
|
|
35633
|
+
let hasQualifier = false;
|
|
35634
|
+
let hasStrongNoun = false;
|
|
35635
|
+
let hasWeakNoun = false;
|
|
35636
|
+
for (const token of tokens) {
|
|
35637
|
+
if (AUTH_STRONG_TOKEN_PATTERN.test(token) || AUTH_STANDALONE_NOUN_TOKENS.has(token)) return true;
|
|
35638
|
+
if (AUTH_ASSERTIVE_VERB_TOKENS.has(token)) hasAssertiveVerb = true;
|
|
35639
|
+
if (AUTH_GETTER_VERB_TOKENS.has(token)) hasGetterVerb = true;
|
|
35640
|
+
if (AUTH_QUALIFIER_TOKENS.has(token)) hasQualifier = true;
|
|
35641
|
+
if (AUTH_STRONG_NOUN_TOKENS.has(token)) hasStrongNoun = true;
|
|
35642
|
+
if (AUTH_WEAK_NOUN_TOKENS.has(token)) hasWeakNoun = true;
|
|
35643
|
+
}
|
|
35644
|
+
if (hasAssertiveVerb && (hasStrongNoun || hasWeakNoun)) return true;
|
|
35645
|
+
if (hasGetterVerb && hasStrongNoun) return true;
|
|
35646
|
+
if (hasQualifier && hasWeakNoun) return true;
|
|
35647
|
+
return false;
|
|
35648
|
+
};
|
|
35649
|
+
//#endregion
|
|
34898
35650
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34899
35651
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34900
35652
|
if (!node) return false;
|
|
@@ -34937,9 +35689,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34937
35689
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34938
35690
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34939
35691
|
if (!calleeNode) return null;
|
|
34940
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35692
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35693
|
+
const calleeName = calleeNode.name;
|
|
35694
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35695
|
+
}
|
|
34941
35696
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34942
35697
|
const methodName = calleeNode.property.name;
|
|
35698
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34943
35699
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34944
35700
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34945
35701
|
return methodName;
|
|
@@ -38721,6 +39477,17 @@ const reactDoctorRules = [
|
|
|
38721
39477
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
38722
39478
|
}
|
|
38723
39479
|
},
|
|
39480
|
+
{
|
|
39481
|
+
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39482
|
+
id: "no-arbitrary-px-font-size",
|
|
39483
|
+
source: "react-doctor",
|
|
39484
|
+
originallyExternal: false,
|
|
39485
|
+
rule: {
|
|
39486
|
+
...noArbitraryPxFontSize,
|
|
39487
|
+
framework: "global",
|
|
39488
|
+
category: "Accessibility"
|
|
39489
|
+
}
|
|
39490
|
+
},
|
|
38724
39491
|
{
|
|
38725
39492
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
38726
39493
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -38780,6 +39547,18 @@ const reactDoctorRules = [
|
|
|
38780
39547
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
38781
39548
|
}
|
|
38782
39549
|
},
|
|
39550
|
+
{
|
|
39551
|
+
key: "react-doctor/no-autoplay-without-muted",
|
|
39552
|
+
id: "no-autoplay-without-muted",
|
|
39553
|
+
source: "react-doctor",
|
|
39554
|
+
originallyExternal: false,
|
|
39555
|
+
rule: {
|
|
39556
|
+
...noAutoplayWithoutMuted,
|
|
39557
|
+
framework: "global",
|
|
39558
|
+
category: "Accessibility",
|
|
39559
|
+
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39560
|
+
}
|
|
39561
|
+
},
|
|
38783
39562
|
{
|
|
38784
39563
|
key: "react-doctor/no-barrel-import",
|
|
38785
39564
|
id: "no-barrel-import",
|
|
@@ -38933,6 +39712,17 @@ const reactDoctorRules = [
|
|
|
38933
39712
|
category: "Maintainability"
|
|
38934
39713
|
}
|
|
38935
39714
|
},
|
|
39715
|
+
{
|
|
39716
|
+
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39717
|
+
id: "no-deprecated-tailwind-class",
|
|
39718
|
+
source: "react-doctor",
|
|
39719
|
+
originallyExternal: false,
|
|
39720
|
+
rule: {
|
|
39721
|
+
...noDeprecatedTailwindClass,
|
|
39722
|
+
framework: "global",
|
|
39723
|
+
category: "Maintainability"
|
|
39724
|
+
}
|
|
39725
|
+
},
|
|
38936
39726
|
{
|
|
38937
39727
|
key: "react-doctor/no-derived-state",
|
|
38938
39728
|
id: "no-derived-state",
|
|
@@ -39052,6 +39842,17 @@ const reactDoctorRules = [
|
|
|
39052
39842
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
39053
39843
|
}
|
|
39054
39844
|
},
|
|
39845
|
+
{
|
|
39846
|
+
key: "react-doctor/no-document-write",
|
|
39847
|
+
id: "no-document-write",
|
|
39848
|
+
source: "react-doctor",
|
|
39849
|
+
originallyExternal: false,
|
|
39850
|
+
rule: {
|
|
39851
|
+
...noDocumentWrite,
|
|
39852
|
+
framework: "global",
|
|
39853
|
+
category: "Performance"
|
|
39854
|
+
}
|
|
39855
|
+
},
|
|
39055
39856
|
{
|
|
39056
39857
|
key: "react-doctor/no-dynamic-import-path",
|
|
39057
39858
|
id: "no-dynamic-import-path",
|
|
@@ -39193,6 +39994,17 @@ const reactDoctorRules = [
|
|
|
39193
39994
|
category: "Performance"
|
|
39194
39995
|
}
|
|
39195
39996
|
},
|
|
39997
|
+
{
|
|
39998
|
+
key: "react-doctor/no-full-viewport-width",
|
|
39999
|
+
id: "no-full-viewport-width",
|
|
40000
|
+
source: "react-doctor",
|
|
40001
|
+
originallyExternal: false,
|
|
40002
|
+
rule: {
|
|
40003
|
+
...noFullViewportWidth,
|
|
40004
|
+
framework: "global",
|
|
40005
|
+
category: "Maintainability"
|
|
40006
|
+
}
|
|
40007
|
+
},
|
|
39196
40008
|
{
|
|
39197
40009
|
key: "react-doctor/no-generic-handler-names",
|
|
39198
40010
|
id: "no-generic-handler-names",
|
|
@@ -39432,6 +40244,17 @@ const reactDoctorRules = [
|
|
|
39432
40244
|
category: "Performance"
|
|
39433
40245
|
}
|
|
39434
40246
|
},
|
|
40247
|
+
{
|
|
40248
|
+
key: "react-doctor/no-low-contrast-inline-style",
|
|
40249
|
+
id: "no-low-contrast-inline-style",
|
|
40250
|
+
source: "react-doctor",
|
|
40251
|
+
originallyExternal: false,
|
|
40252
|
+
rule: {
|
|
40253
|
+
...noLowContrastInlineStyle,
|
|
40254
|
+
framework: "global",
|
|
40255
|
+
category: "Accessibility"
|
|
40256
|
+
}
|
|
40257
|
+
},
|
|
39435
40258
|
{
|
|
39436
40259
|
key: "react-doctor/no-many-boolean-props",
|
|
39437
40260
|
id: "no-many-boolean-props",
|
|
@@ -39709,6 +40532,17 @@ const reactDoctorRules = [
|
|
|
39709
40532
|
category: "Maintainability"
|
|
39710
40533
|
}
|
|
39711
40534
|
},
|
|
40535
|
+
{
|
|
40536
|
+
key: "react-doctor/no-redundant-display-class",
|
|
40537
|
+
id: "no-redundant-display-class",
|
|
40538
|
+
source: "react-doctor",
|
|
40539
|
+
originallyExternal: false,
|
|
40540
|
+
rule: {
|
|
40541
|
+
...noRedundantDisplayClass,
|
|
40542
|
+
framework: "global",
|
|
40543
|
+
category: "Maintainability"
|
|
40544
|
+
}
|
|
40545
|
+
},
|
|
39712
40546
|
{
|
|
39713
40547
|
key: "react-doctor/no-redundant-roles",
|
|
39714
40548
|
id: "no-redundant-roles",
|
|
@@ -39861,6 +40695,18 @@ const reactDoctorRules = [
|
|
|
39861
40695
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39862
40696
|
}
|
|
39863
40697
|
},
|
|
40698
|
+
{
|
|
40699
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40700
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40701
|
+
source: "react-doctor",
|
|
40702
|
+
originallyExternal: false,
|
|
40703
|
+
rule: {
|
|
40704
|
+
...noStringFalseOnBooleanAttribute,
|
|
40705
|
+
framework: "global",
|
|
40706
|
+
category: "Bugs",
|
|
40707
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40708
|
+
}
|
|
40709
|
+
},
|
|
39864
40710
|
{
|
|
39865
40711
|
key: "react-doctor/no-string-refs",
|
|
39866
40712
|
id: "no-string-refs",
|
|
@@ -39873,6 +40719,51 @@ const reactDoctorRules = [
|
|
|
39873
40719
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39874
40720
|
}
|
|
39875
40721
|
},
|
|
40722
|
+
{
|
|
40723
|
+
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40724
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
40725
|
+
source: "react-doctor",
|
|
40726
|
+
originallyExternal: false,
|
|
40727
|
+
rule: {
|
|
40728
|
+
...noSvgCurrentcolorWithFillClass,
|
|
40729
|
+
framework: "global",
|
|
40730
|
+
category: "Maintainability"
|
|
40731
|
+
}
|
|
40732
|
+
},
|
|
40733
|
+
{
|
|
40734
|
+
key: "react-doctor/no-sync-xhr",
|
|
40735
|
+
id: "no-sync-xhr",
|
|
40736
|
+
source: "react-doctor",
|
|
40737
|
+
originallyExternal: false,
|
|
40738
|
+
rule: {
|
|
40739
|
+
...noSyncXhr,
|
|
40740
|
+
framework: "global",
|
|
40741
|
+
category: "Performance"
|
|
40742
|
+
}
|
|
40743
|
+
},
|
|
40744
|
+
{
|
|
40745
|
+
key: "react-doctor/no-tailwind-layout-transition",
|
|
40746
|
+
id: "no-tailwind-layout-transition",
|
|
40747
|
+
source: "react-doctor",
|
|
40748
|
+
originallyExternal: false,
|
|
40749
|
+
rule: {
|
|
40750
|
+
...noTailwindLayoutTransition,
|
|
40751
|
+
framework: "global",
|
|
40752
|
+
category: "Performance"
|
|
40753
|
+
}
|
|
40754
|
+
},
|
|
40755
|
+
{
|
|
40756
|
+
key: "react-doctor/no-target-blank-without-rel",
|
|
40757
|
+
id: "no-target-blank-without-rel",
|
|
40758
|
+
source: "react-doctor",
|
|
40759
|
+
originallyExternal: false,
|
|
40760
|
+
rule: {
|
|
40761
|
+
...noTargetBlankWithoutRel,
|
|
40762
|
+
framework: "global",
|
|
40763
|
+
category: "Accessibility",
|
|
40764
|
+
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40765
|
+
}
|
|
40766
|
+
},
|
|
39876
40767
|
{
|
|
39877
40768
|
key: "react-doctor/no-this-in-sfc",
|
|
39878
40769
|
id: "no-this-in-sfc",
|
|
@@ -39942,6 +40833,18 @@ const reactDoctorRules = [
|
|
|
39942
40833
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
39943
40834
|
}
|
|
39944
40835
|
},
|
|
40836
|
+
{
|
|
40837
|
+
key: "react-doctor/no-uninformative-aria-label",
|
|
40838
|
+
id: "no-uninformative-aria-label",
|
|
40839
|
+
source: "react-doctor",
|
|
40840
|
+
originallyExternal: false,
|
|
40841
|
+
rule: {
|
|
40842
|
+
...noUninformativeAriaLabel,
|
|
40843
|
+
framework: "global",
|
|
40844
|
+
category: "Accessibility",
|
|
40845
|
+
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40846
|
+
}
|
|
40847
|
+
},
|
|
39945
40848
|
{
|
|
39946
40849
|
key: "react-doctor/no-unknown-property",
|
|
39947
40850
|
id: "no-unknown-property",
|
|
@@ -40151,6 +41054,17 @@ const reactDoctorRules = [
|
|
|
40151
41054
|
category: "Bugs"
|
|
40152
41055
|
}
|
|
40153
41056
|
},
|
|
41057
|
+
{
|
|
41058
|
+
key: "react-doctor/prefer-dvh-over-vh",
|
|
41059
|
+
id: "prefer-dvh-over-vh",
|
|
41060
|
+
source: "react-doctor",
|
|
41061
|
+
originallyExternal: false,
|
|
41062
|
+
rule: {
|
|
41063
|
+
...preferDvhOverVh,
|
|
41064
|
+
framework: "global",
|
|
41065
|
+
category: "Maintainability"
|
|
41066
|
+
}
|
|
41067
|
+
},
|
|
40154
41068
|
{
|
|
40155
41069
|
key: "react-doctor/prefer-dynamic-import",
|
|
40156
41070
|
id: "prefer-dynamic-import",
|
|
@@ -40255,6 +41169,17 @@ const reactDoctorRules = [
|
|
|
40255
41169
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
40256
41170
|
}
|
|
40257
41171
|
},
|
|
41172
|
+
{
|
|
41173
|
+
key: "react-doctor/prefer-truncate-shorthand",
|
|
41174
|
+
id: "prefer-truncate-shorthand",
|
|
41175
|
+
source: "react-doctor",
|
|
41176
|
+
originallyExternal: false,
|
|
41177
|
+
rule: {
|
|
41178
|
+
...preferTruncateShorthand,
|
|
41179
|
+
framework: "global",
|
|
41180
|
+
category: "Maintainability"
|
|
41181
|
+
}
|
|
41182
|
+
},
|
|
40258
41183
|
{
|
|
40259
41184
|
key: "react-doctor/prefer-use-effect-event",
|
|
40260
41185
|
id: "prefer-use-effect-event",
|