oxlint-plugin-react-doctor 0.7.2-dev.b97a92f → 0.7.2-dev.cb8f726
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +251 -135
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -595,6 +595,19 @@ const TRIVIAL_INITIALIZER_NAMES = new Set([
|
|
|
595
595
|
"parseInt",
|
|
596
596
|
"parseFloat"
|
|
597
597
|
]);
|
|
598
|
+
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
599
|
+
"Date",
|
|
600
|
+
"Map",
|
|
601
|
+
"Set",
|
|
602
|
+
"WeakMap",
|
|
603
|
+
"WeakSet",
|
|
604
|
+
"WeakRef",
|
|
605
|
+
"RegExp",
|
|
606
|
+
"Error",
|
|
607
|
+
"URL",
|
|
608
|
+
"URLSearchParams",
|
|
609
|
+
"AbortController"
|
|
610
|
+
]);
|
|
598
611
|
const TRIVIAL_DERIVATION_CALLEE_NAMES = new Set([
|
|
599
612
|
"Boolean",
|
|
600
613
|
"String",
|
|
@@ -2235,6 +2248,38 @@ const anchorHasContent = defineRule({
|
|
|
2235
2248
|
}
|
|
2236
2249
|
});
|
|
2237
2250
|
//#endregion
|
|
2251
|
+
//#region src/plugin/utils/get-jsx-prop-static-string-values.ts
|
|
2252
|
+
const MAX_CONST_RESOLUTION_HOPS = 4;
|
|
2253
|
+
const resolveStaticStringValues = (rawExpression, scopes, remainingHops) => {
|
|
2254
|
+
const expression = stripParenExpression(rawExpression);
|
|
2255
|
+
if (isNodeOfType(expression, "Literal")) return typeof expression.value === "string" ? [expression.value] : null;
|
|
2256
|
+
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2257
|
+
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2258
|
+
return staticValue === null ? null : [staticValue];
|
|
2259
|
+
}
|
|
2260
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
2261
|
+
const consequentValues = resolveStaticStringValues(expression.consequent, scopes, remainingHops);
|
|
2262
|
+
if (consequentValues === null) return null;
|
|
2263
|
+
const alternateValues = resolveStaticStringValues(expression.alternate, scopes, remainingHops);
|
|
2264
|
+
if (alternateValues === null) return null;
|
|
2265
|
+
return [...consequentValues, ...alternateValues];
|
|
2266
|
+
}
|
|
2267
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
2268
|
+
if (remainingHops === 0) return null;
|
|
2269
|
+
const symbol = scopes.referenceFor(expression)?.resolvedSymbol;
|
|
2270
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return null;
|
|
2271
|
+
return resolveStaticStringValues(symbol.initializer, scopes, remainingHops - 1);
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
};
|
|
2275
|
+
const getJsxPropStaticStringValues = (attribute, scopes) => {
|
|
2276
|
+
const value = attribute.value;
|
|
2277
|
+
if (!value) return null;
|
|
2278
|
+
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? [value.value] : null;
|
|
2279
|
+
if (isNodeOfType(value, "JSXExpressionContainer")) return resolveStaticStringValues(value.expression, scopes, MAX_CONST_RESOLUTION_HOPS);
|
|
2280
|
+
return null;
|
|
2281
|
+
};
|
|
2282
|
+
//#endregion
|
|
2238
2283
|
//#region src/plugin/rules/a11y/anchor-is-valid.ts
|
|
2239
2284
|
const MESSAGE_MISSING_HREF = "Keyboard users can't reach this link because it has no `href`, so add a real `href` (or use `<button>` for actions).";
|
|
2240
2285
|
const MESSAGE_INCORRECT_HREF = "Keyboard users can't reach this link because its `href` goes nowhere (`#`, `javascript:`, or empty), so point it at a real destination.";
|
|
@@ -2264,28 +2309,11 @@ const isInvalidHref = (value, validHrefs) => {
|
|
|
2264
2309
|
if (validHrefs.has(value)) return false;
|
|
2265
2310
|
return value === "" || value === "#" || value === "javascript:void(0)";
|
|
2266
2311
|
};
|
|
2267
|
-
const
|
|
2268
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? value.value : null;
|
|
2269
|
-
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2270
|
-
const expression = value.expression;
|
|
2271
|
-
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
2272
|
-
if (isNodeOfType(expression, "TemplateLiteral")) return getStaticTemplateLiteralValue(expression);
|
|
2273
|
-
}
|
|
2274
|
-
return null;
|
|
2275
|
-
};
|
|
2276
|
-
const checkValueIsEmptyOrInvalid = (value, validHrefs) => {
|
|
2277
|
-
if (isNodeOfType(value, "Literal")) return typeof value.value === "string" ? isInvalidHref(value.value, validHrefs) : false;
|
|
2312
|
+
const isNullishOrFragmentHref = (value) => {
|
|
2278
2313
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
2279
2314
|
const expression = value.expression;
|
|
2280
2315
|
if (isNodeOfType(expression, "Identifier") && expression.name === "undefined") return true;
|
|
2281
|
-
if (isNodeOfType(expression, "Literal"))
|
|
2282
|
-
if (expression.value === null) return true;
|
|
2283
|
-
if (typeof expression.value === "string") return isInvalidHref(expression.value, validHrefs);
|
|
2284
|
-
}
|
|
2285
|
-
if (isNodeOfType(expression, "TemplateLiteral")) {
|
|
2286
|
-
const staticValue = getStaticTemplateLiteralValue(expression);
|
|
2287
|
-
return staticValue === null ? false : isInvalidHref(staticValue, validHrefs);
|
|
2288
|
-
}
|
|
2316
|
+
if (isNodeOfType(expression, "Literal") && expression.value === null) return true;
|
|
2289
2317
|
}
|
|
2290
2318
|
if (isNodeOfType(value, "JSXFragment")) return true;
|
|
2291
2319
|
return false;
|
|
@@ -2318,9 +2346,10 @@ const anchorIsValid = defineRule({
|
|
|
2318
2346
|
});
|
|
2319
2347
|
return;
|
|
2320
2348
|
}
|
|
2321
|
-
|
|
2349
|
+
const hrefCandidates = getJsxPropStaticStringValues(hrefAttribute, context.scopes);
|
|
2350
|
+
if (hrefCandidates !== null ? hrefCandidates.length > 0 && hrefCandidates.every((candidate) => isInvalidHref(candidate, settings.validHrefs)) : isNullishOrFragmentHref(hrefAttribute.value)) {
|
|
2322
2351
|
const hasOnClick = Boolean(hasJsxPropIgnoreCase(node.attributes, "onClick"));
|
|
2323
|
-
if (!hasOnClick &&
|
|
2352
|
+
if (!hasOnClick && hrefCandidates?.every((candidate) => candidate === "#")) return;
|
|
2324
2353
|
context.report({
|
|
2325
2354
|
node: node.name,
|
|
2326
2355
|
message: hasOnClick ? MESSAGE_CANT_BE_ANCHOR : MESSAGE_INCORRECT_HREF
|
|
@@ -3376,6 +3405,25 @@ const ariaRole = defineRule({
|
|
|
3376
3405
|
if (!roleAttribute) return;
|
|
3377
3406
|
const elementType = getElementType(node, context.settings);
|
|
3378
3407
|
if (settings.ignoreNonDOM && !HTML_TAGS.has(elementType)) return;
|
|
3408
|
+
const reportFirstInvalidCandidate = (candidates) => {
|
|
3409
|
+
for (const candidate of candidates) {
|
|
3410
|
+
if (candidate.trim().length === 0) {
|
|
3411
|
+
context.report({
|
|
3412
|
+
node: roleAttribute,
|
|
3413
|
+
message: buildBaseMessage("")
|
|
3414
|
+
});
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
const tokens = candidate.split(/\s+/).filter((token) => token.length > 0);
|
|
3418
|
+
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3419
|
+
context.report({
|
|
3420
|
+
node: roleAttribute,
|
|
3421
|
+
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3422
|
+
});
|
|
3423
|
+
return;
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
};
|
|
3379
3427
|
const value = roleAttribute.value;
|
|
3380
3428
|
if (!value) {
|
|
3381
3429
|
context.report({
|
|
@@ -3392,22 +3440,7 @@ const ariaRole = defineRule({
|
|
|
3392
3440
|
});
|
|
3393
3441
|
return;
|
|
3394
3442
|
}
|
|
3395
|
-
|
|
3396
|
-
if (stringValue.trim().length === 0) {
|
|
3397
|
-
context.report({
|
|
3398
|
-
node: roleAttribute,
|
|
3399
|
-
message: buildBaseMessage("")
|
|
3400
|
-
});
|
|
3401
|
-
return;
|
|
3402
|
-
}
|
|
3403
|
-
const tokens = stringValue.split(/\s+/).filter((token) => token.length > 0);
|
|
3404
|
-
for (const token of tokens) if (!VALID_ARIA_ROLES.has(token) && !settings.allowedInvalidRoles.includes(token)) {
|
|
3405
|
-
context.report({
|
|
3406
|
-
node: roleAttribute,
|
|
3407
|
-
message: buildBaseMessage(` \`${token}\` is not one.`)
|
|
3408
|
-
});
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3443
|
+
reportFirstInvalidCandidate([value.value]);
|
|
3411
3444
|
return;
|
|
3412
3445
|
}
|
|
3413
3446
|
if (isNodeOfType(value, "JSXExpressionContainer")) {
|
|
@@ -3428,6 +3461,11 @@ const ariaRole = defineRule({
|
|
|
3428
3461
|
});
|
|
3429
3462
|
return;
|
|
3430
3463
|
}
|
|
3464
|
+
const resolvedCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
3465
|
+
if (resolvedCandidates !== null) {
|
|
3466
|
+
reportFirstInvalidCandidate(resolvedCandidates);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3431
3469
|
return;
|
|
3432
3470
|
}
|
|
3433
3471
|
context.report({
|
|
@@ -9616,7 +9654,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
9616
9654
|
});
|
|
9617
9655
|
//#endregion
|
|
9618
9656
|
//#region src/plugin/rules/react-native/expo-no-non-inlined-env.ts
|
|
9619
|
-
const EMPTY_VISITORS$
|
|
9657
|
+
const EMPTY_VISITORS$6 = {};
|
|
9620
9658
|
const NODE_OR_BUILD_FILE = /(\.config\.[cm]?[jt]sx?$)|((^|\/)(scripts|tools|tooling|cli|bin)\/)|(\+(api|html)\.[cm]?[jt]sx?$)|(\.server\.[cm]?[jt]sx?$)|(\.(test|spec)\.[cm]?[jt]sx?$)|((^|\/)__tests__\/)|(\.e2e\.[cm]?[jt]sx?$)/;
|
|
9621
9659
|
const isNonExpoPublicLiteralKey = (key) => isNodeOfType(key, "Literal") && typeof key.value === "string" && !key.value.startsWith("EXPO_PUBLIC_");
|
|
9622
9660
|
const isProcessEnv = (node) => isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && node.object.name === "process" && isNodeOfType(node.property, "Identifier") && node.property.name === "env";
|
|
@@ -9628,7 +9666,7 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
9628
9666
|
recommendation: "Read env vars with static dotted access (`process.env.EXPO_PUBLIC_NAME`). Computed access and destructuring aren't inlined by babel-preset-expo and resolve to `undefined` at runtime.",
|
|
9629
9667
|
create: (context) => {
|
|
9630
9668
|
const filename = normalizeFilename(context.filename ?? "");
|
|
9631
|
-
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$
|
|
9669
|
+
if (filename && NODE_OR_BUILD_FILE.test(filename)) return EMPTY_VISITORS$6;
|
|
9632
9670
|
return {
|
|
9633
9671
|
MemberExpression(node) {
|
|
9634
9672
|
if (!node.computed) return;
|
|
@@ -11046,8 +11084,8 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11046
11084
|
if (node.attributes.length === 0) return;
|
|
11047
11085
|
if (hasJsxSpreadAttribute$1(node.attributes)) return;
|
|
11048
11086
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
11049
|
-
const
|
|
11050
|
-
if (
|
|
11087
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : null;
|
|
11088
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
11051
11089
|
let hasInteractiveHandler = false;
|
|
11052
11090
|
for (const attribute of node.attributes) {
|
|
11053
11091
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11061,11 +11099,16 @@ const interactiveSupportsFocus = defineRule({
|
|
|
11061
11099
|
const elementType = getElementType(node, context.settings);
|
|
11062
11100
|
if (!HTML_TAGS.has(elementType)) return;
|
|
11063
11101
|
if (isDisabledElement(node) || isHiddenFromScreenReader(node, context.settings) || isPresentationRole(node)) return;
|
|
11064
|
-
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11065
|
-
if (COMPOSITE_ITEM_ROLES.has(role) && Boolean(hasJsxPropIgnoreCase(node.attributes, "id"))) return;
|
|
11066
11102
|
const hasTabIndex = Boolean(hasJsxPropIgnoreCase(node.attributes, "tabIndex"));
|
|
11067
|
-
|
|
11068
|
-
const
|
|
11103
|
+
const hasId = Boolean(hasJsxPropIgnoreCase(node.attributes, "id"));
|
|
11104
|
+
for (const role of roleCandidates) {
|
|
11105
|
+
if (COMPOSITE_CONTAINER_ROLES.has(role)) return;
|
|
11106
|
+
if (COMPOSITE_ITEM_ROLES.has(role) && hasId) return;
|
|
11107
|
+
if (!isInteractiveRole(role) || isInteractiveElement(elementType, node) || isNonInteractiveRole(role) || isNonInteractiveElement(elementType, node) || hasTabIndex) return;
|
|
11108
|
+
}
|
|
11109
|
+
const isEveryCandidateTabbable = roleCandidates.every((role) => tabbableSet.has(role));
|
|
11110
|
+
const roleDisplay = roleCandidates.join("' / '");
|
|
11111
|
+
const message = isEveryCandidateTabbable ? buildTabbableMessage(roleDisplay) : buildFocusableMessage(roleDisplay);
|
|
11069
11112
|
context.report({
|
|
11070
11113
|
node,
|
|
11071
11114
|
message
|
|
@@ -22477,6 +22520,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
22477
22520
|
const section = manifest[sectionName];
|
|
22478
22521
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
22479
22522
|
});
|
|
22523
|
+
const declaresDependency = (manifest, dependencyName) => {
|
|
22524
|
+
for (const declaredName of iterateDependencyNames(manifest)) if (declaredName === dependencyName) return true;
|
|
22525
|
+
return false;
|
|
22526
|
+
};
|
|
22480
22527
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
22481
22528
|
const classifyPackagePlatform = (filename) => {
|
|
22482
22529
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -22493,9 +22540,7 @@ const classifyPackagePlatform = (filename) => {
|
|
|
22493
22540
|
return result;
|
|
22494
22541
|
};
|
|
22495
22542
|
//#endregion
|
|
22496
|
-
//#region src/plugin/utils/is-
|
|
22497
|
-
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22498
|
-
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22543
|
+
//#region src/plugin/utils/is-package-nested-below-project-root.ts
|
|
22499
22544
|
const cachedRealDirectoryByDirectory = /* @__PURE__ */ new Map();
|
|
22500
22545
|
const resolveRealDirectory = (directory) => {
|
|
22501
22546
|
const cached = cachedRealDirectoryByDirectory.get(directory);
|
|
@@ -22516,6 +22561,10 @@ const isPackageNestedBelowProjectRoot = (packageDirectory, rootDirectory) => {
|
|
|
22516
22561
|
const rootPrefix = normalizedRootDirectory.endsWith("/") ? normalizedRootDirectory : `${normalizedRootDirectory}/`;
|
|
22517
22562
|
return realPackageDirectory.startsWith(rootPrefix);
|
|
22518
22563
|
};
|
|
22564
|
+
//#endregion
|
|
22565
|
+
//#region src/plugin/utils/is-react-native-file.ts
|
|
22566
|
+
const WEB_FILE_EXTENSION_PATTERN = /\.web\.[cm]?[jt]sx?$/;
|
|
22567
|
+
const NATIVE_FILE_EXTENSION_PATTERN = /\.(?:ios|android|native)\.[cm]?[jt]sx?$/;
|
|
22519
22568
|
const classifyReactNativeFileTarget = (context) => {
|
|
22520
22569
|
const rawFilename = context.filename;
|
|
22521
22570
|
if (!rawFilename) return "unknown";
|
|
@@ -22932,7 +22981,6 @@ const countStatementSequenceSetStateCalls = (statements, context) => {
|
|
|
22932
22981
|
let fallThroughCount = 0;
|
|
22933
22982
|
let maxTerminatingPathCount = 0;
|
|
22934
22983
|
for (const statement of statements) {
|
|
22935
|
-
if (isFunctionLike$1(statement)) continue;
|
|
22936
22984
|
const guardBranch = isGuardWithTerminatingBranch(statement);
|
|
22937
22985
|
if (guardBranch) {
|
|
22938
22986
|
maxTerminatingPathCount = Math.max(maxTerminatingPathCount, fallThroughCount + countMaxPathSetStateCalls(guardBranch, context));
|
|
@@ -22968,14 +23016,23 @@ const isWholesaleDelegationCall = (callNode, effectCallback) => {
|
|
|
22968
23016
|
if (!isNodeOfType(parent, "ExpressionStatement")) return false;
|
|
22969
23017
|
return parent.parent === effectCallback.body;
|
|
22970
23018
|
};
|
|
23019
|
+
const countFunctionBodySetStateCalls = (functionNode, context) => {
|
|
23020
|
+
if (isAsyncFunctionLike(functionNode)) return 0;
|
|
23021
|
+
const body = functionNode.body;
|
|
23022
|
+
if (!body) return 0;
|
|
23023
|
+
return countMaxPathSetStateCalls(body, context);
|
|
23024
|
+
};
|
|
22971
23025
|
const countMaxPathSetStateCalls = (node, context) => {
|
|
22972
23026
|
if (!node || typeof node !== "object") return 0;
|
|
22973
|
-
if (
|
|
23027
|
+
if (isFunctionLike$1(node)) return 0;
|
|
22974
23028
|
if (isNodeOfType(node, "BlockStatement") || isNodeOfType(node, "Program")) return countStatementSequenceSetStateCalls(node.body ?? [], context);
|
|
22975
23029
|
if (isNodeOfType(node, "IfStatement") || isNodeOfType(node, "ConditionalExpression")) {
|
|
22976
23030
|
const consequent = node.consequent;
|
|
22977
23031
|
const alternate = node.alternate;
|
|
22978
|
-
|
|
23032
|
+
const testCount = countMaxPathSetStateCalls(node.test, context);
|
|
23033
|
+
const thenCount = countMaxPathSetStateCalls(consequent, context);
|
|
23034
|
+
const elseCount = alternate ? countMaxPathSetStateCalls(alternate, context) : 0;
|
|
23035
|
+
return testCount + Math.max(thenCount, elseCount);
|
|
22979
23036
|
}
|
|
22980
23037
|
if (isNodeOfType(node, "SwitchStatement")) {
|
|
22981
23038
|
let maxRunSetters = 0;
|
|
@@ -23005,28 +23062,31 @@ const countMaxPathSetStateCalls = (node, context) => {
|
|
|
23005
23062
|
}
|
|
23006
23063
|
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
23007
23064
|
let nestedSettersInArgs = 0;
|
|
23008
|
-
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument, context);
|
|
23065
|
+
for (const argument of node.arguments ?? []) nestedSettersInArgs += isFunctionLike$1(argument) ? countFunctionBodySetStateCalls(argument, context) : countMaxPathSetStateCalls(argument, context);
|
|
23009
23066
|
return 1 + nestedSettersInArgs;
|
|
23010
23067
|
}
|
|
23011
23068
|
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier")) {
|
|
23012
23069
|
const helperFunction = context.helpersByName.get(node.callee.name);
|
|
23013
23070
|
if (helperFunction && !context.activeHelpers.has(helperFunction) && isWholesaleDelegationCall(node, context.effectCallback)) {
|
|
23014
23071
|
context.activeHelpers.add(helperFunction);
|
|
23015
|
-
let helperCount =
|
|
23072
|
+
let helperCount = countFunctionBodySetStateCalls(helperFunction, context);
|
|
23016
23073
|
context.activeHelpers.delete(helperFunction);
|
|
23017
23074
|
for (const argument of node.arguments ?? []) helperCount += countMaxPathSetStateCalls(argument, context);
|
|
23018
23075
|
return helperCount;
|
|
23019
23076
|
}
|
|
23020
23077
|
}
|
|
23021
|
-
const
|
|
23078
|
+
const countChild = (child) => {
|
|
23079
|
+
if (isFunctionLike$1(child)) return runsOnEffectDispatch(child) ? countFunctionBodySetStateCalls(child, context) : 0;
|
|
23080
|
+
return countMaxPathSetStateCalls(child, context);
|
|
23081
|
+
};
|
|
23022
23082
|
let total = 0;
|
|
23023
23083
|
const nodeRecord = node;
|
|
23024
23084
|
for (const key of Object.keys(nodeRecord)) {
|
|
23025
23085
|
if (key === "parent") continue;
|
|
23026
23086
|
const child = nodeRecord[key];
|
|
23027
23087
|
if (Array.isArray(child)) {
|
|
23028
|
-
for (const item of child) if (item && typeof item === "object" && "type" in item
|
|
23029
|
-
} else if (child && typeof child === "object" && "type" in child
|
|
23088
|
+
for (const item of child) if (item && typeof item === "object" && "type" in item) total += countChild(item);
|
|
23089
|
+
} else if (child && typeof child === "object" && "type" in child) total += countChild(child);
|
|
23030
23090
|
}
|
|
23031
23091
|
return total;
|
|
23032
23092
|
};
|
|
@@ -23075,7 +23135,7 @@ const noCascadingSetState = defineRule({
|
|
|
23075
23135
|
const callback = getEffectCallback(node);
|
|
23076
23136
|
if (!callback) return;
|
|
23077
23137
|
if (isDevOnlyGuardedEffect(callback)) return;
|
|
23078
|
-
const setStateCallCount =
|
|
23138
|
+
const setStateCallCount = countFunctionBodySetStateCalls(callback, {
|
|
23079
23139
|
helpersByName: collectLocalHelperFunctions(findProgramRoot(node) ?? callback),
|
|
23080
23140
|
activeHelpers: /* @__PURE__ */ new Set(),
|
|
23081
23141
|
effectCallback: callback
|
|
@@ -26293,7 +26353,11 @@ const noEffectEventInDeps = defineRule({
|
|
|
26293
26353
|
const initializer = declaratorNode.init;
|
|
26294
26354
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return;
|
|
26295
26355
|
if (!isHookCall$2(initializer, "useEffectEvent")) return;
|
|
26296
|
-
if (isNodeOfType(initializer.callee, "Identifier")
|
|
26356
|
+
if (isNodeOfType(initializer.callee, "Identifier")) {
|
|
26357
|
+
if (isImportedFromNonReactModule(declaratorNode, initializer.callee.name)) return;
|
|
26358
|
+
const calleeSymbol = context.scopes.referenceFor(initializer.callee)?.resolvedSymbol;
|
|
26359
|
+
if (calleeSymbol && calleeSymbol.kind !== "import") return;
|
|
26360
|
+
}
|
|
26297
26361
|
componentBindings.addBindingToCurrentFrame(declaratorNode.id.name);
|
|
26298
26362
|
} });
|
|
26299
26363
|
return {
|
|
@@ -36575,8 +36639,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
36639
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
36640
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
36641
|
const REACT_CONTEXT_MESSAGE = "This file exports a context with components, so Fast Refresh can't safely preserve component state.";
|
|
36578
|
-
const
|
|
36579
|
-
const NO_EXPORT_MESSAGE = "This file exports nothing, so Fast Refresh can't track the component and local edits can full-reload.";
|
|
36642
|
+
const NAMESPACE_OBJECT_MESSAGE = "This export bundles components inside an object, so Fast Refresh can't track them and falls back to a full reload.";
|
|
36580
36643
|
const DEFAULT_REACT_HOCS = [
|
|
36581
36644
|
"memo",
|
|
36582
36645
|
"forwardRef",
|
|
@@ -36647,6 +36710,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
36710
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
36711
|
return false;
|
|
36649
36712
|
};
|
|
36713
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
36714
|
+
for (const property of objectExpression.properties ?? []) {
|
|
36715
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
36716
|
+
const value = skipTsExpression(property.value);
|
|
36717
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
36718
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
36719
|
+
continue;
|
|
36720
|
+
}
|
|
36721
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36722
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
36723
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36724
|
+
}
|
|
36725
|
+
return false;
|
|
36726
|
+
};
|
|
36650
36727
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
36728
|
if (initializer) {
|
|
36652
36729
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +36754,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
36754
|
reportNode
|
|
36678
36755
|
};
|
|
36679
36756
|
}
|
|
36757
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
36758
|
+
kind: "namespace-object",
|
|
36759
|
+
reportNode
|
|
36760
|
+
};
|
|
36680
36761
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
36762
|
kind: "non-component",
|
|
36682
36763
|
reportNode
|
|
@@ -36693,7 +36774,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
36774
|
walkAst(programRoot, (child) => {
|
|
36694
36775
|
const childType = child.type;
|
|
36695
36776
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
36777
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
36778
|
});
|
|
36698
36779
|
return {
|
|
36699
36780
|
exportNodes,
|
|
@@ -36738,18 +36819,31 @@ const onlyExportComponents = defineRule({
|
|
|
36738
36819
|
category: "Architecture",
|
|
36739
36820
|
create: (context) => {
|
|
36740
36821
|
const settings = resolveSettings$6(context.settings);
|
|
36741
|
-
const state = {
|
|
36742
|
-
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36743
|
-
allowExportNames: new Set(settings.allowExportNames),
|
|
36744
|
-
allowConstantExport: settings.allowConstantExport
|
|
36745
|
-
};
|
|
36746
36822
|
return { Program(node) {
|
|
36747
36823
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
36824
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
36825
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
36826
|
+
const state = {
|
|
36827
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36828
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
36829
|
+
allowConstantExport: settings.allowConstantExport,
|
|
36830
|
+
localComponentNames
|
|
36831
|
+
};
|
|
36832
|
+
for (const child of componentCandidates) {
|
|
36833
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36834
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36835
|
+
}
|
|
36836
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36837
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36838
|
+
}
|
|
36839
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36840
|
+
const initializer = child.init;
|
|
36841
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36842
|
+
}
|
|
36843
|
+
}
|
|
36749
36844
|
const exports = [];
|
|
36750
36845
|
let hasReactExport = false;
|
|
36751
36846
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
36847
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
36848
|
for (const child of exportNodes) {
|
|
36755
36849
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36820,7 +36914,14 @@ const onlyExportComponents = defineRule({
|
|
|
36820
36914
|
});
|
|
36821
36915
|
continue;
|
|
36822
36916
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
36917
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
36918
|
+
context.report({
|
|
36919
|
+
node: stripped,
|
|
36920
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
36921
|
+
});
|
|
36922
|
+
continue;
|
|
36923
|
+
}
|
|
36924
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
36925
|
context.report({
|
|
36825
36926
|
node: stripped,
|
|
36826
36927
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +36974,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
36974
|
let entry;
|
|
36874
36975
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
36976
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
36977
|
+
else {
|
|
36978
|
+
entry = {
|
|
36979
|
+
kind: "non-component",
|
|
36980
|
+
reportNode
|
|
36981
|
+
};
|
|
36982
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
36983
|
+
}
|
|
36880
36984
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
36985
|
exports.push(entry);
|
|
36882
36986
|
}
|
|
36883
36987
|
}
|
|
36884
36988
|
}
|
|
36885
36989
|
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
36886
|
-
const
|
|
36887
|
-
|
|
36888
|
-
|
|
36889
|
-
|
|
36890
|
-
walker = walker.parent ?? null;
|
|
36891
|
-
}
|
|
36892
|
-
return false;
|
|
36893
|
-
};
|
|
36894
|
-
for (const child of componentCandidates) {
|
|
36895
|
-
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36896
|
-
if (isReactComponentName(child.id.name) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36897
|
-
}
|
|
36898
|
-
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36899
|
-
if (isReactComponentName(child.id.name) && canBeReactFunctionComponent(child.init, state) && !isInsideExport(child) && !isInsideFunctionScope(child)) localComponents.push(child.id);
|
|
36900
|
-
}
|
|
36901
|
-
}
|
|
36990
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
36991
|
+
node: entry.reportNode,
|
|
36992
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
36993
|
+
});
|
|
36902
36994
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
36995
|
if (entry.kind === "non-component") context.report({
|
|
36904
36996
|
node: entry.reportNode,
|
|
@@ -36909,14 +37001,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37001
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37002
|
});
|
|
36911
37003
|
}
|
|
36912
|
-
else if (hasAnyExports && !hasReactExport && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36913
|
-
node: local,
|
|
36914
|
-
message: LOCAL_COMPONENT_MESSAGE
|
|
36915
|
-
});
|
|
36916
|
-
else if (!hasAnyExports && localComponents.length > 0) for (const local of localComponents) context.report({
|
|
36917
|
-
node: local,
|
|
36918
|
-
message: NO_EXPORT_MESSAGE
|
|
36919
|
-
});
|
|
36920
37004
|
} };
|
|
36921
37005
|
}
|
|
36922
37006
|
});
|
|
@@ -37714,8 +37798,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
37798
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
37799
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
37800
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
37801
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
37802
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
37803
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
37804
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
37805
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -40704,6 +40788,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
40788
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
40789
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
40790
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40791
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
40707
40792
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
40793
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
40794
|
context.report({
|
|
@@ -40736,18 +40821,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
40821
|
"getUTCMilliseconds",
|
|
40737
40822
|
"valueOf"
|
|
40738
40823
|
]);
|
|
40739
|
-
const TRIVIAL_CONSTRUCTOR_NAMES = new Set([
|
|
40740
|
-
"Date",
|
|
40741
|
-
"Map",
|
|
40742
|
-
"Set",
|
|
40743
|
-
"WeakMap",
|
|
40744
|
-
"WeakSet",
|
|
40745
|
-
"RegExp",
|
|
40746
|
-
"Error",
|
|
40747
|
-
"URL",
|
|
40748
|
-
"URLSearchParams",
|
|
40749
|
-
"AbortController"
|
|
40750
|
-
]);
|
|
40751
40824
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
40825
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
40826
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
@@ -41491,7 +41564,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
41564
|
});
|
|
41492
41565
|
//#endregion
|
|
41493
41566
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
41567
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
41568
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
41569
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
41570
|
"tap",
|
|
@@ -41551,7 +41624,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
41624
|
recommendation: "Prepend `await` to Detox actions, `waitFor(...)` chains, and `expect(element(...))` assertions. They return promises tied to Detox's synchronization, so a missing await runs steps out of order.",
|
|
41552
41625
|
create: (context) => {
|
|
41553
41626
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41627
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
41628
|
return { ExpressionStatement(node) {
|
|
41556
41629
|
const expression = node.expression;
|
|
41557
41630
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42526,7 +42599,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
42599
|
};
|
|
42527
42600
|
//#endregion
|
|
42528
42601
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
42602
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
42603
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
42604
|
const legacyShadowPropertyNames = [];
|
|
42532
42605
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +42622,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
42622
|
severity: "warn",
|
|
42550
42623
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
42624
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42625
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
42626
|
return {
|
|
42554
42627
|
JSXAttribute(node) {
|
|
42555
42628
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43470,7 +43543,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
43543
|
};
|
|
43471
43544
|
//#endregion
|
|
43472
43545
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
43546
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
43547
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
43548
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
43549
|
const isRequireOfBundledAsset = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === "require" && node.arguments?.length === 1 && isNodeOfType(node.arguments[0], "Literal") && typeof node.arguments[0].value === "string" && BUNDLED_ASSET_SOURCE_PATTERN.test(node.arguments[0].value);
|
|
@@ -43504,7 +43577,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
43577
|
severity: "warn",
|
|
43505
43578
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
43579
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43580
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
43581
|
const flaggedImports = [];
|
|
43509
43582
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
43583
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43991,7 +44064,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
44064
|
});
|
|
43992
44065
|
//#endregion
|
|
43993
44066
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
44067
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
44068
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
44069
|
"shadowColor",
|
|
43997
44070
|
"shadowOffset",
|
|
@@ -44077,7 +44150,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
44150
|
severity: "warn",
|
|
44078
44151
|
recommendation: "These shadow keys only work on one platform. On RN v7+, use the CSS `boxShadow` string instead, like `boxShadow: \"0 2px 8px rgba(0,0,0,0.1)\"`, which works on both.",
|
|
44079
44152
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44153
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
44154
|
return {
|
|
44082
44155
|
JSXAttribute(node) {
|
|
44083
44156
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +44247,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
44247
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
44248
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
44249
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
44250
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
44251
|
+
if (roleCandidates === null) return;
|
|
44252
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
44253
|
for (const role of roles) {
|
|
44181
44254
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
44255
|
if (!required) continue;
|
|
@@ -47293,7 +47366,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
47366
|
};
|
|
47294
47367
|
//#endregion
|
|
47295
47368
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
47369
|
+
const buildMessageDefault = (roles, propName) => {
|
|
47370
|
+
return `Screen reader users get no help from \`${propName}\` because role ${roles.map((role) => `\`${role}\``).join(" / ")} ignores it, so remove it or change the role.`;
|
|
47371
|
+
};
|
|
47297
47372
|
const buildMessageImplicit = (role, propName, elementType) => `Screen reader users get no help from \`${propName}\` because \`${elementType}\` has role \`${role}\`, which ignores it, so remove \`${propName}\` or change the element.`;
|
|
47298
47373
|
const roleSupportsAriaProps = defineRule({
|
|
47299
47374
|
id: "role-supports-aria-props",
|
|
@@ -47321,17 +47396,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
47396
|
if (!ariaAttributes) return;
|
|
47322
47397
|
const elementType = getElementType(node, context.settings);
|
|
47323
47398
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
47399
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
47400
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
47401
|
+
const supportedSets = [];
|
|
47402
|
+
for (const role of roleCandidates) {
|
|
47403
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47404
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47405
|
+
if (!supported) return;
|
|
47406
|
+
supportedSets.push(supported);
|
|
47407
|
+
}
|
|
47327
47408
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
47409
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
47410
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
47411
|
context.report({
|
|
47333
47412
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47413
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
47414
|
});
|
|
47336
47415
|
}
|
|
47337
47416
|
} })
|
|
@@ -47683,11 +47762,15 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47683
47762
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
47763
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
47764
|
};
|
|
47686
|
-
const
|
|
47687
|
-
const
|
|
47765
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47766
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47767
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
47768
|
+
};
|
|
47769
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47770
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47688
47771
|
const initializer = symbol.initializer;
|
|
47689
47772
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47773
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47691
47774
|
};
|
|
47692
47775
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
47776
|
let current = node.parent;
|
|
@@ -47766,7 +47849,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
47849
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
47850
|
if (!hookContext) return;
|
|
47768
47851
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
47852
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
47853
|
context.report({
|
|
47771
47854
|
node: node.callee,
|
|
47772
47855
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47879,7 +47962,7 @@ const rulesOfHooks = defineRule({
|
|
|
47879
47962
|
Identifier(node) {
|
|
47880
47963
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
47964
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
47965
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47883
47966
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
47967
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
47968
|
context.report({
|
|
@@ -55451,6 +55534,34 @@ const reactDoctorRules = [
|
|
|
55451
55534
|
];
|
|
55452
55535
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
55536
|
//#endregion
|
|
55537
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
55538
|
+
const isNextFileActive = (context) => {
|
|
55539
|
+
const rawFilename = context.filename;
|
|
55540
|
+
if (!rawFilename) return true;
|
|
55541
|
+
const filename = normalizeFilename(rawFilename);
|
|
55542
|
+
const manifest = readNearestPackageManifest(filename);
|
|
55543
|
+
if (!manifest) return true;
|
|
55544
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
55545
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
55546
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
55547
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
55548
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
55549
|
+
return true;
|
|
55550
|
+
};
|
|
55551
|
+
//#endregion
|
|
55552
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
55553
|
+
const EMPTY_VISITORS$1 = {};
|
|
55554
|
+
const wrapNextjsRule = (rule) => {
|
|
55555
|
+
const innerCreate = rule.create.bind(rule);
|
|
55556
|
+
return {
|
|
55557
|
+
...rule,
|
|
55558
|
+
create: (context) => {
|
|
55559
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
55560
|
+
return innerCreate(context);
|
|
55561
|
+
}
|
|
55562
|
+
};
|
|
55563
|
+
};
|
|
55564
|
+
//#endregion
|
|
55454
55565
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
55566
|
const EMPTY_VISITORS = {};
|
|
55456
55567
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +56031,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
56031
|
});
|
|
55921
56032
|
//#endregion
|
|
55922
56033
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56034
|
+
const applyFrameworkGate = (rule) => {
|
|
56035
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56036
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56037
|
+
return rule;
|
|
56038
|
+
};
|
|
55923
56039
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
56040
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56041
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
56042
|
return wrapped;
|
|
55927
56043
|
};
|
|
55928
56044
|
const plugin = {
|