oxlint-plugin-react-doctor 0.5.6-dev.15238de → 0.5.6-dev.431e515
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 +945 -21
- package/dist/index.js +1438 -219
- 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
|
});
|
|
@@ -3085,7 +3085,7 @@ const artifactBaasAuthoritySurface = defineRule({
|
|
|
3085
3085
|
scan: scanByPattern({
|
|
3086
3086
|
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3087
3087
|
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3088
|
-
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore
|
|
3088
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b|\bcreateClient\b[\s\S]{0,700}\b(?:supabase|SUPABASE_URL)\b|\b(?:supabase|SUPABASE_URL)\b[\s\S]{0,700}\bcreateClient\b/i],
|
|
3089
3089
|
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3090
3090
|
})
|
|
3091
3091
|
});
|
|
@@ -3104,6 +3104,76 @@ const AUTH_FUNCTION_NAMES = new Set([
|
|
|
3104
3104
|
"getAuth",
|
|
3105
3105
|
"validateSession"
|
|
3106
3106
|
]);
|
|
3107
|
+
const AUTH_STRONG_TOKEN_PATTERN = /^auth(?:n|z|ed|enticate[ds]?|enticating|entication|orize[ds]?|orizing|orization|orizer)?$/;
|
|
3108
|
+
const AUTH_STANDALONE_NOUN_TOKENS = new Set([
|
|
3109
|
+
"signedin",
|
|
3110
|
+
"loggedin",
|
|
3111
|
+
"signin"
|
|
3112
|
+
]);
|
|
3113
|
+
const AUTH_ASSERTIVE_VERB_TOKENS = new Set([
|
|
3114
|
+
"require",
|
|
3115
|
+
"ensure",
|
|
3116
|
+
"assert",
|
|
3117
|
+
"verify",
|
|
3118
|
+
"validate",
|
|
3119
|
+
"check",
|
|
3120
|
+
"protect",
|
|
3121
|
+
"enforce",
|
|
3122
|
+
"guard",
|
|
3123
|
+
"gate",
|
|
3124
|
+
"restrict",
|
|
3125
|
+
"is",
|
|
3126
|
+
"has",
|
|
3127
|
+
"can",
|
|
3128
|
+
"must"
|
|
3129
|
+
]);
|
|
3130
|
+
const AUTH_GETTER_VERB_TOKENS = new Set([
|
|
3131
|
+
"get",
|
|
3132
|
+
"fetch",
|
|
3133
|
+
"load",
|
|
3134
|
+
"read",
|
|
3135
|
+
"resolve",
|
|
3136
|
+
"retrieve",
|
|
3137
|
+
"use"
|
|
3138
|
+
]);
|
|
3139
|
+
const AUTH_QUALIFIER_TOKENS = new Set([
|
|
3140
|
+
"current",
|
|
3141
|
+
"my",
|
|
3142
|
+
"own"
|
|
3143
|
+
]);
|
|
3144
|
+
const AUTH_STRONG_NOUN_TOKENS = new Set([
|
|
3145
|
+
"session",
|
|
3146
|
+
"sessions",
|
|
3147
|
+
"login",
|
|
3148
|
+
"admin",
|
|
3149
|
+
"admins",
|
|
3150
|
+
"superadmin",
|
|
3151
|
+
"superuser",
|
|
3152
|
+
"role",
|
|
3153
|
+
"roles",
|
|
3154
|
+
"permission",
|
|
3155
|
+
"permissions",
|
|
3156
|
+
"jwt",
|
|
3157
|
+
"identity",
|
|
3158
|
+
"principal",
|
|
3159
|
+
"credential",
|
|
3160
|
+
"credentials"
|
|
3161
|
+
]);
|
|
3162
|
+
const AUTH_WEAK_NOUN_TOKENS = new Set([
|
|
3163
|
+
"user",
|
|
3164
|
+
"users",
|
|
3165
|
+
"account",
|
|
3166
|
+
"accounts",
|
|
3167
|
+
"token",
|
|
3168
|
+
"tokens",
|
|
3169
|
+
"access",
|
|
3170
|
+
"me",
|
|
3171
|
+
"viewer",
|
|
3172
|
+
"caller",
|
|
3173
|
+
"subject",
|
|
3174
|
+
"scope",
|
|
3175
|
+
"scopes"
|
|
3176
|
+
]);
|
|
3107
3177
|
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3108
3178
|
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3109
3179
|
const SECRET_PATTERNS = [
|
|
@@ -4201,6 +4271,58 @@ const asyncParallel = defineRule({
|
|
|
4201
4271
|
}
|
|
4202
4272
|
});
|
|
4203
4273
|
//#endregion
|
|
4274
|
+
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
4275
|
+
const MESSAGE$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.";
|
|
4276
|
+
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
4277
|
+
const STORAGE_GLOBALS = new Set([
|
|
4278
|
+
"window",
|
|
4279
|
+
"globalThis",
|
|
4280
|
+
"self"
|
|
4281
|
+
]);
|
|
4282
|
+
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
4283
|
+
const isWebStorageObject = (node) => {
|
|
4284
|
+
if (isNodeOfType(node, "Identifier")) return STORAGE_NAMES.has(node.name);
|
|
4285
|
+
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
4286
|
+
return false;
|
|
4287
|
+
};
|
|
4288
|
+
const staticMemberName = (member) => {
|
|
4289
|
+
if (!member.computed && isNodeOfType(member.property, "Identifier")) return member.property.name;
|
|
4290
|
+
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
4291
|
+
return null;
|
|
4292
|
+
};
|
|
4293
|
+
const authTokenInWebStorage = defineRule({
|
|
4294
|
+
id: "auth-token-in-web-storage",
|
|
4295
|
+
title: "Auth token in web storage",
|
|
4296
|
+
severity: "warn",
|
|
4297
|
+
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4298
|
+
create: (context) => ({
|
|
4299
|
+
CallExpression(node) {
|
|
4300
|
+
const callee = node.callee;
|
|
4301
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
4302
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem") return;
|
|
4303
|
+
if (!isWebStorageObject(callee.object)) return;
|
|
4304
|
+
const keyArgument = node.arguments?.[0];
|
|
4305
|
+
if (!keyArgument || !isNodeOfType(keyArgument, "Literal") || typeof keyArgument.value !== "string") return;
|
|
4306
|
+
if (!SENSITIVE_KEY_PATTERN.test(keyArgument.value)) return;
|
|
4307
|
+
context.report({
|
|
4308
|
+
node,
|
|
4309
|
+
message: MESSAGE$62
|
|
4310
|
+
});
|
|
4311
|
+
},
|
|
4312
|
+
AssignmentExpression(node) {
|
|
4313
|
+
const target = node.left;
|
|
4314
|
+
if (!isNodeOfType(target, "MemberExpression")) return;
|
|
4315
|
+
if (!isWebStorageObject(target.object)) return;
|
|
4316
|
+
const propertyName = staticMemberName(target);
|
|
4317
|
+
if (!propertyName || !SENSITIVE_KEY_PATTERN.test(propertyName)) return;
|
|
4318
|
+
context.report({
|
|
4319
|
+
node: target,
|
|
4320
|
+
message: MESSAGE$62
|
|
4321
|
+
});
|
|
4322
|
+
}
|
|
4323
|
+
})
|
|
4324
|
+
});
|
|
4325
|
+
//#endregion
|
|
4204
4326
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
4205
4327
|
const buildMessage$25 = (value) => `Users who rely on autofill can't fill this field because \`${value}\` isn't a known token, so use a valid \`autoComplete\` token.`;
|
|
4206
4328
|
const AUTOFILL_TOKENS = new Set([
|
|
@@ -4572,7 +4694,7 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
4572
4694
|
//#endregion
|
|
4573
4695
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
4574
4696
|
const PRESENTATION_ROLES$1 = new Set(["presentation", "none"]);
|
|
4575
|
-
const MESSAGE$
|
|
4697
|
+
const MESSAGE$61 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
4576
4698
|
const KEY_HANDLERS = [
|
|
4577
4699
|
"onKeyUp",
|
|
4578
4700
|
"onKeyDown",
|
|
@@ -4604,7 +4726,7 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4604
4726
|
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
4605
4727
|
context.report({
|
|
4606
4728
|
node: node.name,
|
|
4607
|
-
message: MESSAGE$
|
|
4729
|
+
message: MESSAGE$61
|
|
4608
4730
|
});
|
|
4609
4731
|
} };
|
|
4610
4732
|
}
|
|
@@ -4719,7 +4841,7 @@ const isReactComponentName = (name) => {
|
|
|
4719
4841
|
};
|
|
4720
4842
|
//#endregion
|
|
4721
4843
|
//#region src/plugin/rules/a11y/control-has-associated-label.ts
|
|
4722
|
-
const MESSAGE$
|
|
4844
|
+
const MESSAGE$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`.";
|
|
4723
4845
|
const DEFAULT_IGNORE_ELEMENTS = ["link", "canvas"];
|
|
4724
4846
|
const DEFAULT_LABELLING_PROPS = [
|
|
4725
4847
|
"alt",
|
|
@@ -4880,7 +5002,7 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4880
5002
|
for (const child of node.children) if (checkChildForLabel(child, 1, checkContext)) return;
|
|
4881
5003
|
context.report({
|
|
4882
5004
|
node: opening,
|
|
4883
|
-
message: MESSAGE$
|
|
5005
|
+
message: MESSAGE$60
|
|
4884
5006
|
});
|
|
4885
5007
|
} };
|
|
4886
5008
|
}
|
|
@@ -5009,6 +5131,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
5009
5131
|
return findings;
|
|
5010
5132
|
}
|
|
5011
5133
|
});
|
|
5134
|
+
const WCAG_CONTRAST_NORMAL_MIN = 4.5;
|
|
5012
5135
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
5013
5136
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
5014
5137
|
"continue",
|
|
@@ -5306,6 +5429,38 @@ const noVagueButtonLabel = defineRule({
|
|
|
5306
5429
|
} })
|
|
5307
5430
|
});
|
|
5308
5431
|
//#endregion
|
|
5432
|
+
//#region src/plugin/utils/has-jsx-spread-attribute.ts
|
|
5433
|
+
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
5434
|
+
//#endregion
|
|
5435
|
+
//#region src/plugin/rules/a11y/dialog-has-accessible-name.ts
|
|
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.";
|
|
5437
|
+
const DIALOG_ROLES = new Set(["dialog", "alertdialog"]);
|
|
5438
|
+
const NAME_PROVIDING_ATTRIBUTES = [
|
|
5439
|
+
"aria-label",
|
|
5440
|
+
"aria-labelledby",
|
|
5441
|
+
"title"
|
|
5442
|
+
];
|
|
5443
|
+
const dialogHasAccessibleName = defineRule({
|
|
5444
|
+
id: "dialog-has-accessible-name",
|
|
5445
|
+
title: "Dialog without accessible name",
|
|
5446
|
+
severity: "warn",
|
|
5447
|
+
recommendation: "Give every `<dialog>` / `role=\"dialog\"` an accessible name with `aria-label` or `aria-labelledby` (referencing the dialog's title element).",
|
|
5448
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
5449
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
5450
|
+
const tagName = node.name.name;
|
|
5451
|
+
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
5452
|
+
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
5453
|
+
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
5454
|
+
if (!(tagName === "dialog" || roleValue !== null && DIALOG_ROLES.has(roleValue))) return;
|
|
5455
|
+
if (hasJsxSpreadAttribute(node.attributes)) return;
|
|
5456
|
+
if (NAME_PROVIDING_ATTRIBUTES.some((attribute) => hasJsxPropIgnoreCase(node.attributes, attribute))) return;
|
|
5457
|
+
context.report({
|
|
5458
|
+
node: node.name,
|
|
5459
|
+
message: MESSAGE$59
|
|
5460
|
+
});
|
|
5461
|
+
} })
|
|
5462
|
+
});
|
|
5463
|
+
//#endregion
|
|
5309
5464
|
//#region src/plugin/utils/is-es5-component.ts
|
|
5310
5465
|
const PRAGMA$2 = "React";
|
|
5311
5466
|
const CREATE_CLASS = "createReactClass";
|
|
@@ -5340,7 +5495,7 @@ const isEs6Component = (node) => {
|
|
|
5340
5495
|
};
|
|
5341
5496
|
//#endregion
|
|
5342
5497
|
//#region src/plugin/rules/react-builtins/display-name.ts
|
|
5343
|
-
const MESSAGE$
|
|
5498
|
+
const MESSAGE$58 = "This component shows up as Anonymous in React DevTools because it has no `displayName`.";
|
|
5344
5499
|
const DEFAULT_ADDITIONAL_HOCS = [
|
|
5345
5500
|
"observer",
|
|
5346
5501
|
"lazy",
|
|
@@ -5543,7 +5698,7 @@ const displayName = defineRule({
|
|
|
5543
5698
|
const reportAt = (node) => {
|
|
5544
5699
|
context.report({
|
|
5545
5700
|
node,
|
|
5546
|
-
message: MESSAGE$
|
|
5701
|
+
message: MESSAGE$58
|
|
5547
5702
|
});
|
|
5548
5703
|
};
|
|
5549
5704
|
return {
|
|
@@ -7691,7 +7846,7 @@ const forbidElements = defineRule({
|
|
|
7691
7846
|
});
|
|
7692
7847
|
//#endregion
|
|
7693
7848
|
//#region src/plugin/rules/react-builtins/forward-ref-uses-ref.ts
|
|
7694
|
-
const MESSAGE$
|
|
7849
|
+
const MESSAGE$57 = "The parent can't reach this component's node because the `forwardRef` wrapper ignores `ref`.";
|
|
7695
7850
|
const forwardRefUsesRef = defineRule({
|
|
7696
7851
|
id: "forward-ref-uses-ref",
|
|
7697
7852
|
title: "forwardRef without ref parameter",
|
|
@@ -7711,7 +7866,7 @@ const forwardRefUsesRef = defineRule({
|
|
|
7711
7866
|
if (isNodeOfType(onlyParam, "RestElement")) return;
|
|
7712
7867
|
context.report({
|
|
7713
7868
|
node: inner,
|
|
7714
|
-
message: MESSAGE$
|
|
7869
|
+
message: MESSAGE$57
|
|
7715
7870
|
});
|
|
7716
7871
|
} })
|
|
7717
7872
|
});
|
|
@@ -7748,7 +7903,7 @@ const gitProviderUrlInjectionRisk = defineRule({
|
|
|
7748
7903
|
});
|
|
7749
7904
|
//#endregion
|
|
7750
7905
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7751
|
-
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`.";
|
|
7752
7907
|
const DEFAULT_HEADING_TAGS = [
|
|
7753
7908
|
"h1",
|
|
7754
7909
|
"h2",
|
|
@@ -7781,7 +7936,7 @@ const headingHasContent = defineRule({
|
|
|
7781
7936
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
7782
7937
|
context.report({
|
|
7783
7938
|
node,
|
|
7784
|
-
message: MESSAGE$
|
|
7939
|
+
message: MESSAGE$56
|
|
7785
7940
|
});
|
|
7786
7941
|
} };
|
|
7787
7942
|
}
|
|
@@ -7919,7 +8074,7 @@ const hooksNoNanInDeps = defineRule({
|
|
|
7919
8074
|
});
|
|
7920
8075
|
//#endregion
|
|
7921
8076
|
//#region src/plugin/rules/a11y/html-has-lang.ts
|
|
7922
|
-
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`.";
|
|
7923
8078
|
const resolveSettings$38 = (settings) => {
|
|
7924
8079
|
const reactDoctor = settings?.["react-doctor"];
|
|
7925
8080
|
return { htmlTags: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.htmlHasLang ?? {} : {}).htmlTags ?? ["html"] };
|
|
@@ -7967,7 +8122,7 @@ const htmlHasLang = defineRule({
|
|
|
7967
8122
|
if (!lang) {
|
|
7968
8123
|
context.report({
|
|
7969
8124
|
node: node.name,
|
|
7970
|
-
message: MESSAGE$
|
|
8125
|
+
message: MESSAGE$55
|
|
7971
8126
|
});
|
|
7972
8127
|
return;
|
|
7973
8128
|
}
|
|
@@ -7975,13 +8130,13 @@ const htmlHasLang = defineRule({
|
|
|
7975
8130
|
if (verdict === "missing" || verdict === "empty") {
|
|
7976
8131
|
context.report({
|
|
7977
8132
|
node: lang,
|
|
7978
|
-
message: MESSAGE$
|
|
8133
|
+
message: MESSAGE$55
|
|
7979
8134
|
});
|
|
7980
8135
|
return;
|
|
7981
8136
|
}
|
|
7982
8137
|
if (hasSpread && !lang) context.report({
|
|
7983
8138
|
node: node.name,
|
|
7984
|
-
message: MESSAGE$
|
|
8139
|
+
message: MESSAGE$55
|
|
7985
8140
|
});
|
|
7986
8141
|
} };
|
|
7987
8142
|
}
|
|
@@ -8195,7 +8350,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
8195
8350
|
});
|
|
8196
8351
|
//#endregion
|
|
8197
8352
|
//#region src/plugin/rules/a11y/iframe-has-title.ts
|
|
8198
|
-
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.";
|
|
8199
8354
|
const evaluateTitleValue = (value) => {
|
|
8200
8355
|
if (!value) return "missing";
|
|
8201
8356
|
if (isNodeOfType(value, "Literal")) {
|
|
@@ -8235,14 +8390,14 @@ const iframeHasTitle = defineRule({
|
|
|
8235
8390
|
if (!titleAttr) {
|
|
8236
8391
|
if (hasSpread || tag === "iframe") context.report({
|
|
8237
8392
|
node: node.name,
|
|
8238
|
-
message: MESSAGE$
|
|
8393
|
+
message: MESSAGE$54
|
|
8239
8394
|
});
|
|
8240
8395
|
return;
|
|
8241
8396
|
}
|
|
8242
8397
|
const verdict = evaluateTitleValue(titleAttr.value);
|
|
8243
8398
|
if (verdict === "missing" || verdict === "empty") context.report({
|
|
8244
8399
|
node: titleAttr,
|
|
8245
|
-
message: MESSAGE$
|
|
8400
|
+
message: MESSAGE$54
|
|
8246
8401
|
});
|
|
8247
8402
|
} })
|
|
8248
8403
|
});
|
|
@@ -8346,7 +8501,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
8346
8501
|
});
|
|
8347
8502
|
//#endregion
|
|
8348
8503
|
//#region src/plugin/rules/a11y/img-redundant-alt.ts
|
|
8349
|
-
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.";
|
|
8350
8505
|
const DEFAULT_COMPONENTS = ["img"];
|
|
8351
8506
|
const DEFAULT_REDUNDANT_WORDS = [
|
|
8352
8507
|
"image",
|
|
@@ -8411,7 +8566,7 @@ const imgRedundantAlt = defineRule({
|
|
|
8411
8566
|
if (!altAttribute) return;
|
|
8412
8567
|
if (altValueRedundant(altAttribute, settings.words)) context.report({
|
|
8413
8568
|
node: altAttribute,
|
|
8414
|
-
message: MESSAGE$
|
|
8569
|
+
message: MESSAGE$53
|
|
8415
8570
|
});
|
|
8416
8571
|
} };
|
|
8417
8572
|
}
|
|
@@ -10768,7 +10923,7 @@ const jsxMaxDepth = defineRule({
|
|
|
10768
10923
|
});
|
|
10769
10924
|
//#endregion
|
|
10770
10925
|
//#region src/plugin/rules/react-builtins/jsx-no-comment-textnodes.ts
|
|
10771
|
-
const MESSAGE$
|
|
10926
|
+
const MESSAGE$52 = "Your users see this comment as text on the page because `//` & `/*` aren't hidden in JSX.";
|
|
10772
10927
|
const LITERAL_TEXT_TAGS = new Set([
|
|
10773
10928
|
"code",
|
|
10774
10929
|
"pre",
|
|
@@ -10804,7 +10959,7 @@ const jsxNoCommentTextnodes = defineRule({
|
|
|
10804
10959
|
if (isInsideLiteralTextTag(node)) return;
|
|
10805
10960
|
context.report({
|
|
10806
10961
|
node,
|
|
10807
|
-
message: MESSAGE$
|
|
10962
|
+
message: MESSAGE$52
|
|
10808
10963
|
});
|
|
10809
10964
|
} })
|
|
10810
10965
|
});
|
|
@@ -10835,7 +10990,7 @@ const isInsideFunctionScope = (node) => {
|
|
|
10835
10990
|
};
|
|
10836
10991
|
//#endregion
|
|
10837
10992
|
//#region src/plugin/rules/react-builtins/jsx-no-constructed-context-values.ts
|
|
10838
|
-
const MESSAGE$
|
|
10993
|
+
const MESSAGE$51 = "Every reader of this context redraws on each render because you build its `value` inline.";
|
|
10839
10994
|
const CONTEXT_MODULES$1 = [
|
|
10840
10995
|
"react",
|
|
10841
10996
|
"use-context-selector",
|
|
@@ -10933,7 +11088,7 @@ const jsxNoConstructedContextValues = defineRule({
|
|
|
10933
11088
|
if (!isConstructedValue(innerExpression)) continue;
|
|
10934
11089
|
context.report({
|
|
10935
11090
|
node: attribute,
|
|
10936
|
-
message: MESSAGE$
|
|
11091
|
+
message: MESSAGE$51
|
|
10937
11092
|
});
|
|
10938
11093
|
}
|
|
10939
11094
|
}
|
|
@@ -11019,7 +11174,7 @@ const isJsxAttributeOnIntrinsicHtmlElement = (attribute) => {
|
|
|
11019
11174
|
};
|
|
11020
11175
|
//#endregion
|
|
11021
11176
|
//#region src/plugin/rules/react-builtins/jsx-no-jsx-as-prop.ts
|
|
11022
|
-
const MESSAGE$
|
|
11177
|
+
const MESSAGE$50 = "This child redraws every render because the prop gets brand new JSX each time.";
|
|
11023
11178
|
const KNOWN_SLOT_PROP_NAMES = new Set([
|
|
11024
11179
|
"icon",
|
|
11025
11180
|
"Icon",
|
|
@@ -11288,7 +11443,7 @@ const jsxNoJsxAsProp = defineRule({
|
|
|
11288
11443
|
if (!isJsxProducingExpression(expressionNode) && !followsRenderLocalJsxBinding(expressionNode, node)) return;
|
|
11289
11444
|
context.report({
|
|
11290
11445
|
node,
|
|
11291
|
-
message: MESSAGE$
|
|
11446
|
+
message: MESSAGE$50
|
|
11292
11447
|
});
|
|
11293
11448
|
}
|
|
11294
11449
|
};
|
|
@@ -11576,7 +11731,7 @@ const DATA_ARRAY_PROP_SUFFIXES = [
|
|
|
11576
11731
|
];
|
|
11577
11732
|
//#endregion
|
|
11578
11733
|
//#region src/plugin/rules/react-builtins/jsx-no-new-array-as-prop.ts
|
|
11579
|
-
const MESSAGE$
|
|
11734
|
+
const MESSAGE$49 = "This child redraws every render because the prop gets a brand new array each time.";
|
|
11580
11735
|
const isDataArrayPropName = (propName) => {
|
|
11581
11736
|
if (DATA_ARRAY_PROP_NAMES.has(propName)) return true;
|
|
11582
11737
|
for (const suffix of DATA_ARRAY_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -11660,7 +11815,7 @@ const jsxNoNewArrayAsProp = defineRule({
|
|
|
11660
11815
|
if (!isArrayProducingExpression(expressionNode) && !followsRenderLocalArrayBinding(expressionNode, node)) return;
|
|
11661
11816
|
context.report({
|
|
11662
11817
|
node,
|
|
11663
|
-
message: MESSAGE$
|
|
11818
|
+
message: MESSAGE$49
|
|
11664
11819
|
});
|
|
11665
11820
|
}
|
|
11666
11821
|
};
|
|
@@ -11918,7 +12073,7 @@ const SAFE_RECEIVER_NAMES = new Set([
|
|
|
11918
12073
|
]);
|
|
11919
12074
|
//#endregion
|
|
11920
12075
|
//#region src/plugin/rules/react-builtins/jsx-no-new-function-as-prop.ts
|
|
11921
|
-
const MESSAGE$
|
|
12076
|
+
const MESSAGE$48 = "This child redraws every render because the prop gets a brand new function each time.";
|
|
11922
12077
|
const isAccessorPredicateName = (propName) => {
|
|
11923
12078
|
for (const prefix of ACCESSOR_PREDICATE_PREFIXES) {
|
|
11924
12079
|
if (propName.length <= prefix.length) continue;
|
|
@@ -12124,7 +12279,7 @@ const jsxNoNewFunctionAsProp = defineRule({
|
|
|
12124
12279
|
if (!isFunctionProducingExpression(expressionNode) && !followsRenderLocalFunctionBinding(expressionNode, node)) return;
|
|
12125
12280
|
context.report({
|
|
12126
12281
|
node,
|
|
12127
|
-
message: MESSAGE$
|
|
12282
|
+
message: MESSAGE$48
|
|
12128
12283
|
});
|
|
12129
12284
|
}
|
|
12130
12285
|
};
|
|
@@ -12344,7 +12499,7 @@ const CONFIG_OBJECT_PROP_SUFFIXES = [
|
|
|
12344
12499
|
];
|
|
12345
12500
|
//#endregion
|
|
12346
12501
|
//#region src/plugin/rules/react-builtins/jsx-no-new-object-as-prop.ts
|
|
12347
|
-
const MESSAGE$
|
|
12502
|
+
const MESSAGE$47 = "This child redraws every render because the prop gets a brand new object each time.";
|
|
12348
12503
|
const isConfigObjectPropName = (propName) => {
|
|
12349
12504
|
if (CONFIG_OBJECT_PROP_NAMES.has(propName)) return true;
|
|
12350
12505
|
for (const suffix of CONFIG_OBJECT_PROP_SUFFIXES) if (propName.length > suffix.length && propName.endsWith(suffix)) return true;
|
|
@@ -12432,7 +12587,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12432
12587
|
if (!isObjectProducingExpression(expressionNode) && !followsRenderLocalObjectBinding(expressionNode, node)) return;
|
|
12433
12588
|
context.report({
|
|
12434
12589
|
node,
|
|
12435
|
-
message: MESSAGE$
|
|
12590
|
+
message: MESSAGE$47
|
|
12436
12591
|
});
|
|
12437
12592
|
}
|
|
12438
12593
|
};
|
|
@@ -12440,7 +12595,7 @@ const jsxNoNewObjectAsProp = defineRule({
|
|
|
12440
12595
|
});
|
|
12441
12596
|
//#endregion
|
|
12442
12597
|
//#region src/plugin/rules/react-builtins/jsx-no-script-url.ts
|
|
12443
|
-
const MESSAGE$
|
|
12598
|
+
const MESSAGE$46 = "A `javascript:` URL is an XSS hole that runs injected input as code.";
|
|
12444
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;
|
|
12445
12600
|
const resolveSettings$28 = (settings) => {
|
|
12446
12601
|
const reactDoctor = settings?.["react-doctor"];
|
|
@@ -12481,7 +12636,7 @@ const jsxNoScriptUrl = defineRule({
|
|
|
12481
12636
|
if (!value || !isNodeOfType(value, "Literal") || typeof value.value !== "string") continue;
|
|
12482
12637
|
if (JAVASCRIPT_URL_PATTERN.test(value.value)) context.report({
|
|
12483
12638
|
node: attribute,
|
|
12484
|
-
message: MESSAGE$
|
|
12639
|
+
message: MESSAGE$46
|
|
12485
12640
|
});
|
|
12486
12641
|
}
|
|
12487
12642
|
} };
|
|
@@ -12796,7 +12951,7 @@ const jsxPropsNoSpreadMulti = defineRule({
|
|
|
12796
12951
|
});
|
|
12797
12952
|
//#endregion
|
|
12798
12953
|
//#region src/plugin/rules/react-builtins/jsx-props-no-spreading.ts
|
|
12799
|
-
const MESSAGE$
|
|
12954
|
+
const MESSAGE$45 = "You can't tell what props reach this element when you spread them.";
|
|
12800
12955
|
const resolveSettings$25 = (settings) => {
|
|
12801
12956
|
const reactDoctor = settings?.["react-doctor"];
|
|
12802
12957
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.jsxPropsNoSpreading ?? {} : {};
|
|
@@ -12837,7 +12992,7 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
12837
12992
|
}
|
|
12838
12993
|
context.report({
|
|
12839
12994
|
node: attribute,
|
|
12840
|
-
message: MESSAGE$
|
|
12995
|
+
message: MESSAGE$45
|
|
12841
12996
|
});
|
|
12842
12997
|
}
|
|
12843
12998
|
} };
|
|
@@ -13065,7 +13220,7 @@ const labelHasAssociatedControl = defineRule({
|
|
|
13065
13220
|
});
|
|
13066
13221
|
//#endregion
|
|
13067
13222
|
//#region src/plugin/rules/a11y/lang.ts
|
|
13068
|
-
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`.";
|
|
13069
13224
|
const COMMON_LANGUAGE_PRIMARY_TAGS = new Set([
|
|
13070
13225
|
"aa",
|
|
13071
13226
|
"ab",
|
|
@@ -13277,7 +13432,7 @@ const lang = defineRule({
|
|
|
13277
13432
|
if (expression.type === "Identifier" && expression.name === "undefined" || expression.type === "Literal" && expression.value === null) {
|
|
13278
13433
|
context.report({
|
|
13279
13434
|
node: langAttr,
|
|
13280
|
-
message: MESSAGE$
|
|
13435
|
+
message: MESSAGE$44
|
|
13281
13436
|
});
|
|
13282
13437
|
return;
|
|
13283
13438
|
}
|
|
@@ -13286,7 +13441,7 @@ const lang = defineRule({
|
|
|
13286
13441
|
if (value === null) return;
|
|
13287
13442
|
if (!isValidLangTag(value)) context.report({
|
|
13288
13443
|
node: langAttr,
|
|
13289
|
-
message: MESSAGE$
|
|
13444
|
+
message: MESSAGE$44
|
|
13290
13445
|
});
|
|
13291
13446
|
} })
|
|
13292
13447
|
});
|
|
@@ -13330,7 +13485,7 @@ const mdxSsrExecutionRisk = defineRule({
|
|
|
13330
13485
|
});
|
|
13331
13486
|
//#endregion
|
|
13332
13487
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
13333
|
-
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>`.";
|
|
13334
13489
|
const DEFAULT_AUDIO = ["audio"];
|
|
13335
13490
|
const DEFAULT_VIDEO = ["video"];
|
|
13336
13491
|
const DEFAULT_TRACK = ["track"];
|
|
@@ -13371,7 +13526,7 @@ const mediaHasCaption = defineRule({
|
|
|
13371
13526
|
if (!parent || !isNodeOfType(parent, "JSXElement")) {
|
|
13372
13527
|
context.report({
|
|
13373
13528
|
node: node.name,
|
|
13374
|
-
message: MESSAGE$
|
|
13529
|
+
message: MESSAGE$43
|
|
13375
13530
|
});
|
|
13376
13531
|
return;
|
|
13377
13532
|
}
|
|
@@ -13388,7 +13543,7 @@ const mediaHasCaption = defineRule({
|
|
|
13388
13543
|
return kindValue.value.toLowerCase() === "captions";
|
|
13389
13544
|
})) context.report({
|
|
13390
13545
|
node: node.name,
|
|
13391
|
-
message: MESSAGE$
|
|
13546
|
+
message: MESSAGE$43
|
|
13392
13547
|
});
|
|
13393
13548
|
} };
|
|
13394
13549
|
}
|
|
@@ -15189,7 +15344,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
15189
15344
|
});
|
|
15190
15345
|
//#endregion
|
|
15191
15346
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
15192
|
-
const MESSAGE$
|
|
15347
|
+
const MESSAGE$42 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
15193
15348
|
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
15194
15349
|
const noAccessKey = defineRule({
|
|
15195
15350
|
id: "no-access-key",
|
|
@@ -15206,7 +15361,7 @@ const noAccessKey = defineRule({
|
|
|
15206
15361
|
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") {
|
|
15207
15362
|
context.report({
|
|
15208
15363
|
node: accessKey,
|
|
15209
|
-
message: MESSAGE$
|
|
15364
|
+
message: MESSAGE$42
|
|
15210
15365
|
});
|
|
15211
15366
|
return;
|
|
15212
15367
|
}
|
|
@@ -15216,7 +15371,7 @@ const noAccessKey = defineRule({
|
|
|
15216
15371
|
if (isUndefinedIdentifier(expression)) return;
|
|
15217
15372
|
context.report({
|
|
15218
15373
|
node: accessKey,
|
|
15219
|
-
message: MESSAGE$
|
|
15374
|
+
message: MESSAGE$42
|
|
15220
15375
|
});
|
|
15221
15376
|
}
|
|
15222
15377
|
} })
|
|
@@ -15698,8 +15853,41 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
15698
15853
|
} })
|
|
15699
15854
|
});
|
|
15700
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
|
|
15701
15889
|
//#region src/plugin/rules/a11y/no-aria-hidden-on-focusable.ts
|
|
15702
|
-
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.";
|
|
15703
15891
|
const noAriaHiddenOnFocusable = defineRule({
|
|
15704
15892
|
id: "no-aria-hidden-on-focusable",
|
|
15705
15893
|
title: "aria-hidden on focusable element",
|
|
@@ -15726,7 +15914,7 @@ const noAriaHiddenOnFocusable = defineRule({
|
|
|
15726
15914
|
const isImplicitlyFocusable = isInteractiveElement(tag, node);
|
|
15727
15915
|
if (isExplicitlyFocusable || isImplicitlyFocusable) context.report({
|
|
15728
15916
|
node: ariaHidden,
|
|
15729
|
-
message: MESSAGE$
|
|
15917
|
+
message: MESSAGE$41
|
|
15730
15918
|
});
|
|
15731
15919
|
} })
|
|
15732
15920
|
});
|
|
@@ -16094,7 +16282,7 @@ const noArrayIndexAsKey = defineRule({
|
|
|
16094
16282
|
});
|
|
16095
16283
|
//#endregion
|
|
16096
16284
|
//#region src/plugin/rules/react-builtins/no-array-index-key.ts
|
|
16097
|
-
const MESSAGE$
|
|
16285
|
+
const MESSAGE$40 = "Your users can see & submit the wrong data when this list reorders.";
|
|
16098
16286
|
const SECOND_INDEX_METHODS = new Set([
|
|
16099
16287
|
"every",
|
|
16100
16288
|
"filter",
|
|
@@ -16298,7 +16486,7 @@ const noArrayIndexKey = defineRule({
|
|
|
16298
16486
|
}
|
|
16299
16487
|
context.report({
|
|
16300
16488
|
node: keyAttribute,
|
|
16301
|
-
message: MESSAGE$
|
|
16489
|
+
message: MESSAGE$40
|
|
16302
16490
|
});
|
|
16303
16491
|
},
|
|
16304
16492
|
CallExpression(node) {
|
|
@@ -16318,15 +16506,35 @@ const noArrayIndexKey = defineRule({
|
|
|
16318
16506
|
if (propName !== "key") continue;
|
|
16319
16507
|
if (expressionUsesIndex(property.value, indexBinding.name)) context.report({
|
|
16320
16508
|
node: property,
|
|
16321
|
-
message: MESSAGE$
|
|
16509
|
+
message: MESSAGE$40
|
|
16322
16510
|
});
|
|
16323
16511
|
}
|
|
16324
16512
|
}
|
|
16325
16513
|
})
|
|
16326
16514
|
});
|
|
16327
16515
|
//#endregion
|
|
16516
|
+
//#region src/plugin/rules/state-and-effects/no-async-effect-callback.ts
|
|
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.";
|
|
16518
|
+
const noAsyncEffectCallback = defineRule({
|
|
16519
|
+
id: "no-async-effect-callback",
|
|
16520
|
+
title: "Async effect callback",
|
|
16521
|
+
severity: "warn",
|
|
16522
|
+
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
16523
|
+
create: (context) => ({ CallExpression(node) {
|
|
16524
|
+
if (!isHookCall$1(node, EFFECT_HOOK_NAMES$1)) return;
|
|
16525
|
+
const callback = getEffectCallback(node);
|
|
16526
|
+
if (!callback) return;
|
|
16527
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
16528
|
+
if (!callback.async) return;
|
|
16529
|
+
context.report({
|
|
16530
|
+
node: callback,
|
|
16531
|
+
message: MESSAGE$39
|
|
16532
|
+
});
|
|
16533
|
+
} })
|
|
16534
|
+
});
|
|
16535
|
+
//#endregion
|
|
16328
16536
|
//#region src/plugin/rules/a11y/no-autofocus.ts
|
|
16329
|
-
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.";
|
|
16330
16538
|
const resolveSettings$21 = (settings) => {
|
|
16331
16539
|
const reactDoctor = settings?.["react-doctor"];
|
|
16332
16540
|
return { ignoreNonDOM: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noAutofocus ?? {} : {}).ignoreNonDOM ?? true };
|
|
@@ -16382,12 +16590,45 @@ const noAutofocus = defineRule({
|
|
|
16382
16590
|
}
|
|
16383
16591
|
context.report({
|
|
16384
16592
|
node: autoFocusAttribute,
|
|
16385
|
-
message: MESSAGE$
|
|
16593
|
+
message: MESSAGE$38
|
|
16386
16594
|
});
|
|
16387
16595
|
} };
|
|
16388
16596
|
}
|
|
16389
16597
|
});
|
|
16390
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
|
|
16391
16632
|
//#region src/plugin/utils/create-relative-import-source.ts
|
|
16392
16633
|
const createRelativeImportSource = (filename, targetFilePath) => {
|
|
16393
16634
|
const targetPathWithoutExtension = targetFilePath.slice(0, targetFilePath.length - path.extname(targetFilePath).length);
|
|
@@ -16632,6 +16873,109 @@ const noBarrelImport = defineRule({
|
|
|
16632
16873
|
}
|
|
16633
16874
|
});
|
|
16634
16875
|
//#endregion
|
|
16876
|
+
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
16877
|
+
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
16878
|
+
"FunctionDeclaration",
|
|
16879
|
+
"FunctionExpression",
|
|
16880
|
+
"ArrowFunctionExpression",
|
|
16881
|
+
"ClassDeclaration",
|
|
16882
|
+
"ClassExpression"
|
|
16883
|
+
]);
|
|
16884
|
+
const isReactImport$1 = (symbol) => {
|
|
16885
|
+
let importDeclaration = symbol.declarationNode?.parent;
|
|
16886
|
+
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
16887
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
16888
|
+
return importDeclaration.source.value === "react";
|
|
16889
|
+
};
|
|
16890
|
+
const getImportedName = (symbol) => {
|
|
16891
|
+
if (symbol.kind !== "import") return null;
|
|
16892
|
+
if (!isReactImport$1(symbol)) return null;
|
|
16893
|
+
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
16894
|
+
};
|
|
16895
|
+
const isReactNamespaceImport = (symbol) => {
|
|
16896
|
+
if (symbol.kind !== "import") return false;
|
|
16897
|
+
if (!isReactImport$1(symbol)) return false;
|
|
16898
|
+
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
16899
|
+
};
|
|
16900
|
+
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
16901
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
16902
|
+
const symbol = scopes.symbolFor(callee);
|
|
16903
|
+
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
16904
|
+
};
|
|
16905
|
+
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
16906
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
16907
|
+
if (callee.computed) return false;
|
|
16908
|
+
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
16909
|
+
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
16910
|
+
if (callee.property.name !== "createElement") return false;
|
|
16911
|
+
const symbol = scopes.symbolFor(callee.object);
|
|
16912
|
+
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
16913
|
+
};
|
|
16914
|
+
const isReactCreateElementCall = (node, scopes) => {
|
|
16915
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
16916
|
+
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
16917
|
+
};
|
|
16918
|
+
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
16919
|
+
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
16920
|
+
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
16921
|
+
if (isReactCreateElementCall(node, scopes)) return true;
|
|
16922
|
+
const nodeRecord = node;
|
|
16923
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
16924
|
+
if (key === "parent") continue;
|
|
16925
|
+
const child = nodeRecord[key];
|
|
16926
|
+
if (Array.isArray(child)) {
|
|
16927
|
+
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
16928
|
+
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
16929
|
+
}
|
|
16930
|
+
return false;
|
|
16931
|
+
};
|
|
16932
|
+
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
16933
|
+
//#endregion
|
|
16934
|
+
//#region src/plugin/utils/is-component-declaration.ts
|
|
16935
|
+
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
16936
|
+
//#endregion
|
|
16937
|
+
//#region src/plugin/rules/react-builtins/no-call-component-as-function.ts
|
|
16938
|
+
const message = (name) => `\`${name}\` is a component, so calling it as a plain function (\`${name}(...)\`) runs it outside React: its hooks break, it gets no fiber/state, and memoization is lost. Render it as \`<${name} />\` instead.`;
|
|
16939
|
+
const symbolIsLocalComponent = (symbol, context) => {
|
|
16940
|
+
const declaration = symbol.declarationNode;
|
|
16941
|
+
if (isComponentDeclaration(declaration)) return functionContainsReactRenderOutput(declaration, context.scopes);
|
|
16942
|
+
if (isComponentAssignment(declaration) && symbol.initializer) return functionContainsReactRenderOutput(symbol.initializer, context.scopes);
|
|
16943
|
+
return false;
|
|
16944
|
+
};
|
|
16945
|
+
const noCallComponentAsFunction = defineRule({
|
|
16946
|
+
id: "no-call-component-as-function",
|
|
16947
|
+
title: "Component called as a function",
|
|
16948
|
+
severity: "warn",
|
|
16949
|
+
tags: ["test-noise"],
|
|
16950
|
+
recommendation: "Render components as JSX (`<Component />`), never call them like functions (`Component(props)`). A direct call runs the component outside React and breaks hooks, state, and memoization.",
|
|
16951
|
+
create: (context) => {
|
|
16952
|
+
const renderedJsxNames = /* @__PURE__ */ new Set();
|
|
16953
|
+
const candidateCalls = [];
|
|
16954
|
+
return {
|
|
16955
|
+
JSXOpeningElement(node) {
|
|
16956
|
+
if (isNodeOfType(node.name, "JSXIdentifier") && isUppercaseName(node.name.name)) renderedJsxNames.add(node.name.name);
|
|
16957
|
+
},
|
|
16958
|
+
CallExpression(node) {
|
|
16959
|
+
if (isNodeOfType(node.callee, "Identifier") && isUppercaseName(node.callee.name)) candidateCalls.push({
|
|
16960
|
+
node,
|
|
16961
|
+
callee: node.callee,
|
|
16962
|
+
name: node.callee.name
|
|
16963
|
+
});
|
|
16964
|
+
},
|
|
16965
|
+
"Program:exit"() {
|
|
16966
|
+
for (const candidate of candidateCalls) {
|
|
16967
|
+
const symbol = context.scopes.symbolFor(candidate.callee);
|
|
16968
|
+
if (!symbol) continue;
|
|
16969
|
+
if (symbolIsLocalComponent(symbol, context) || symbol.kind === "import" && renderedJsxNames.has(candidate.name)) context.report({
|
|
16970
|
+
node: candidate.node,
|
|
16971
|
+
message: message(candidate.name)
|
|
16972
|
+
});
|
|
16973
|
+
}
|
|
16974
|
+
}
|
|
16975
|
+
};
|
|
16976
|
+
}
|
|
16977
|
+
});
|
|
16978
|
+
//#endregion
|
|
16635
16979
|
//#region src/plugin/utils/is-setter-identifier.ts
|
|
16636
16980
|
const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
16637
16981
|
//#endregion
|
|
@@ -16783,7 +17127,7 @@ const noChainStateUpdates = defineRule({
|
|
|
16783
17127
|
});
|
|
16784
17128
|
//#endregion
|
|
16785
17129
|
//#region src/plugin/rules/react-builtins/no-children-prop.ts
|
|
16786
|
-
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.";
|
|
16787
17131
|
const noChildrenProp = defineRule({
|
|
16788
17132
|
id: "no-children-prop",
|
|
16789
17133
|
title: "Children passed as a prop",
|
|
@@ -16795,7 +17139,7 @@ const noChildrenProp = defineRule({
|
|
|
16795
17139
|
if (node.name.name !== "children") return;
|
|
16796
17140
|
context.report({
|
|
16797
17141
|
node: node.name,
|
|
16798
|
-
message: MESSAGE$
|
|
17142
|
+
message: MESSAGE$36
|
|
16799
17143
|
});
|
|
16800
17144
|
},
|
|
16801
17145
|
CallExpression(node) {
|
|
@@ -16808,7 +17152,7 @@ const noChildrenProp = defineRule({
|
|
|
16808
17152
|
const propertyKey = property.key;
|
|
16809
17153
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "children" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "children") context.report({
|
|
16810
17154
|
node: propertyKey,
|
|
16811
|
-
message: MESSAGE$
|
|
17155
|
+
message: MESSAGE$36
|
|
16812
17156
|
});
|
|
16813
17157
|
}
|
|
16814
17158
|
}
|
|
@@ -16816,7 +17160,7 @@ const noChildrenProp = defineRule({
|
|
|
16816
17160
|
});
|
|
16817
17161
|
//#endregion
|
|
16818
17162
|
//#region src/plugin/rules/react-builtins/no-clone-element.ts
|
|
16819
|
-
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.";
|
|
16820
17164
|
const noCloneElement = defineRule({
|
|
16821
17165
|
id: "no-clone-element",
|
|
16822
17166
|
title: "cloneElement makes child props fragile",
|
|
@@ -16829,7 +17173,7 @@ const noCloneElement = defineRule({
|
|
|
16829
17173
|
if (isNodeOfType(callee, "Identifier") && callee.name === "cloneElement") {
|
|
16830
17174
|
if (isImportedFromModule(node, "cloneElement", "react")) context.report({
|
|
16831
17175
|
node: callee,
|
|
16832
|
-
message: MESSAGE$
|
|
17176
|
+
message: MESSAGE$35
|
|
16833
17177
|
});
|
|
16834
17178
|
return;
|
|
16835
17179
|
}
|
|
@@ -16842,7 +17186,7 @@ const noCloneElement = defineRule({
|
|
|
16842
17186
|
if (!isImportedFromModule(node, callee.object.name, "react")) return;
|
|
16843
17187
|
context.report({
|
|
16844
17188
|
node: callee,
|
|
16845
|
-
message: MESSAGE$
|
|
17189
|
+
message: MESSAGE$35
|
|
16846
17190
|
});
|
|
16847
17191
|
}
|
|
16848
17192
|
} })
|
|
@@ -16891,7 +17235,7 @@ const enclosingComponentOrHookName = (node) => {
|
|
|
16891
17235
|
};
|
|
16892
17236
|
//#endregion
|
|
16893
17237
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
16894
|
-
const MESSAGE$
|
|
17238
|
+
const MESSAGE$34 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
16895
17239
|
const CONTEXT_MODULES = [
|
|
16896
17240
|
"react",
|
|
16897
17241
|
"use-context-selector",
|
|
@@ -16927,7 +17271,32 @@ const noCreateContextInRender = defineRule({
|
|
|
16927
17271
|
if (!componentOrHookName) return;
|
|
16928
17272
|
context.report({
|
|
16929
17273
|
node,
|
|
16930
|
-
message: `${MESSAGE$
|
|
17274
|
+
message: `${MESSAGE$34} (called inside "${componentOrHookName}")`
|
|
17275
|
+
});
|
|
17276
|
+
} })
|
|
17277
|
+
});
|
|
17278
|
+
//#endregion
|
|
17279
|
+
//#region src/plugin/rules/react-builtins/no-create-ref-in-function-component.ts
|
|
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.";
|
|
17281
|
+
const noCreateRefInFunctionComponent = defineRule({
|
|
17282
|
+
id: "no-create-ref-in-function-component",
|
|
17283
|
+
title: "createRef in function component",
|
|
17284
|
+
severity: "warn",
|
|
17285
|
+
recommendation: "Replace `createRef()` with the `useRef()` hook inside function components and hooks. `createRef` is only for class components.",
|
|
17286
|
+
create: (context) => ({ CallExpression(node) {
|
|
17287
|
+
if (!isReactFunctionCall(node, "createRef")) return;
|
|
17288
|
+
if (isNodeOfType(node.callee, "Identifier")) {
|
|
17289
|
+
const symbol = context.scopes.symbolFor(node.callee);
|
|
17290
|
+
if (symbol && symbol.kind !== "import") return;
|
|
17291
|
+
}
|
|
17292
|
+
const enclosingFunction = nearestEnclosingFunction(node);
|
|
17293
|
+
if (!enclosingFunction) return;
|
|
17294
|
+
const displayName = componentOrHookDisplayNameForFunction(enclosingFunction);
|
|
17295
|
+
if (!displayName) return;
|
|
17296
|
+
if (!(isReactHookName(displayName) || functionContainsReactRenderOutput(enclosingFunction, context.scopes))) return;
|
|
17297
|
+
context.report({
|
|
17298
|
+
node,
|
|
17299
|
+
message: MESSAGE$33
|
|
16931
17300
|
});
|
|
16932
17301
|
} })
|
|
16933
17302
|
});
|
|
@@ -17067,7 +17436,7 @@ const noCreateStoreInRender = defineRule({
|
|
|
17067
17436
|
});
|
|
17068
17437
|
//#endregion
|
|
17069
17438
|
//#region src/plugin/rules/react-builtins/no-danger.ts
|
|
17070
|
-
const MESSAGE$
|
|
17439
|
+
const MESSAGE$32 = "`dangerouslySetInnerHTML` is an XSS hole that runs attacker-controlled HTML in your users' browsers.";
|
|
17071
17440
|
const noDanger = defineRule({
|
|
17072
17441
|
id: "no-danger",
|
|
17073
17442
|
title: "Raw HTML injection can run unsafe markup",
|
|
@@ -17080,7 +17449,7 @@ const noDanger = defineRule({
|
|
|
17080
17449
|
if (!propAttribute) return;
|
|
17081
17450
|
context.report({
|
|
17082
17451
|
node: propAttribute.name,
|
|
17083
|
-
message: MESSAGE$
|
|
17452
|
+
message: MESSAGE$32
|
|
17084
17453
|
});
|
|
17085
17454
|
},
|
|
17086
17455
|
CallExpression(node) {
|
|
@@ -17092,7 +17461,7 @@ const noDanger = defineRule({
|
|
|
17092
17461
|
const propertyKey = property.key;
|
|
17093
17462
|
if (isNodeOfType(propertyKey, "Identifier") && propertyKey.name === "dangerouslySetInnerHTML" || isNodeOfType(propertyKey, "Literal") && propertyKey.value === "dangerouslySetInnerHTML") context.report({
|
|
17094
17463
|
node: propertyKey,
|
|
17095
|
-
message: MESSAGE$
|
|
17464
|
+
message: MESSAGE$32
|
|
17096
17465
|
});
|
|
17097
17466
|
}
|
|
17098
17467
|
}
|
|
@@ -17100,7 +17469,7 @@ const noDanger = defineRule({
|
|
|
17100
17469
|
});
|
|
17101
17470
|
//#endregion
|
|
17102
17471
|
//#region src/plugin/rules/react-builtins/no-danger-with-children.ts
|
|
17103
|
-
const MESSAGE$
|
|
17472
|
+
const MESSAGE$31 = "React throws an error when you set both children & `dangerouslySetInnerHTML`.";
|
|
17104
17473
|
const isLineBreak = (child) => {
|
|
17105
17474
|
if (!isNodeOfType(child, "JSXText")) return false;
|
|
17106
17475
|
return child.value.trim().length === 0 && child.value.includes("\n");
|
|
@@ -17170,7 +17539,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17170
17539
|
if (!hasChildrenProp && !hasNestedChildren) return;
|
|
17171
17540
|
if (hasJsxPropIgnoreCase(opening.attributes, "dangerouslySetInnerHTML") || spreadPropsShape.hasDangerously) context.report({
|
|
17172
17541
|
node: opening,
|
|
17173
|
-
message: MESSAGE$
|
|
17542
|
+
message: MESSAGE$31
|
|
17174
17543
|
});
|
|
17175
17544
|
},
|
|
17176
17545
|
CallExpression(node) {
|
|
@@ -17182,7 +17551,7 @@ const noDangerWithChildren = defineRule({
|
|
|
17182
17551
|
if (!propsShape.hasDangerously) return;
|
|
17183
17552
|
if (node.arguments.length >= 3 || propsShape.hasChildren) context.report({
|
|
17184
17553
|
node,
|
|
17185
|
-
message: MESSAGE$
|
|
17554
|
+
message: MESSAGE$31
|
|
17186
17555
|
});
|
|
17187
17556
|
}
|
|
17188
17557
|
})
|
|
@@ -17347,6 +17716,37 @@ const noDefaultProps = defineRule({
|
|
|
17347
17716
|
} })
|
|
17348
17717
|
});
|
|
17349
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
|
|
17350
17750
|
//#region src/plugin/utils/is-initial-only-prop-name.ts
|
|
17351
17751
|
const isInitialOnlyPropName = (propName) => {
|
|
17352
17752
|
if (propName === "initialValue" || propName === "defaultValue" || propName === "seedValue") return true;
|
|
@@ -17759,7 +18159,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
17759
18159
|
//#endregion
|
|
17760
18160
|
//#region src/plugin/rules/react-builtins/no-did-mount-set-state.ts
|
|
17761
18161
|
const LIFECYCLE_NAMES$2 = new Set(["componentDidMount"]);
|
|
17762
|
-
const MESSAGE$
|
|
18162
|
+
const MESSAGE$30 = "Your users see an extra render right after mount when you call `setState` in `componentDidMount`.";
|
|
17763
18163
|
const resolveSettings$20 = (settings) => {
|
|
17764
18164
|
const reactDoctor = settings?.["react-doctor"];
|
|
17765
18165
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidMountSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17778,7 +18178,7 @@ const noDidMountSetState = defineRule({
|
|
|
17778
18178
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$2, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17779
18179
|
context.report({
|
|
17780
18180
|
node: node.callee,
|
|
17781
|
-
message: MESSAGE$
|
|
18181
|
+
message: MESSAGE$30
|
|
17782
18182
|
});
|
|
17783
18183
|
} };
|
|
17784
18184
|
}
|
|
@@ -17786,7 +18186,7 @@ const noDidMountSetState = defineRule({
|
|
|
17786
18186
|
//#endregion
|
|
17787
18187
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
17788
18188
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
17789
|
-
const MESSAGE$
|
|
18189
|
+
const MESSAGE$29 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
17790
18190
|
const resolveSettings$19 = (settings) => {
|
|
17791
18191
|
const reactDoctor = settings?.["react-doctor"];
|
|
17792
18192
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noDidUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -17805,7 +18205,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
17805
18205
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
17806
18206
|
context.report({
|
|
17807
18207
|
node: node.callee,
|
|
17808
|
-
message: MESSAGE$
|
|
18208
|
+
message: MESSAGE$29
|
|
17809
18209
|
});
|
|
17810
18210
|
} };
|
|
17811
18211
|
}
|
|
@@ -17828,7 +18228,7 @@ const isStateMemberExpression = (node) => {
|
|
|
17828
18228
|
};
|
|
17829
18229
|
//#endregion
|
|
17830
18230
|
//#region src/plugin/rules/react-builtins/no-direct-mutation-state.ts
|
|
17831
|
-
const MESSAGE$
|
|
18231
|
+
const MESSAGE$28 = "Your users see stale data because mutating `this.state` by hand never redraws & gets overwritten.";
|
|
17832
18232
|
const shouldIgnoreMutation = (node) => {
|
|
17833
18233
|
let isConstructor = false;
|
|
17834
18234
|
let isInsideCallExpression = false;
|
|
@@ -17850,7 +18250,7 @@ const reportIfStateMutation = (context, reportNode, target) => {
|
|
|
17850
18250
|
if (shouldIgnoreMutation(reportNode)) return;
|
|
17851
18251
|
context.report({
|
|
17852
18252
|
node: reportNode,
|
|
17853
|
-
message: MESSAGE$
|
|
18253
|
+
message: MESSAGE$28
|
|
17854
18254
|
});
|
|
17855
18255
|
};
|
|
17856
18256
|
const noDirectMutationState = defineRule({
|
|
@@ -18060,6 +18460,26 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
18060
18460
|
} })
|
|
18061
18461
|
});
|
|
18062
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
|
|
18063
18483
|
//#region src/plugin/rules/bundle-size/no-dynamic-import-path.ts
|
|
18064
18484
|
const noDynamicImportPath = defineRule({
|
|
18065
18485
|
id: "no-dynamic-import-path",
|
|
@@ -19438,7 +19858,7 @@ const ALLOWED_NAMESPACES = new Set([
|
|
|
19438
19858
|
"ReactDOM",
|
|
19439
19859
|
"ReactDom"
|
|
19440
19860
|
]);
|
|
19441
|
-
const MESSAGE$
|
|
19861
|
+
const MESSAGE$26 = "`findDOMNode` crashes your app in React 19 because it was removed.";
|
|
19442
19862
|
const noFindDomNode = defineRule({
|
|
19443
19863
|
id: "no-find-dom-node",
|
|
19444
19864
|
title: "findDOMNode breaks component encapsulation",
|
|
@@ -19449,7 +19869,7 @@ const noFindDomNode = defineRule({
|
|
|
19449
19869
|
if (isNodeOfType(callee, "Identifier") && callee.name === "findDOMNode") {
|
|
19450
19870
|
context.report({
|
|
19451
19871
|
node: callee,
|
|
19452
|
-
message: MESSAGE$
|
|
19872
|
+
message: MESSAGE$26
|
|
19453
19873
|
});
|
|
19454
19874
|
return;
|
|
19455
19875
|
}
|
|
@@ -19460,7 +19880,7 @@ const noFindDomNode = defineRule({
|
|
|
19460
19880
|
if (callee.property.name !== "findDOMNode") return;
|
|
19461
19881
|
context.report({
|
|
19462
19882
|
node: callee.property,
|
|
19463
|
-
message: MESSAGE$
|
|
19883
|
+
message: MESSAGE$26
|
|
19464
19884
|
});
|
|
19465
19885
|
}
|
|
19466
19886
|
} })
|
|
@@ -19501,6 +19921,41 @@ const noFullLodashImport = defineRule({
|
|
|
19501
19921
|
} })
|
|
19502
19922
|
});
|
|
19503
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
|
|
19504
19959
|
//#region src/plugin/rules/architecture/no-generic-handler-names.ts
|
|
19505
19960
|
const noGenericHandlerNames = defineRule({
|
|
19506
19961
|
id: "no-generic-handler-names",
|
|
@@ -19523,64 +19978,6 @@ const noGenericHandlerNames = defineRule({
|
|
|
19523
19978
|
} })
|
|
19524
19979
|
});
|
|
19525
19980
|
//#endregion
|
|
19526
|
-
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
19527
|
-
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
19528
|
-
"FunctionDeclaration",
|
|
19529
|
-
"FunctionExpression",
|
|
19530
|
-
"ArrowFunctionExpression",
|
|
19531
|
-
"ClassDeclaration",
|
|
19532
|
-
"ClassExpression"
|
|
19533
|
-
]);
|
|
19534
|
-
const isReactImport$1 = (symbol) => {
|
|
19535
|
-
let importDeclaration = symbol.declarationNode?.parent;
|
|
19536
|
-
while (importDeclaration && !isNodeOfType(importDeclaration, "ImportDeclaration")) importDeclaration = importDeclaration.parent ?? null;
|
|
19537
|
-
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration")) return false;
|
|
19538
|
-
return importDeclaration.source.value === "react";
|
|
19539
|
-
};
|
|
19540
|
-
const getImportedName = (symbol) => {
|
|
19541
|
-
if (symbol.kind !== "import") return null;
|
|
19542
|
-
if (!isReactImport$1(symbol)) return null;
|
|
19543
|
-
return getImportedName$1(symbol.declarationNode) ?? null;
|
|
19544
|
-
};
|
|
19545
|
-
const isReactNamespaceImport = (symbol) => {
|
|
19546
|
-
if (symbol.kind !== "import") return false;
|
|
19547
|
-
if (!isReactImport$1(symbol)) return false;
|
|
19548
|
-
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
19549
|
-
};
|
|
19550
|
-
const isReactCreateElementIdentifierCall = (callee, scopes) => {
|
|
19551
|
-
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
19552
|
-
const symbol = scopes.symbolFor(callee);
|
|
19553
|
-
return Boolean(symbol && getImportedName(symbol) === "createElement");
|
|
19554
|
-
};
|
|
19555
|
-
const isReactCreateElementMemberCall = (callee, scopes) => {
|
|
19556
|
-
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
19557
|
-
if (callee.computed) return false;
|
|
19558
|
-
if (!isNodeOfType(callee.object, "Identifier")) return false;
|
|
19559
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
19560
|
-
if (callee.property.name !== "createElement") return false;
|
|
19561
|
-
const symbol = scopes.symbolFor(callee.object);
|
|
19562
|
-
return Boolean(symbol && isReactNamespaceImport(symbol));
|
|
19563
|
-
};
|
|
19564
|
-
const isReactCreateElementCall = (node, scopes) => {
|
|
19565
|
-
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
19566
|
-
return isReactCreateElementIdentifierCall(node.callee, scopes) || isReactCreateElementMemberCall(node.callee, scopes);
|
|
19567
|
-
};
|
|
19568
|
-
const containsRenderOutput = (node, rootNode, scopes) => {
|
|
19569
|
-
if (node !== rootNode && NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type)) return false;
|
|
19570
|
-
if (node.type === "JSXElement" || node.type === "JSXFragment") return true;
|
|
19571
|
-
if (isReactCreateElementCall(node, scopes)) return true;
|
|
19572
|
-
const nodeRecord = node;
|
|
19573
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
19574
|
-
if (key === "parent") continue;
|
|
19575
|
-
const child = nodeRecord[key];
|
|
19576
|
-
if (Array.isArray(child)) {
|
|
19577
|
-
for (const innerChild of child) if (isAstNode(innerChild) && containsRenderOutput(innerChild, rootNode, scopes)) return true;
|
|
19578
|
-
} else if (isAstNode(child) && containsRenderOutput(child, rootNode, scopes)) return true;
|
|
19579
|
-
}
|
|
19580
|
-
return false;
|
|
19581
|
-
};
|
|
19582
|
-
const functionContainsReactRenderOutput = (functionNode, scopes) => containsRenderOutput(functionNode, functionNode, scopes);
|
|
19583
|
-
//#endregion
|
|
19584
19981
|
//#region src/plugin/rules/architecture/no-giant-component.ts
|
|
19585
19982
|
const noGiantComponent = defineRule({
|
|
19586
19983
|
id: "no-giant-component",
|
|
@@ -19621,7 +20018,7 @@ const noGiantComponent = defineRule({
|
|
|
19621
20018
|
});
|
|
19622
20019
|
//#endregion
|
|
19623
20020
|
//#region src/plugin/constants/style.ts
|
|
19624
|
-
const LAYOUT_PROPERTIES = new Set([
|
|
20021
|
+
const LAYOUT_PROPERTIES$1 = new Set([
|
|
19625
20022
|
"width",
|
|
19626
20023
|
"height",
|
|
19627
20024
|
"top",
|
|
@@ -19691,17 +20088,6 @@ const noGlobalCssVariableAnimation = defineRule({
|
|
|
19691
20088
|
} })
|
|
19692
20089
|
});
|
|
19693
20090
|
//#endregion
|
|
19694
|
-
//#region src/plugin/rules/design/utils/get-string-from-class-name-attr.ts
|
|
19695
|
-
const getStringFromClassNameAttr = (node) => {
|
|
19696
|
-
if (!isNodeOfType(node, "JSXOpeningElement")) return null;
|
|
19697
|
-
const classAttr = findJsxAttribute(node.attributes ?? [], "className");
|
|
19698
|
-
if (!classAttr?.value) return null;
|
|
19699
|
-
if (isNodeOfType(classAttr.value, "Literal") && typeof classAttr.value.value === "string") return classAttr.value.value;
|
|
19700
|
-
if (isNodeOfType(classAttr.value, "JSXExpressionContainer") && isNodeOfType(classAttr.value.expression, "Literal") && typeof classAttr.value.expression.value === "string") return classAttr.value.expression.value;
|
|
19701
|
-
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;
|
|
19702
|
-
return null;
|
|
19703
|
-
};
|
|
19704
|
-
//#endregion
|
|
19705
20091
|
//#region src/plugin/rules/design/no-gradient-text.ts
|
|
19706
20092
|
const noGradientText = defineRule({
|
|
19707
20093
|
id: "no-gradient-text",
|
|
@@ -19759,6 +20145,26 @@ const noGrayOnColoredBackground = defineRule({
|
|
|
19759
20145
|
} })
|
|
19760
20146
|
});
|
|
19761
20147
|
//#endregion
|
|
20148
|
+
//#region src/plugin/rules/performance/no-img-lazy-with-high-fetchpriority.ts
|
|
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.";
|
|
20150
|
+
const noImgLazyWithHighFetchpriority = defineRule({
|
|
20151
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
20152
|
+
title: "Lazy image with high fetchPriority",
|
|
20153
|
+
severity: "warn",
|
|
20154
|
+
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
20155
|
+
create: (context) => ({ JSXOpeningElement(node) {
|
|
20156
|
+
if (!isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "img") return;
|
|
20157
|
+
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
20158
|
+
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
20159
|
+
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
20160
|
+
if (!fetchPriorityAttribute || getJsxPropStringValue(fetchPriorityAttribute)?.toLowerCase() !== "high") return;
|
|
20161
|
+
context.report({
|
|
20162
|
+
node: node.name,
|
|
20163
|
+
message: MESSAGE$24
|
|
20164
|
+
});
|
|
20165
|
+
} })
|
|
20166
|
+
});
|
|
20167
|
+
//#endregion
|
|
19762
20168
|
//#region src/plugin/rules/state-and-effects/no-initialize-state.ts
|
|
19763
20169
|
const noInitializeState = defineRule({
|
|
19764
20170
|
id: "no-initialize-state",
|
|
@@ -19988,8 +20394,31 @@ const noIsMounted = defineRule({
|
|
|
19988
20394
|
} })
|
|
19989
20395
|
});
|
|
19990
20396
|
//#endregion
|
|
20397
|
+
//#region src/plugin/rules/js-performance/no-json-parse-stringify-clone.ts
|
|
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)`.";
|
|
20399
|
+
const isJsonMethodCall = (node, method) => {
|
|
20400
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
20401
|
+
const callee = node.callee;
|
|
20402
|
+
return isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.object, "Identifier") && callee.object.name === "JSON" && isNodeOfType(callee.property, "Identifier") && callee.property.name === method;
|
|
20403
|
+
};
|
|
20404
|
+
const noJsonParseStringifyClone = defineRule({
|
|
20405
|
+
id: "no-json-parse-stringify-clone",
|
|
20406
|
+
title: "JSON parse/stringify deep clone",
|
|
20407
|
+
severity: "warn",
|
|
20408
|
+
recommendation: "Replace `JSON.parse(JSON.stringify(value))` with `structuredClone(value)`. It is faster and preserves Dates, Maps, Sets, and cyclic references.",
|
|
20409
|
+
create: (context) => ({ CallExpression(node) {
|
|
20410
|
+
if (!isJsonMethodCall(node, "parse")) return;
|
|
20411
|
+
const firstArgument = node.arguments?.[0];
|
|
20412
|
+
if (!firstArgument || !isJsonMethodCall(firstArgument, "stringify")) return;
|
|
20413
|
+
context.report({
|
|
20414
|
+
node,
|
|
20415
|
+
message: MESSAGE$23
|
|
20416
|
+
});
|
|
20417
|
+
} })
|
|
20418
|
+
});
|
|
20419
|
+
//#endregion
|
|
19991
20420
|
//#region src/plugin/rules/correctness/no-jsx-element-type.ts
|
|
19992
|
-
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.";
|
|
19993
20422
|
const isJsxElementTypeReference = (node) => {
|
|
19994
20423
|
if (!isNodeOfType(node, "TSTypeReference")) return false;
|
|
19995
20424
|
const typeName = node.typeName;
|
|
@@ -20006,7 +20435,7 @@ const checkReturnType = (context, returnType) => {
|
|
|
20006
20435
|
if (!typeAnnotation) return;
|
|
20007
20436
|
if (isJsxElementTypeReference(typeAnnotation)) context.report({
|
|
20008
20437
|
node: typeAnnotation,
|
|
20009
|
-
message: MESSAGE$
|
|
20438
|
+
message: MESSAGE$22
|
|
20010
20439
|
});
|
|
20011
20440
|
};
|
|
20012
20441
|
const noJsxElementType = defineRule({
|
|
@@ -20116,7 +20545,7 @@ const noLayoutPropertyAnimation = defineRule({
|
|
|
20116
20545
|
let propertyName = null;
|
|
20117
20546
|
if (isNodeOfType(property.key, "Identifier")) propertyName = property.key.name;
|
|
20118
20547
|
else if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") propertyName = property.key.value;
|
|
20119
|
-
if (propertyName && LAYOUT_PROPERTIES.has(propertyName)) context.report({
|
|
20548
|
+
if (propertyName && LAYOUT_PROPERTIES$1.has(propertyName)) context.report({
|
|
20120
20549
|
node: property,
|
|
20121
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`
|
|
20122
20551
|
});
|
|
@@ -20306,13 +20735,138 @@ const noLongTransitionDuration = defineRule({
|
|
|
20306
20735
|
} })
|
|
20307
20736
|
});
|
|
20308
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
|
|
20309
20866
|
//#region src/plugin/utils/is-boolean-prefixed-prop-name.ts
|
|
20310
20867
|
const BOOLEAN_PROP_PREFIX_PATTERN = /^(?:is|has|should|can|show|hide|enable|disable|with)[A-Z]/;
|
|
20311
20868
|
const isBooleanPrefixedPropName = (propName) => BOOLEAN_PROP_PREFIX_PATTERN.test(propName);
|
|
20312
20869
|
//#endregion
|
|
20313
|
-
//#region src/plugin/utils/is-component-declaration.ts
|
|
20314
|
-
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
20315
|
-
//#endregion
|
|
20316
20870
|
//#region src/plugin/rules/architecture/no-many-boolean-props.ts
|
|
20317
20871
|
const collectBooleanLikePropsFromBody = (componentBody, propsParamName) => {
|
|
20318
20872
|
const found = /* @__PURE__ */ new Set();
|
|
@@ -20464,7 +21018,7 @@ const noMoment = defineRule({
|
|
|
20464
21018
|
});
|
|
20465
21019
|
//#endregion
|
|
20466
21020
|
//#region src/plugin/rules/react-builtins/no-multi-comp.ts
|
|
20467
|
-
const MESSAGE$
|
|
21021
|
+
const MESSAGE$21 = "This file declares several components, so each component is harder to find, test, and change.";
|
|
20468
21022
|
const resolveSettings$16 = (settings) => {
|
|
20469
21023
|
const reactDoctor = settings?.["react-doctor"];
|
|
20470
21024
|
return { ignoreStateless: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noMultiComp ?? {} : {}).ignoreStateless ?? false };
|
|
@@ -20786,7 +21340,7 @@ const noMultiComp = defineRule({
|
|
|
20786
21340
|
if (isSmallFeatureModule || isLargeFeatureModule || isVeryLargeFeatureModule) return;
|
|
20787
21341
|
for (const component of flagged.slice(1)) context.report({
|
|
20788
21342
|
node: component.reportNode,
|
|
20789
|
-
message: MESSAGE$
|
|
21343
|
+
message: MESSAGE$21
|
|
20790
21344
|
});
|
|
20791
21345
|
} };
|
|
20792
21346
|
}
|
|
@@ -20954,7 +21508,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
20954
21508
|
};
|
|
20955
21509
|
//#endregion
|
|
20956
21510
|
//#region src/plugin/rules/state-and-effects/no-mutating-reducer-state.ts
|
|
20957
|
-
const MESSAGE$
|
|
21511
|
+
const MESSAGE$20 = "This reducer changes state in place, so your update is silently skipped.";
|
|
20958
21512
|
const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
20959
21513
|
"copyWithin",
|
|
20960
21514
|
"fill",
|
|
@@ -21164,7 +21718,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21164
21718
|
reportedNodes.add(options.crossFileConsumerCallSite);
|
|
21165
21719
|
context.report({
|
|
21166
21720
|
node: options.crossFileConsumerCallSite,
|
|
21167
|
-
message: `${MESSAGE$
|
|
21721
|
+
message: `${MESSAGE$20} (mutation in imported reducer at \`${options.crossFileSourceDisplay}\`)`
|
|
21168
21722
|
});
|
|
21169
21723
|
return;
|
|
21170
21724
|
}
|
|
@@ -21173,7 +21727,7 @@ const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, r
|
|
|
21173
21727
|
reportedNodes.add(mutation.node);
|
|
21174
21728
|
context.report({
|
|
21175
21729
|
node: mutation.node,
|
|
21176
|
-
message: MESSAGE$
|
|
21730
|
+
message: MESSAGE$20
|
|
21177
21731
|
});
|
|
21178
21732
|
}
|
|
21179
21733
|
};
|
|
@@ -21445,7 +21999,7 @@ const noNoninteractiveElementToInteractiveRole = defineRule({
|
|
|
21445
21999
|
});
|
|
21446
22000
|
//#endregion
|
|
21447
22001
|
//#region src/plugin/rules/a11y/no-noninteractive-tabindex.ts
|
|
21448
|
-
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.";
|
|
21449
22003
|
const resolveSettings$14 = (settings) => {
|
|
21450
22004
|
const reactDoctor = settings?.["react-doctor"];
|
|
21451
22005
|
const ruleSettings = typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noNoninteractiveTabindex ?? {} : {};
|
|
@@ -21473,7 +22027,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21473
22027
|
if (numeric === null) {
|
|
21474
22028
|
if (isNodeOfType(tabIndexValue, "JSXExpressionContainer") && !settings.allowExpressionValues) context.report({
|
|
21475
22029
|
node: tabIndex,
|
|
21476
|
-
message: MESSAGE$
|
|
22030
|
+
message: MESSAGE$19
|
|
21477
22031
|
});
|
|
21478
22032
|
return;
|
|
21479
22033
|
}
|
|
@@ -21486,7 +22040,7 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21486
22040
|
if (!roleAttribute) {
|
|
21487
22041
|
context.report({
|
|
21488
22042
|
node: tabIndex,
|
|
21489
|
-
message: MESSAGE$
|
|
22043
|
+
message: MESSAGE$19
|
|
21490
22044
|
});
|
|
21491
22045
|
return;
|
|
21492
22046
|
}
|
|
@@ -21500,20 +22054,12 @@ const noNoninteractiveTabindex = defineRule({
|
|
|
21500
22054
|
}
|
|
21501
22055
|
context.report({
|
|
21502
22056
|
node: tabIndex,
|
|
21503
|
-
message: MESSAGE$
|
|
22057
|
+
message: MESSAGE$19
|
|
21504
22058
|
});
|
|
21505
22059
|
} };
|
|
21506
22060
|
}
|
|
21507
22061
|
});
|
|
21508
22062
|
//#endregion
|
|
21509
|
-
//#region src/plugin/rules/design/utils/get-style-property-number-value.ts
|
|
21510
|
-
const getStylePropertyNumberValue = (property) => {
|
|
21511
|
-
if (!isNodeOfType(property, "Property")) return null;
|
|
21512
|
-
if (isNodeOfType(property.value, "Literal") && typeof property.value.value === "number") return property.value.value;
|
|
21513
|
-
if (isNodeOfType(property.value, "UnaryExpression") && property.value.operator === "-" && isNodeOfType(property.value.argument, "Literal") && typeof property.value.argument.value === "number") return -property.value.argument.value;
|
|
21514
|
-
return null;
|
|
21515
|
-
};
|
|
21516
|
-
//#endregion
|
|
21517
22063
|
//#region src/plugin/rules/design/no-outline-none.ts
|
|
21518
22064
|
const noOutlineNone = defineRule({
|
|
21519
22065
|
id: "no-outline-none",
|
|
@@ -22191,7 +22737,7 @@ const noRandomKey = defineRule({
|
|
|
22191
22737
|
});
|
|
22192
22738
|
//#endregion
|
|
22193
22739
|
//#region src/plugin/rules/react-builtins/no-react-children.ts
|
|
22194
|
-
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.";
|
|
22195
22741
|
const isChildrenIdentifier = (node, contextNode) => {
|
|
22196
22742
|
if (!isNodeOfType(node, "Identifier") || node.name !== "Children") return false;
|
|
22197
22743
|
return isImportedFromModule(contextNode, "Children", "react");
|
|
@@ -22217,13 +22763,13 @@ const noReactChildren = defineRule({
|
|
|
22217
22763
|
if (isChildrenIdentifier(memberObject, node)) {
|
|
22218
22764
|
context.report({
|
|
22219
22765
|
node: calleeOuter,
|
|
22220
|
-
message: MESSAGE$
|
|
22766
|
+
message: MESSAGE$18
|
|
22221
22767
|
});
|
|
22222
22768
|
return;
|
|
22223
22769
|
}
|
|
22224
22770
|
if (isReactNamespaceMember(memberObject, node)) context.report({
|
|
22225
22771
|
node: calleeOuter,
|
|
22226
|
-
message: MESSAGE$
|
|
22772
|
+
message: MESSAGE$18
|
|
22227
22773
|
});
|
|
22228
22774
|
} })
|
|
22229
22775
|
});
|
|
@@ -22334,6 +22880,86 @@ const noReact19DeprecatedApis = defineRule({
|
|
|
22334
22880
|
})
|
|
22335
22881
|
});
|
|
22336
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
|
|
22337
22963
|
//#region src/plugin/constants/aria-element-roles.ts
|
|
22338
22964
|
const ELEMENT_ROLE_PAIRS = [
|
|
22339
22965
|
["a", "link"],
|
|
@@ -22546,7 +23172,7 @@ const noRenderPropChildren = defineRule({
|
|
|
22546
23172
|
});
|
|
22547
23173
|
//#endregion
|
|
22548
23174
|
//#region src/plugin/rules/react-builtins/no-render-return-value.ts
|
|
22549
|
-
const MESSAGE$
|
|
23175
|
+
const MESSAGE$17 = "Your app breaks in React 19 because `ReactDOM.render` returns nothing there.";
|
|
22550
23176
|
const isReactDomRenderCall = (node) => {
|
|
22551
23177
|
if (!isNodeOfType(node.callee, "MemberExpression")) return false;
|
|
22552
23178
|
if (!isNodeOfType(node.callee.object, "Identifier")) return false;
|
|
@@ -22570,7 +23196,7 @@ const noRenderReturnValue = defineRule({
|
|
|
22570
23196
|
if (!isUsedAsReturnValue(node.parent)) return;
|
|
22571
23197
|
context.report({
|
|
22572
23198
|
node: node.callee,
|
|
22573
|
-
message: MESSAGE$
|
|
23199
|
+
message: MESSAGE$17
|
|
22574
23200
|
});
|
|
22575
23201
|
} })
|
|
22576
23202
|
});
|
|
@@ -22730,11 +23356,17 @@ const classifySecretFileExposure = (filename, options = {}) => {
|
|
|
22730
23356
|
return "unknown";
|
|
22731
23357
|
};
|
|
22732
23358
|
//#endregion
|
|
22733
|
-
//#region src/plugin/utils/
|
|
22734
|
-
const
|
|
22735
|
-
|
|
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());
|
|
22736
23365
|
};
|
|
22737
23366
|
//#endregion
|
|
23367
|
+
//#region src/plugin/utils/get-identifier-trailing-word.ts
|
|
23368
|
+
const getIdentifierTrailingWord = (identifierName) => tokenizeIdentifierWords(identifierName).at(-1) ?? identifierName.toLowerCase();
|
|
23369
|
+
//#endregion
|
|
22738
23370
|
//#region src/plugin/constants/tanstack.ts
|
|
22739
23371
|
const TANSTACK_ROUTE_FILE_PATTERN = /\/routes\//;
|
|
22740
23372
|
const TANSTACK_ROOT_ROUTE_FILE_PATTERN = /__root\.(tsx?|jsx?)$/;
|
|
@@ -23262,7 +23894,7 @@ const getParentComponent = (node) => {
|
|
|
23262
23894
|
};
|
|
23263
23895
|
//#endregion
|
|
23264
23896
|
//#region src/plugin/rules/react-builtins/no-set-state.ts
|
|
23265
|
-
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.";
|
|
23266
23898
|
const noSetState = defineRule({
|
|
23267
23899
|
id: "no-set-state",
|
|
23268
23900
|
title: "Local class state forbidden",
|
|
@@ -23277,7 +23909,7 @@ const noSetState = defineRule({
|
|
|
23277
23909
|
if (!getParentComponent(node)) return;
|
|
23278
23910
|
context.report({
|
|
23279
23911
|
node: node.callee,
|
|
23280
|
-
message: MESSAGE$
|
|
23912
|
+
message: MESSAGE$16
|
|
23281
23913
|
});
|
|
23282
23914
|
} })
|
|
23283
23915
|
});
|
|
@@ -23439,7 +24071,7 @@ const isAbstractRole = (openingElement, settings) => {
|
|
|
23439
24071
|
};
|
|
23440
24072
|
//#endregion
|
|
23441
24073
|
//#region src/plugin/rules/a11y/no-static-element-interactions.ts
|
|
23442
|
-
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.";
|
|
23443
24075
|
const DEFAULT_HANDLERS = [
|
|
23444
24076
|
"onClick",
|
|
23445
24077
|
"onMouseDown",
|
|
@@ -23499,7 +24131,7 @@ const noStaticElementInteractions = defineRule({
|
|
|
23499
24131
|
if (!roleAttribute || !roleAttribute.value) {
|
|
23500
24132
|
context.report({
|
|
23501
24133
|
node: node.name,
|
|
23502
|
-
message: MESSAGE$
|
|
24134
|
+
message: MESSAGE$15
|
|
23503
24135
|
});
|
|
23504
24136
|
return;
|
|
23505
24137
|
}
|
|
@@ -23509,19 +24141,66 @@ const noStaticElementInteractions = defineRule({
|
|
|
23509
24141
|
if (firstRole && (isInteractiveRole(firstRole) || isNonInteractiveRole(firstRole))) return;
|
|
23510
24142
|
context.report({
|
|
23511
24143
|
node: node.name,
|
|
23512
|
-
message: MESSAGE$
|
|
24144
|
+
message: MESSAGE$15
|
|
23513
24145
|
});
|
|
23514
24146
|
return;
|
|
23515
24147
|
}
|
|
23516
24148
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer") && settings.allowExpressionValues) return;
|
|
23517
24149
|
context.report({
|
|
23518
24150
|
node: node.name,
|
|
23519
|
-
message: MESSAGE$
|
|
24151
|
+
message: MESSAGE$15
|
|
23520
24152
|
});
|
|
23521
24153
|
} };
|
|
23522
24154
|
}
|
|
23523
24155
|
});
|
|
23524
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
|
|
23525
24204
|
//#region src/plugin/rules/react-builtins/no-string-refs.ts
|
|
23526
24205
|
const STRING_IN_REF_MESSAGE = "Your component can't reach this node because string refs don't work in modern React.";
|
|
23527
24206
|
const THIS_REFS_MESSAGE = "Your component can't reach its nodes because `this.refs` is empty in modern React.";
|
|
@@ -23572,8 +24251,154 @@ const noStringRefs = defineRule({
|
|
|
23572
24251
|
}
|
|
23573
24252
|
});
|
|
23574
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
|
|
23575
24400
|
//#region src/plugin/rules/react-builtins/no-this-in-sfc.ts
|
|
23576
|
-
const MESSAGE$
|
|
24401
|
+
const MESSAGE$12 = "This value is `undefined` because function components have no `this`.";
|
|
23577
24402
|
const isInsideClassMethod = (node, customClassFactoryNames) => {
|
|
23578
24403
|
let ancestor = node.parent;
|
|
23579
24404
|
while (ancestor) {
|
|
@@ -23642,7 +24467,7 @@ const noThisInSfc = defineRule({
|
|
|
23642
24467
|
if (!looksLikeFunctionComponent(enclosingFunction)) return;
|
|
23643
24468
|
context.report({
|
|
23644
24469
|
node,
|
|
23645
|
-
message: MESSAGE$
|
|
24470
|
+
message: MESSAGE$12
|
|
23646
24471
|
});
|
|
23647
24472
|
} };
|
|
23648
24473
|
}
|
|
@@ -23680,26 +24505,39 @@ const noTinyText = defineRule({
|
|
|
23680
24505
|
});
|
|
23681
24506
|
//#endregion
|
|
23682
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`.";
|
|
23683
24510
|
const noTransitionAll = defineRule({
|
|
23684
24511
|
id: "no-transition-all",
|
|
23685
24512
|
title: "transition: all animates everything",
|
|
23686
24513
|
tags: ["test-noise"],
|
|
23687
24514
|
severity: "warn",
|
|
23688
24515
|
recommendation: "List the specific properties: `transition: \"opacity 200ms, transform 200ms\"`. In Tailwind, use `transition-colors`, `transition-opacity`, or `transition-transform`",
|
|
23689
|
-
create: (context) => ({
|
|
23690
|
-
|
|
23691
|
-
|
|
23692
|
-
|
|
23693
|
-
|
|
23694
|
-
|
|
23695
|
-
|
|
23696
|
-
|
|
23697
|
-
|
|
23698
|
-
|
|
23699
|
-
|
|
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
|
|
23700
24538
|
});
|
|
23701
24539
|
}
|
|
23702
|
-
}
|
|
24540
|
+
})
|
|
23703
24541
|
});
|
|
23704
24542
|
//#endregion
|
|
23705
24543
|
//#region src/plugin/rules/correctness/no-uncontrolled-input.ts
|
|
@@ -23743,7 +24581,6 @@ const collectUndefinedInitialStateNames = (componentBody) => {
|
|
|
23743
24581
|
}
|
|
23744
24582
|
return stateNames;
|
|
23745
24583
|
};
|
|
23746
|
-
const hasJsxSpreadAttribute = (attributes) => attributes.some((attribute) => isNodeOfType(attribute, "JSXSpreadAttribute"));
|
|
23747
24584
|
const noUncontrolledInput = defineRule({
|
|
23748
24585
|
id: "no-uncontrolled-input",
|
|
23749
24586
|
title: "Uncontrolled input value",
|
|
@@ -23847,6 +24684,38 @@ const noUnescapedEntities = defineRule({
|
|
|
23847
24684
|
} })
|
|
23848
24685
|
});
|
|
23849
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
|
|
23850
24719
|
//#region src/plugin/constants/dom-aria-properties.ts
|
|
23851
24720
|
const ARIA_PROPERTY_NAMES = new Set([
|
|
23852
24721
|
"activedescendant",
|
|
@@ -25318,7 +26187,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
25318
26187
|
//#endregion
|
|
25319
26188
|
//#region src/plugin/rules/react-builtins/no-will-update-set-state.ts
|
|
25320
26189
|
const LIFECYCLE_NAMES = new Set(["componentWillUpdate", "UNSAFE_componentWillUpdate"]);
|
|
25321
|
-
const MESSAGE$
|
|
26190
|
+
const MESSAGE$10 = "Calling setState in componentWillUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
25322
26191
|
const resolveSettings$7 = (settings) => {
|
|
25323
26192
|
const reactDoctor = settings?.["react-doctor"];
|
|
25324
26193
|
return { mode: (typeof reactDoctor === "object" && reactDoctor !== null ? reactDoctor.noWillUpdateSetState ?? {} : {}).mode ?? "allowed" };
|
|
@@ -25352,7 +26221,7 @@ const noWillUpdateSetState = defineRule({
|
|
|
25352
26221
|
if (!isSetStateCallInLifecycle(node, activeLifecycleNames, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
25353
26222
|
context.report({
|
|
25354
26223
|
node: node.callee,
|
|
25355
|
-
message: MESSAGE$
|
|
26224
|
+
message: MESSAGE$10
|
|
25356
26225
|
});
|
|
25357
26226
|
} };
|
|
25358
26227
|
}
|
|
@@ -26230,7 +27099,7 @@ const preactNoRenderArguments = defineRule({
|
|
|
26230
27099
|
});
|
|
26231
27100
|
//#endregion
|
|
26232
27101
|
//#region src/plugin/rules/preact/preact-prefer-ondblclick.ts
|
|
26233
|
-
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.";
|
|
26234
27103
|
const preactPreferOndblclick = defineRule({
|
|
26235
27104
|
id: "preact-prefer-ondblclick",
|
|
26236
27105
|
title: "onDoubleClick instead of onDblClick",
|
|
@@ -26245,7 +27114,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
26245
27114
|
if (!onDoubleClickAttribute) return;
|
|
26246
27115
|
context.report({
|
|
26247
27116
|
node: onDoubleClickAttribute,
|
|
26248
|
-
message: MESSAGE$
|
|
27117
|
+
message: MESSAGE$9
|
|
26249
27118
|
});
|
|
26250
27119
|
} })
|
|
26251
27120
|
});
|
|
@@ -26285,6 +27154,42 @@ const preactPreferOninput = defineRule({
|
|
|
26285
27154
|
} })
|
|
26286
27155
|
});
|
|
26287
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
|
|
26288
27193
|
//#region src/plugin/rules/bundle-size/prefer-dynamic-import.ts
|
|
26289
27194
|
const preferDynamicImport = defineRule({
|
|
26290
27195
|
id: "prefer-dynamic-import",
|
|
@@ -26876,6 +27781,26 @@ const preferTagOverRole = defineRule({
|
|
|
26876
27781
|
} })
|
|
26877
27782
|
});
|
|
26878
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
|
|
26879
27804
|
//#region src/plugin/rules/state-and-effects/prefer-use-effect-event.ts
|
|
26880
27805
|
const collectFunctionTypedLocalBindings = (componentBody) => {
|
|
26881
27806
|
const functionTypedLocals = /* @__PURE__ */ new Set();
|
|
@@ -34681,6 +35606,47 @@ const serverAfterNonblocking = defineRule({
|
|
|
34681
35606
|
}
|
|
34682
35607
|
});
|
|
34683
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
|
|
34684
35650
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
34685
35651
|
const isAsyncFunctionLikeNode = (node) => {
|
|
34686
35652
|
if (!node) return false;
|
|
@@ -34723,9 +35689,13 @@ const isMemberCallAuthRelated = (receiverNode, methodName, genericMethodNames) =
|
|
|
34723
35689
|
const getAuthCallName = (callExpression, allowedFunctionNames, genericMethodNames) => {
|
|
34724
35690
|
const calleeNode = unwrapTypeWrappedCallee(callExpression.callee);
|
|
34725
35691
|
if (!calleeNode) return null;
|
|
34726
|
-
if (isNodeOfType(calleeNode, "Identifier"))
|
|
35692
|
+
if (isNodeOfType(calleeNode, "Identifier")) {
|
|
35693
|
+
const calleeName = calleeNode.name;
|
|
35694
|
+
return allowedFunctionNames.has(calleeName) || isAuthGuardName(calleeName) ? calleeName : null;
|
|
35695
|
+
}
|
|
34727
35696
|
if (isNodeOfType(calleeNode, "MemberExpression") && isNodeOfType(calleeNode.property, "Identifier")) {
|
|
34728
35697
|
const methodName = calleeNode.property.name;
|
|
35698
|
+
if (isAuthGuardName(methodName)) return methodName;
|
|
34729
35699
|
if (!allowedFunctionNames.has(methodName)) return null;
|
|
34730
35700
|
if (!isMemberCallAuthRelated(calleeNode.object, methodName, genericMethodNames)) return null;
|
|
34731
35701
|
return methodName;
|
|
@@ -35102,13 +36072,7 @@ const serverNoMutableModuleState = defineRule({
|
|
|
35102
36072
|
const collectDeclaredNames = (declaration) => {
|
|
35103
36073
|
const names = /* @__PURE__ */ new Set();
|
|
35104
36074
|
if (!isNodeOfType(declaration, "VariableDeclaration")) return names;
|
|
35105
|
-
for (const declarator of declaration.declarations ?? [])
|
|
35106
|
-
else if (isNodeOfType(declarator.id, "ObjectPattern")) {
|
|
35107
|
-
for (const property of declarator.id.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.value, "Identifier")) names.add(property.value.name);
|
|
35108
|
-
else if (isNodeOfType(property, "RestElement") && isNodeOfType(property.argument, "Identifier")) names.add(property.argument.name);
|
|
35109
|
-
} else if (isNodeOfType(declarator.id, "ArrayPattern")) {
|
|
35110
|
-
for (const element of declarator.id.elements ?? []) if (isNodeOfType(element, "Identifier")) names.add(element.name);
|
|
35111
|
-
}
|
|
36075
|
+
for (const declarator of declaration.declarations ?? []) collectPatternNames(declarator.id, names);
|
|
35112
36076
|
return names;
|
|
35113
36077
|
};
|
|
35114
36078
|
const declarationStartsWithAwait = (declaration) => {
|
|
@@ -35118,11 +36082,15 @@ const declarationStartsWithAwait = (declaration) => {
|
|
|
35118
36082
|
};
|
|
35119
36083
|
const declarationReadsAnyName = (declaration, names) => {
|
|
35120
36084
|
if (names.size === 0) return false;
|
|
36085
|
+
if (!isNodeOfType(declaration, "VariableDeclaration")) return false;
|
|
35121
36086
|
let didRead = false;
|
|
35122
|
-
|
|
35123
|
-
if (
|
|
35124
|
-
|
|
35125
|
-
|
|
36087
|
+
for (const declarator of declaration.declarations ?? []) {
|
|
36088
|
+
if (!declarator.init) continue;
|
|
36089
|
+
walkAst(declarator.init, (child) => {
|
|
36090
|
+
if (didRead) return;
|
|
36091
|
+
if (isNodeOfType(child, "Identifier") && names.has(child.name)) didRead = true;
|
|
36092
|
+
});
|
|
36093
|
+
}
|
|
35126
36094
|
return didRead;
|
|
35127
36095
|
};
|
|
35128
36096
|
const serverSequentialIndependentAwait = defineRule({
|
|
@@ -36382,7 +37350,7 @@ const urlPrefilledPrivilegedAction = defineRule({
|
|
|
36382
37350
|
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
36383
37351
|
scan: scanByPattern({
|
|
36384
37352
|
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
36385
|
-
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
37353
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?(?:[\w$]+\s*\.\s*){0,4})\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
36386
37354
|
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
36387
37355
|
})
|
|
36388
37356
|
});
|
|
@@ -37144,6 +38112,17 @@ const reactDoctorRules = [
|
|
|
37144
38112
|
category: "Performance"
|
|
37145
38113
|
}
|
|
37146
38114
|
},
|
|
38115
|
+
{
|
|
38116
|
+
key: "react-doctor/auth-token-in-web-storage",
|
|
38117
|
+
id: "auth-token-in-web-storage",
|
|
38118
|
+
source: "react-doctor",
|
|
38119
|
+
originallyExternal: false,
|
|
38120
|
+
rule: {
|
|
38121
|
+
...authTokenInWebStorage,
|
|
38122
|
+
framework: "global",
|
|
38123
|
+
category: "Security"
|
|
38124
|
+
}
|
|
38125
|
+
},
|
|
37147
38126
|
{
|
|
37148
38127
|
key: "react-doctor/autocomplete-valid",
|
|
37149
38128
|
id: "autocomplete-valid",
|
|
@@ -37360,6 +38339,18 @@ const reactDoctorRules = [
|
|
|
37360
38339
|
requires: [...new Set(["react", ...noVagueButtonLabel.requires ?? []])]
|
|
37361
38340
|
}
|
|
37362
38341
|
},
|
|
38342
|
+
{
|
|
38343
|
+
key: "react-doctor/dialog-has-accessible-name",
|
|
38344
|
+
id: "dialog-has-accessible-name",
|
|
38345
|
+
source: "react-doctor",
|
|
38346
|
+
originallyExternal: false,
|
|
38347
|
+
rule: {
|
|
38348
|
+
...dialogHasAccessibleName,
|
|
38349
|
+
framework: "global",
|
|
38350
|
+
category: "Accessibility",
|
|
38351
|
+
requires: [...new Set(["react", ...dialogHasAccessibleName.requires ?? []])]
|
|
38352
|
+
}
|
|
38353
|
+
},
|
|
37363
38354
|
{
|
|
37364
38355
|
key: "react-doctor/display-name",
|
|
37365
38356
|
id: "display-name",
|
|
@@ -38484,6 +39475,17 @@ const reactDoctorRules = [
|
|
|
38484
39475
|
requires: [...new Set(["react", ...noAdjustStateOnPropChange.requires ?? []])]
|
|
38485
39476
|
}
|
|
38486
39477
|
},
|
|
39478
|
+
{
|
|
39479
|
+
key: "react-doctor/no-arbitrary-px-font-size",
|
|
39480
|
+
id: "no-arbitrary-px-font-size",
|
|
39481
|
+
source: "react-doctor",
|
|
39482
|
+
originallyExternal: false,
|
|
39483
|
+
rule: {
|
|
39484
|
+
...noArbitraryPxFontSize,
|
|
39485
|
+
framework: "global",
|
|
39486
|
+
category: "Accessibility"
|
|
39487
|
+
}
|
|
39488
|
+
},
|
|
38487
39489
|
{
|
|
38488
39490
|
key: "react-doctor/no-aria-hidden-on-focusable",
|
|
38489
39491
|
id: "no-aria-hidden-on-focusable",
|
|
@@ -38519,6 +39521,18 @@ const reactDoctorRules = [
|
|
|
38519
39521
|
requires: [...new Set(["react", ...noArrayIndexKey.requires ?? []])]
|
|
38520
39522
|
}
|
|
38521
39523
|
},
|
|
39524
|
+
{
|
|
39525
|
+
key: "react-doctor/no-async-effect-callback",
|
|
39526
|
+
id: "no-async-effect-callback",
|
|
39527
|
+
source: "react-doctor",
|
|
39528
|
+
originallyExternal: false,
|
|
39529
|
+
rule: {
|
|
39530
|
+
...noAsyncEffectCallback,
|
|
39531
|
+
framework: "global",
|
|
39532
|
+
category: "Bugs",
|
|
39533
|
+
requires: [...new Set(["react", ...noAsyncEffectCallback.requires ?? []])]
|
|
39534
|
+
}
|
|
39535
|
+
},
|
|
38522
39536
|
{
|
|
38523
39537
|
key: "react-doctor/no-autofocus",
|
|
38524
39538
|
id: "no-autofocus",
|
|
@@ -38531,6 +39545,18 @@ const reactDoctorRules = [
|
|
|
38531
39545
|
requires: [...new Set(["react", ...noAutofocus.requires ?? []])]
|
|
38532
39546
|
}
|
|
38533
39547
|
},
|
|
39548
|
+
{
|
|
39549
|
+
key: "react-doctor/no-autoplay-without-muted",
|
|
39550
|
+
id: "no-autoplay-without-muted",
|
|
39551
|
+
source: "react-doctor",
|
|
39552
|
+
originallyExternal: false,
|
|
39553
|
+
rule: {
|
|
39554
|
+
...noAutoplayWithoutMuted,
|
|
39555
|
+
framework: "global",
|
|
39556
|
+
category: "Accessibility",
|
|
39557
|
+
requires: [...new Set(["react", ...noAutoplayWithoutMuted.requires ?? []])]
|
|
39558
|
+
}
|
|
39559
|
+
},
|
|
38534
39560
|
{
|
|
38535
39561
|
key: "react-doctor/no-barrel-import",
|
|
38536
39562
|
id: "no-barrel-import",
|
|
@@ -38542,6 +39568,18 @@ const reactDoctorRules = [
|
|
|
38542
39568
|
category: "Performance"
|
|
38543
39569
|
}
|
|
38544
39570
|
},
|
|
39571
|
+
{
|
|
39572
|
+
key: "react-doctor/no-call-component-as-function",
|
|
39573
|
+
id: "no-call-component-as-function",
|
|
39574
|
+
source: "react-doctor",
|
|
39575
|
+
originallyExternal: false,
|
|
39576
|
+
rule: {
|
|
39577
|
+
...noCallComponentAsFunction,
|
|
39578
|
+
framework: "global",
|
|
39579
|
+
category: "Bugs",
|
|
39580
|
+
requires: [...new Set(["react", ...noCallComponentAsFunction.requires ?? []])]
|
|
39581
|
+
}
|
|
39582
|
+
},
|
|
38545
39583
|
{
|
|
38546
39584
|
key: "react-doctor/no-cascading-set-state",
|
|
38547
39585
|
id: "no-cascading-set-state",
|
|
@@ -38602,6 +39640,18 @@ const reactDoctorRules = [
|
|
|
38602
39640
|
requires: [...new Set(["react", ...noCreateContextInRender.requires ?? []])]
|
|
38603
39641
|
}
|
|
38604
39642
|
},
|
|
39643
|
+
{
|
|
39644
|
+
key: "react-doctor/no-create-ref-in-function-component",
|
|
39645
|
+
id: "no-create-ref-in-function-component",
|
|
39646
|
+
source: "react-doctor",
|
|
39647
|
+
originallyExternal: false,
|
|
39648
|
+
rule: {
|
|
39649
|
+
...noCreateRefInFunctionComponent,
|
|
39650
|
+
framework: "global",
|
|
39651
|
+
category: "Bugs",
|
|
39652
|
+
requires: [...new Set(["react", ...noCreateRefInFunctionComponent.requires ?? []])]
|
|
39653
|
+
}
|
|
39654
|
+
},
|
|
38605
39655
|
{
|
|
38606
39656
|
key: "react-doctor/no-create-store-in-render",
|
|
38607
39657
|
id: "no-create-store-in-render",
|
|
@@ -38660,6 +39710,17 @@ const reactDoctorRules = [
|
|
|
38660
39710
|
category: "Maintainability"
|
|
38661
39711
|
}
|
|
38662
39712
|
},
|
|
39713
|
+
{
|
|
39714
|
+
key: "react-doctor/no-deprecated-tailwind-class",
|
|
39715
|
+
id: "no-deprecated-tailwind-class",
|
|
39716
|
+
source: "react-doctor",
|
|
39717
|
+
originallyExternal: false,
|
|
39718
|
+
rule: {
|
|
39719
|
+
...noDeprecatedTailwindClass,
|
|
39720
|
+
framework: "global",
|
|
39721
|
+
category: "Maintainability"
|
|
39722
|
+
}
|
|
39723
|
+
},
|
|
38663
39724
|
{
|
|
38664
39725
|
key: "react-doctor/no-derived-state",
|
|
38665
39726
|
id: "no-derived-state",
|
|
@@ -38779,6 +39840,17 @@ const reactDoctorRules = [
|
|
|
38779
39840
|
requires: [...new Set(["react", ...noDocumentStartViewTransition.requires ?? []])]
|
|
38780
39841
|
}
|
|
38781
39842
|
},
|
|
39843
|
+
{
|
|
39844
|
+
key: "react-doctor/no-document-write",
|
|
39845
|
+
id: "no-document-write",
|
|
39846
|
+
source: "react-doctor",
|
|
39847
|
+
originallyExternal: false,
|
|
39848
|
+
rule: {
|
|
39849
|
+
...noDocumentWrite,
|
|
39850
|
+
framework: "global",
|
|
39851
|
+
category: "Performance"
|
|
39852
|
+
}
|
|
39853
|
+
},
|
|
38782
39854
|
{
|
|
38783
39855
|
key: "react-doctor/no-dynamic-import-path",
|
|
38784
39856
|
id: "no-dynamic-import-path",
|
|
@@ -38920,6 +39992,17 @@ const reactDoctorRules = [
|
|
|
38920
39992
|
category: "Performance"
|
|
38921
39993
|
}
|
|
38922
39994
|
},
|
|
39995
|
+
{
|
|
39996
|
+
key: "react-doctor/no-full-viewport-width",
|
|
39997
|
+
id: "no-full-viewport-width",
|
|
39998
|
+
source: "react-doctor",
|
|
39999
|
+
originallyExternal: false,
|
|
40000
|
+
rule: {
|
|
40001
|
+
...noFullViewportWidth,
|
|
40002
|
+
framework: "global",
|
|
40003
|
+
category: "Maintainability"
|
|
40004
|
+
}
|
|
40005
|
+
},
|
|
38923
40006
|
{
|
|
38924
40007
|
key: "react-doctor/no-generic-handler-names",
|
|
38925
40008
|
id: "no-generic-handler-names",
|
|
@@ -38976,6 +40059,18 @@ const reactDoctorRules = [
|
|
|
38976
40059
|
category: "Accessibility"
|
|
38977
40060
|
}
|
|
38978
40061
|
},
|
|
40062
|
+
{
|
|
40063
|
+
key: "react-doctor/no-img-lazy-with-high-fetchpriority",
|
|
40064
|
+
id: "no-img-lazy-with-high-fetchpriority",
|
|
40065
|
+
source: "react-doctor",
|
|
40066
|
+
originallyExternal: false,
|
|
40067
|
+
rule: {
|
|
40068
|
+
...noImgLazyWithHighFetchpriority,
|
|
40069
|
+
framework: "global",
|
|
40070
|
+
category: "Performance",
|
|
40071
|
+
requires: [...new Set(["react", ...noImgLazyWithHighFetchpriority.requires ?? []])]
|
|
40072
|
+
}
|
|
40073
|
+
},
|
|
38979
40074
|
{
|
|
38980
40075
|
key: "react-doctor/no-initialize-state",
|
|
38981
40076
|
id: "no-initialize-state",
|
|
@@ -39046,6 +40141,17 @@ const reactDoctorRules = [
|
|
|
39046
40141
|
requires: [...new Set(["react", ...noIsMounted.requires ?? []])]
|
|
39047
40142
|
}
|
|
39048
40143
|
},
|
|
40144
|
+
{
|
|
40145
|
+
key: "react-doctor/no-json-parse-stringify-clone",
|
|
40146
|
+
id: "no-json-parse-stringify-clone",
|
|
40147
|
+
source: "react-doctor",
|
|
40148
|
+
originallyExternal: false,
|
|
40149
|
+
rule: {
|
|
40150
|
+
...noJsonParseStringifyClone,
|
|
40151
|
+
framework: "global",
|
|
40152
|
+
category: "Performance"
|
|
40153
|
+
}
|
|
40154
|
+
},
|
|
39049
40155
|
{
|
|
39050
40156
|
key: "react-doctor/no-jsx-element-type",
|
|
39051
40157
|
id: "no-jsx-element-type",
|
|
@@ -39136,6 +40242,17 @@ const reactDoctorRules = [
|
|
|
39136
40242
|
category: "Performance"
|
|
39137
40243
|
}
|
|
39138
40244
|
},
|
|
40245
|
+
{
|
|
40246
|
+
key: "react-doctor/no-low-contrast-inline-style",
|
|
40247
|
+
id: "no-low-contrast-inline-style",
|
|
40248
|
+
source: "react-doctor",
|
|
40249
|
+
originallyExternal: false,
|
|
40250
|
+
rule: {
|
|
40251
|
+
...noLowContrastInlineStyle,
|
|
40252
|
+
framework: "global",
|
|
40253
|
+
category: "Accessibility"
|
|
40254
|
+
}
|
|
40255
|
+
},
|
|
39139
40256
|
{
|
|
39140
40257
|
key: "react-doctor/no-many-boolean-props",
|
|
39141
40258
|
id: "no-many-boolean-props",
|
|
@@ -39413,6 +40530,17 @@ const reactDoctorRules = [
|
|
|
39413
40530
|
category: "Maintainability"
|
|
39414
40531
|
}
|
|
39415
40532
|
},
|
|
40533
|
+
{
|
|
40534
|
+
key: "react-doctor/no-redundant-display-class",
|
|
40535
|
+
id: "no-redundant-display-class",
|
|
40536
|
+
source: "react-doctor",
|
|
40537
|
+
originallyExternal: false,
|
|
40538
|
+
rule: {
|
|
40539
|
+
...noRedundantDisplayClass,
|
|
40540
|
+
framework: "global",
|
|
40541
|
+
category: "Maintainability"
|
|
40542
|
+
}
|
|
40543
|
+
},
|
|
39416
40544
|
{
|
|
39417
40545
|
key: "react-doctor/no-redundant-roles",
|
|
39418
40546
|
id: "no-redundant-roles",
|
|
@@ -39565,6 +40693,18 @@ const reactDoctorRules = [
|
|
|
39565
40693
|
requires: [...new Set(["react", ...noStaticElementInteractions.requires ?? []])]
|
|
39566
40694
|
}
|
|
39567
40695
|
},
|
|
40696
|
+
{
|
|
40697
|
+
key: "react-doctor/no-string-false-on-boolean-attribute",
|
|
40698
|
+
id: "no-string-false-on-boolean-attribute",
|
|
40699
|
+
source: "react-doctor",
|
|
40700
|
+
originallyExternal: false,
|
|
40701
|
+
rule: {
|
|
40702
|
+
...noStringFalseOnBooleanAttribute,
|
|
40703
|
+
framework: "global",
|
|
40704
|
+
category: "Bugs",
|
|
40705
|
+
requires: [...new Set(["react", ...noStringFalseOnBooleanAttribute.requires ?? []])]
|
|
40706
|
+
}
|
|
40707
|
+
},
|
|
39568
40708
|
{
|
|
39569
40709
|
key: "react-doctor/no-string-refs",
|
|
39570
40710
|
id: "no-string-refs",
|
|
@@ -39577,6 +40717,51 @@ const reactDoctorRules = [
|
|
|
39577
40717
|
requires: [...new Set(["react", ...noStringRefs.requires ?? []])]
|
|
39578
40718
|
}
|
|
39579
40719
|
},
|
|
40720
|
+
{
|
|
40721
|
+
key: "react-doctor/no-svg-currentcolor-with-fill-class",
|
|
40722
|
+
id: "no-svg-currentcolor-with-fill-class",
|
|
40723
|
+
source: "react-doctor",
|
|
40724
|
+
originallyExternal: false,
|
|
40725
|
+
rule: {
|
|
40726
|
+
...noSvgCurrentcolorWithFillClass,
|
|
40727
|
+
framework: "global",
|
|
40728
|
+
category: "Maintainability"
|
|
40729
|
+
}
|
|
40730
|
+
},
|
|
40731
|
+
{
|
|
40732
|
+
key: "react-doctor/no-sync-xhr",
|
|
40733
|
+
id: "no-sync-xhr",
|
|
40734
|
+
source: "react-doctor",
|
|
40735
|
+
originallyExternal: false,
|
|
40736
|
+
rule: {
|
|
40737
|
+
...noSyncXhr,
|
|
40738
|
+
framework: "global",
|
|
40739
|
+
category: "Performance"
|
|
40740
|
+
}
|
|
40741
|
+
},
|
|
40742
|
+
{
|
|
40743
|
+
key: "react-doctor/no-tailwind-layout-transition",
|
|
40744
|
+
id: "no-tailwind-layout-transition",
|
|
40745
|
+
source: "react-doctor",
|
|
40746
|
+
originallyExternal: false,
|
|
40747
|
+
rule: {
|
|
40748
|
+
...noTailwindLayoutTransition,
|
|
40749
|
+
framework: "global",
|
|
40750
|
+
category: "Performance"
|
|
40751
|
+
}
|
|
40752
|
+
},
|
|
40753
|
+
{
|
|
40754
|
+
key: "react-doctor/no-target-blank-without-rel",
|
|
40755
|
+
id: "no-target-blank-without-rel",
|
|
40756
|
+
source: "react-doctor",
|
|
40757
|
+
originallyExternal: false,
|
|
40758
|
+
rule: {
|
|
40759
|
+
...noTargetBlankWithoutRel,
|
|
40760
|
+
framework: "global",
|
|
40761
|
+
category: "Accessibility",
|
|
40762
|
+
requires: [...new Set(["react", ...noTargetBlankWithoutRel.requires ?? []])]
|
|
40763
|
+
}
|
|
40764
|
+
},
|
|
39580
40765
|
{
|
|
39581
40766
|
key: "react-doctor/no-this-in-sfc",
|
|
39582
40767
|
id: "no-this-in-sfc",
|
|
@@ -39646,6 +40831,18 @@ const reactDoctorRules = [
|
|
|
39646
40831
|
requires: [...new Set(["react", ...noUnescapedEntities.requires ?? []])]
|
|
39647
40832
|
}
|
|
39648
40833
|
},
|
|
40834
|
+
{
|
|
40835
|
+
key: "react-doctor/no-uninformative-aria-label",
|
|
40836
|
+
id: "no-uninformative-aria-label",
|
|
40837
|
+
source: "react-doctor",
|
|
40838
|
+
originallyExternal: false,
|
|
40839
|
+
rule: {
|
|
40840
|
+
...noUninformativeAriaLabel,
|
|
40841
|
+
framework: "global",
|
|
40842
|
+
category: "Accessibility",
|
|
40843
|
+
requires: [...new Set(["react", ...noUninformativeAriaLabel.requires ?? []])]
|
|
40844
|
+
}
|
|
40845
|
+
},
|
|
39649
40846
|
{
|
|
39650
40847
|
key: "react-doctor/no-unknown-property",
|
|
39651
40848
|
id: "no-unknown-property",
|
|
@@ -39855,6 +41052,17 @@ const reactDoctorRules = [
|
|
|
39855
41052
|
category: "Bugs"
|
|
39856
41053
|
}
|
|
39857
41054
|
},
|
|
41055
|
+
{
|
|
41056
|
+
key: "react-doctor/prefer-dvh-over-vh",
|
|
41057
|
+
id: "prefer-dvh-over-vh",
|
|
41058
|
+
source: "react-doctor",
|
|
41059
|
+
originallyExternal: false,
|
|
41060
|
+
rule: {
|
|
41061
|
+
...preferDvhOverVh,
|
|
41062
|
+
framework: "global",
|
|
41063
|
+
category: "Maintainability"
|
|
41064
|
+
}
|
|
41065
|
+
},
|
|
39858
41066
|
{
|
|
39859
41067
|
key: "react-doctor/prefer-dynamic-import",
|
|
39860
41068
|
id: "prefer-dynamic-import",
|
|
@@ -39959,6 +41167,17 @@ const reactDoctorRules = [
|
|
|
39959
41167
|
requires: [...new Set(["react", ...preferTagOverRole.requires ?? []])]
|
|
39960
41168
|
}
|
|
39961
41169
|
},
|
|
41170
|
+
{
|
|
41171
|
+
key: "react-doctor/prefer-truncate-shorthand",
|
|
41172
|
+
id: "prefer-truncate-shorthand",
|
|
41173
|
+
source: "react-doctor",
|
|
41174
|
+
originallyExternal: false,
|
|
41175
|
+
rule: {
|
|
41176
|
+
...preferTruncateShorthand,
|
|
41177
|
+
framework: "global",
|
|
41178
|
+
category: "Maintainability"
|
|
41179
|
+
}
|
|
41180
|
+
},
|
|
39962
41181
|
{
|
|
39963
41182
|
key: "react-doctor/prefer-use-effect-event",
|
|
39964
41183
|
id: "prefer-use-effect-event",
|