oxlint-plugin-react-doctor 0.7.2-dev.0eb5293 → 0.7.2-dev.11e9c87
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 +286 -171
- 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 {
|
|
@@ -30015,6 +30079,40 @@ const noMutableInDeps = defineRule({
|
|
|
30015
30079
|
}
|
|
30016
30080
|
});
|
|
30017
30081
|
//#endregion
|
|
30082
|
+
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
30083
|
+
const isResultDiscardedCall = (callExpression) => {
|
|
30084
|
+
let node = callExpression;
|
|
30085
|
+
let parent = node.parent;
|
|
30086
|
+
while (parent) {
|
|
30087
|
+
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
30088
|
+
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
30089
|
+
if (isNodeOfType(parent, "ChainExpression")) {
|
|
30090
|
+
node = parent;
|
|
30091
|
+
parent = node.parent;
|
|
30092
|
+
continue;
|
|
30093
|
+
}
|
|
30094
|
+
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
30095
|
+
node = parent;
|
|
30096
|
+
parent = node.parent;
|
|
30097
|
+
continue;
|
|
30098
|
+
}
|
|
30099
|
+
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
30100
|
+
node = parent;
|
|
30101
|
+
parent = node.parent;
|
|
30102
|
+
continue;
|
|
30103
|
+
}
|
|
30104
|
+
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
30105
|
+
const expressions = parent.expressions ?? [];
|
|
30106
|
+
if (expressions[expressions.length - 1] !== node) return true;
|
|
30107
|
+
node = parent;
|
|
30108
|
+
parent = node.parent;
|
|
30109
|
+
continue;
|
|
30110
|
+
}
|
|
30111
|
+
return false;
|
|
30112
|
+
}
|
|
30113
|
+
return false;
|
|
30114
|
+
};
|
|
30115
|
+
//#endregion
|
|
30018
30116
|
//#region src/plugin/rules/state-and-effects/utils/lodash-mutator-call.ts
|
|
30019
30117
|
const LODASH_MUTATOR_NAMES = new Set([
|
|
30020
30118
|
"set",
|
|
@@ -30112,7 +30210,6 @@ const SAME_REFERENCE_ARRAY_RETURN_METHODS = new Set([
|
|
|
30112
30210
|
"reverse",
|
|
30113
30211
|
"sort"
|
|
30114
30212
|
]);
|
|
30115
|
-
const SAME_REFERENCE_COLLECTION_RETURN_METHODS = new Set(["add", "set"]);
|
|
30116
30213
|
const OBJECT_MUTATION_METHODS = new Set([
|
|
30117
30214
|
"assign",
|
|
30118
30215
|
"defineProperties",
|
|
@@ -30186,7 +30283,6 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
30186
30283
|
const methodName = getStaticMemberPropertyName(unwrappedNode.callee);
|
|
30187
30284
|
if (methodName === "assign" && isNodeOfType(unwrappedNode.callee.object, "Identifier") && unwrappedNode.callee.object.name === "Object") return isExpressionOriginalReducerStateReference(unwrappedNode.arguments?.[0], state);
|
|
30188
30285
|
if (methodName && SAME_REFERENCE_ARRAY_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30189
|
-
if (methodName && SAME_REFERENCE_COLLECTION_RETURN_METHODS.has(methodName) && isExpressionOriginalReducerStateReference(unwrappedNode.callee.object, state)) return true;
|
|
30190
30286
|
}
|
|
30191
30287
|
}
|
|
30192
30288
|
if (isNodeOfType(unwrappedNode, "ConditionalExpression")) return canExpressionReturnOriginalReducerStateReference(unwrappedNode.consequent, state) || canExpressionReturnOriginalReducerStateReference(unwrappedNode.alternate, state);
|
|
@@ -30225,6 +30321,7 @@ const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
|
30225
30321
|
if (!isNodeOfType(unwrappedChild.callee, "MemberExpression")) return;
|
|
30226
30322
|
const methodName = getStaticMemberPropertyName(unwrappedChild.callee);
|
|
30227
30323
|
if (!methodName || !MUTATING_ARRAY_METHODS.has(methodName) && !MUTATING_COLLECTION_METHODS.has(methodName)) return;
|
|
30324
|
+
if (MUTATING_COLLECTION_METHODS.has(methodName) && !MUTATING_ARRAY_METHODS.has(methodName) && !isResultDiscardedCall(unwrappedChild)) return;
|
|
30228
30325
|
if (isExpressionRootedInMutableReducerStateSource(unwrappedChild.callee.object, state)) mutations.push({ node: unwrappedChild });
|
|
30229
30326
|
});
|
|
30230
30327
|
return mutations;
|
|
@@ -31936,40 +32033,6 @@ const noPreventDefault = defineRule({
|
|
|
31936
32033
|
}
|
|
31937
32034
|
});
|
|
31938
32035
|
//#endregion
|
|
31939
|
-
//#region src/plugin/utils/is-result-discarded-call.ts
|
|
31940
|
-
const isResultDiscardedCall = (callExpression) => {
|
|
31941
|
-
let node = callExpression;
|
|
31942
|
-
let parent = node.parent;
|
|
31943
|
-
while (parent) {
|
|
31944
|
-
if (isNodeOfType(parent, "ExpressionStatement")) return true;
|
|
31945
|
-
if (isNodeOfType(parent, "ArrowFunctionExpression") && parent.body === node) return true;
|
|
31946
|
-
if (isNodeOfType(parent, "ChainExpression")) {
|
|
31947
|
-
node = parent;
|
|
31948
|
-
parent = node.parent;
|
|
31949
|
-
continue;
|
|
31950
|
-
}
|
|
31951
|
-
if (isNodeOfType(parent, "LogicalExpression") && parent.right === node) {
|
|
31952
|
-
node = parent;
|
|
31953
|
-
parent = node.parent;
|
|
31954
|
-
continue;
|
|
31955
|
-
}
|
|
31956
|
-
if (isNodeOfType(parent, "ConditionalExpression") && (parent.consequent === node || parent.alternate === node)) {
|
|
31957
|
-
node = parent;
|
|
31958
|
-
parent = node.parent;
|
|
31959
|
-
continue;
|
|
31960
|
-
}
|
|
31961
|
-
if (isNodeOfType(parent, "SequenceExpression")) {
|
|
31962
|
-
const expressions = parent.expressions ?? [];
|
|
31963
|
-
if (expressions[expressions.length - 1] !== node) return true;
|
|
31964
|
-
node = parent;
|
|
31965
|
-
parent = node.parent;
|
|
31966
|
-
continue;
|
|
31967
|
-
}
|
|
31968
|
-
return false;
|
|
31969
|
-
}
|
|
31970
|
-
return false;
|
|
31971
|
-
};
|
|
31972
|
-
//#endregion
|
|
31973
32036
|
//#region src/plugin/rules/state-and-effects/no-prop-callback-in-effect.ts
|
|
31974
32037
|
const isRefLatchGuardedEffect = (callbackBody) => {
|
|
31975
32038
|
const refNamesReadInGuards = /* @__PURE__ */ new Set();
|
|
@@ -36575,8 +36638,7 @@ const NAMED_EXPORT_MESSAGE = "This file exports non-components, so Fast Refresh
|
|
|
36575
36638
|
const ANONYMOUS_MESSAGE = "This component is unnamed, so Fast Refresh can't track it and falls back to a full reload.";
|
|
36576
36639
|
const EXPORT_ALL_MESSAGE = "`export *` hides what's exported, so Fast Refresh can't safely preserve component state.";
|
|
36577
36640
|
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.";
|
|
36641
|
+
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
36642
|
const DEFAULT_REACT_HOCS = [
|
|
36581
36643
|
"memo",
|
|
36582
36644
|
"forwardRef",
|
|
@@ -36647,6 +36709,20 @@ const isReactComponentInitializer = (expression, state) => {
|
|
|
36647
36709
|
if (isNodeOfType(stripped, "CallExpression") && isHocCallee(stripped.callee, state) && stripped.arguments.length > 0) return true;
|
|
36648
36710
|
return false;
|
|
36649
36711
|
};
|
|
36712
|
+
const objectExpressionBundlesComponents = (objectExpression, state) => {
|
|
36713
|
+
for (const property of objectExpression.properties ?? []) {
|
|
36714
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
36715
|
+
const value = skipTsExpression(property.value);
|
|
36716
|
+
if (isNodeOfType(value, "Identifier")) {
|
|
36717
|
+
if (state.localComponentNames.has(value.name)) return true;
|
|
36718
|
+
continue;
|
|
36719
|
+
}
|
|
36720
|
+
if (!(!property.computed && isNodeOfType(property.key, "Identifier") && isReactComponentName(property.key.name))) continue;
|
|
36721
|
+
if (isNodeOfType(value, "ArrowFunctionExpression") || isNodeOfType(value, "FunctionExpression")) return true;
|
|
36722
|
+
if (isNodeOfType(value, "CallExpression") && isHocCallee(value.callee, state) && value.arguments.length > 0) return true;
|
|
36723
|
+
}
|
|
36724
|
+
return false;
|
|
36725
|
+
};
|
|
36650
36726
|
const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
36651
36727
|
if (initializer) {
|
|
36652
36728
|
const expression = skipTsExpression(initializer);
|
|
@@ -36677,6 +36753,10 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
36677
36753
|
reportNode
|
|
36678
36754
|
};
|
|
36679
36755
|
}
|
|
36756
|
+
if (isNodeOfType(stripped, "ObjectExpression") && objectExpressionBundlesComponents(stripped, state)) return {
|
|
36757
|
+
kind: "namespace-object",
|
|
36758
|
+
reportNode
|
|
36759
|
+
};
|
|
36680
36760
|
if (NOT_REACT_COMPONENT_EXPRESSION_TYPES.has(stripped.type)) return {
|
|
36681
36761
|
kind: "non-component",
|
|
36682
36762
|
reportNode
|
|
@@ -36693,7 +36773,7 @@ const collectRelevantNodes = (programRoot) => {
|
|
|
36693
36773
|
walkAst(programRoot, (child) => {
|
|
36694
36774
|
const childType = child.type;
|
|
36695
36775
|
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
36696
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator") componentCandidates.push(child);
|
|
36776
|
+
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
36697
36777
|
});
|
|
36698
36778
|
return {
|
|
36699
36779
|
exportNodes,
|
|
@@ -36738,18 +36818,31 @@ const onlyExportComponents = defineRule({
|
|
|
36738
36818
|
category: "Architecture",
|
|
36739
36819
|
create: (context) => {
|
|
36740
36820
|
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
36821
|
return { Program(node) {
|
|
36747
36822
|
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return;
|
|
36748
36823
|
const { exportNodes, componentCandidates } = collectRelevantNodes(node);
|
|
36824
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
36825
|
+
const state = {
|
|
36826
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
36827
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
36828
|
+
allowConstantExport: settings.allowConstantExport,
|
|
36829
|
+
localComponentNames
|
|
36830
|
+
};
|
|
36831
|
+
for (const child of componentCandidates) {
|
|
36832
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
36833
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36834
|
+
}
|
|
36835
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
36836
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36837
|
+
}
|
|
36838
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
36839
|
+
const initializer = child.init;
|
|
36840
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
36841
|
+
}
|
|
36842
|
+
}
|
|
36749
36843
|
const exports = [];
|
|
36750
36844
|
let hasReactExport = false;
|
|
36751
36845
|
let hasAnyExports = false;
|
|
36752
|
-
const localComponents = [];
|
|
36753
36846
|
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
36754
36847
|
for (const child of exportNodes) {
|
|
36755
36848
|
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
@@ -36820,7 +36913,14 @@ const onlyExportComponents = defineRule({
|
|
|
36820
36913
|
});
|
|
36821
36914
|
continue;
|
|
36822
36915
|
}
|
|
36823
|
-
if (isNodeOfType(stripped, "
|
|
36916
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
36917
|
+
context.report({
|
|
36918
|
+
node: stripped,
|
|
36919
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
36920
|
+
});
|
|
36921
|
+
continue;
|
|
36922
|
+
}
|
|
36923
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
36824
36924
|
context.report({
|
|
36825
36925
|
node: stripped,
|
|
36826
36926
|
message: ANONYMOUS_MESSAGE
|
|
@@ -36873,32 +36973,23 @@ const onlyExportComponents = defineRule({
|
|
|
36873
36973
|
let entry;
|
|
36874
36974
|
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
36875
36975
|
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
36876
|
-
else
|
|
36877
|
-
|
|
36878
|
-
|
|
36879
|
-
|
|
36976
|
+
else {
|
|
36977
|
+
entry = {
|
|
36978
|
+
kind: "non-component",
|
|
36979
|
+
reportNode
|
|
36980
|
+
};
|
|
36981
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
36982
|
+
}
|
|
36880
36983
|
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
36881
36984
|
exports.push(entry);
|
|
36882
36985
|
}
|
|
36883
36986
|
}
|
|
36884
36987
|
}
|
|
36885
36988
|
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
|
-
}
|
|
36989
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
36990
|
+
node: entry.reportNode,
|
|
36991
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
36992
|
+
});
|
|
36902
36993
|
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
36903
36994
|
if (entry.kind === "non-component") context.report({
|
|
36904
36995
|
node: entry.reportNode,
|
|
@@ -36909,14 +37000,6 @@ const onlyExportComponents = defineRule({
|
|
|
36909
37000
|
message: REACT_CONTEXT_MESSAGE
|
|
36910
37001
|
});
|
|
36911
37002
|
}
|
|
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
37003
|
} };
|
|
36921
37004
|
}
|
|
36922
37005
|
});
|
|
@@ -37714,8 +37797,8 @@ const preferHtmlDialog = defineRule({
|
|
|
37714
37797
|
if (!HTML_TAGS.has(tagName)) return;
|
|
37715
37798
|
const roleAttribute = findJsxAttribute(node.attributes, "role");
|
|
37716
37799
|
if (roleAttribute) {
|
|
37717
|
-
const
|
|
37718
|
-
if (
|
|
37800
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
37801
|
+
if (roleCandidates !== null && roleCandidates.length > 0 && roleCandidates.every((candidate) => ROLE_DIALOG_VALUES.has(candidate))) {
|
|
37719
37802
|
if (focusTrapSignals && isElementFocusTrapped(node, focusTrapSignals)) return;
|
|
37720
37803
|
const ariaModalAttribute = findJsxAttribute(node.attributes, "aria-modal");
|
|
37721
37804
|
const isModal = ariaModalAttribute ? isAriaModalTrue(ariaModalAttribute) : false;
|
|
@@ -40704,6 +40787,7 @@ const rerenderLazyRefInit = defineRule({
|
|
|
40704
40787
|
const memberPropertyName = isNodeOfType(callee, "MemberExpression") && (isNodeOfType(callee.property, "Identifier") || isNodeOfType(callee.property, "PrivateIdentifier")) ? callee.property.name : null;
|
|
40705
40788
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : memberPropertyName ?? "fn";
|
|
40706
40789
|
if (TRIVIAL_INITIALIZER_NAMES.has(calleeName)) return;
|
|
40790
|
+
if (isNewCall && TRIVIAL_CONSTRUCTOR_NAMES.has(calleeName)) return;
|
|
40707
40791
|
if (isPlainCall && isReactHookName(calleeName)) return;
|
|
40708
40792
|
const callShape = isNewCall ? `new ${calleeName}()` : `${calleeName}()`;
|
|
40709
40793
|
context.report({
|
|
@@ -40736,18 +40820,6 @@ const TRIVIAL_DATE_GETTER_NAMES = new Set([
|
|
|
40736
40820
|
"getUTCMilliseconds",
|
|
40737
40821
|
"valueOf"
|
|
40738
40822
|
]);
|
|
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
40823
|
const EAGER_CALL_RESOLUTION_DEPTH_LIMIT = 4;
|
|
40752
40824
|
const findEagerInitializerCall = (expression, depth = 0) => {
|
|
40753
40825
|
if (depth > EAGER_CALL_RESOLUTION_DEPTH_LIMIT) return null;
|
|
@@ -41491,7 +41563,7 @@ const rnBottomSheetPreferNative = defineRule({
|
|
|
41491
41563
|
});
|
|
41492
41564
|
//#endregion
|
|
41493
41565
|
//#region src/plugin/rules/react-native/rn-detox-missing-await.ts
|
|
41494
|
-
const EMPTY_VISITORS$
|
|
41566
|
+
const EMPTY_VISITORS$5 = {};
|
|
41495
41567
|
const DETOX_TEST_FILE = /(\.e2e\.[cm]?[jt]sx?$)|((^|\/)e2e\/)/;
|
|
41496
41568
|
const DETOX_ELEMENT_ACTIONS = new Set([
|
|
41497
41569
|
"tap",
|
|
@@ -41551,7 +41623,7 @@ const rnDetoxMissingAwait = defineRule({
|
|
|
41551
41623
|
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
41624
|
create: (context) => {
|
|
41553
41625
|
const filename = normalizeFilename(context.filename ?? "");
|
|
41554
|
-
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$
|
|
41626
|
+
if (!filename || !DETOX_TEST_FILE.test(filename)) return EMPTY_VISITORS$5;
|
|
41555
41627
|
return { ExpressionStatement(node) {
|
|
41556
41628
|
const expression = node.expression;
|
|
41557
41629
|
if (!isNodeOfType(expression, "CallExpression")) return;
|
|
@@ -42526,7 +42598,7 @@ const isLegacyArchReactNativeFile = (filename) => {
|
|
|
42526
42598
|
};
|
|
42527
42599
|
//#endregion
|
|
42528
42600
|
//#region src/plugin/rules/react-native/rn-no-legacy-shadow-styles.ts
|
|
42529
|
-
const EMPTY_VISITORS$
|
|
42601
|
+
const EMPTY_VISITORS$4 = {};
|
|
42530
42602
|
const reportLegacyShadowProperties = (objectExpression, context) => {
|
|
42531
42603
|
const legacyShadowPropertyNames = [];
|
|
42532
42604
|
for (const property of objectExpression.properties ?? []) {
|
|
@@ -42549,7 +42621,7 @@ const rnNoLegacyShadowStyles = defineRule({
|
|
|
42549
42621
|
severity: "warn",
|
|
42550
42622
|
recommendation: "Use `boxShadow` for shadows that work on both platforms on the new architecture, instead of platform-specific shadow properties.",
|
|
42551
42623
|
create: (context) => {
|
|
42552
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
42624
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$4;
|
|
42553
42625
|
return {
|
|
42554
42626
|
JSXAttribute(node) {
|
|
42555
42627
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -43470,7 +43542,7 @@ const isExpoManagedFileActive = (context) => {
|
|
|
43470
43542
|
};
|
|
43471
43543
|
//#endregion
|
|
43472
43544
|
//#region src/plugin/rules/react-native/rn-prefer-expo-image.ts
|
|
43473
|
-
const EMPTY_VISITORS$
|
|
43545
|
+
const EMPTY_VISITORS$3 = {};
|
|
43474
43546
|
const BUNDLED_ASSET_SOURCE_PATTERN = /\.(?:png|jpe?g|gif|webp|bmp)$/i;
|
|
43475
43547
|
const MEMBER_PATH_FAN_OUT_LIMIT = 32;
|
|
43476
43548
|
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 +43576,7 @@ const rnPreferExpoImage = defineRule({
|
|
|
43504
43576
|
severity: "warn",
|
|
43505
43577
|
recommendation: "Use `<Image>` from `expo-image` instead of `react-native`. Same props, plus caching, placeholders, and crossfades for faster image loading.",
|
|
43506
43578
|
create: (context) => {
|
|
43507
|
-
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$
|
|
43579
|
+
if (!isExpoManagedFileActive(context)) return EMPTY_VISITORS$3;
|
|
43508
43580
|
const flaggedImports = [];
|
|
43509
43581
|
const assetBindingNames = /* @__PURE__ */ new Set();
|
|
43510
43582
|
const moduleObjectLiterals = /* @__PURE__ */ new Map();
|
|
@@ -43991,7 +44063,7 @@ const rnScrollviewFlexInContentContainer = defineRule({
|
|
|
43991
44063
|
});
|
|
43992
44064
|
//#endregion
|
|
43993
44065
|
//#region src/plugin/rules/react-native/rn-style-prefer-box-shadow.ts
|
|
43994
|
-
const EMPTY_VISITORS$
|
|
44066
|
+
const EMPTY_VISITORS$2 = {};
|
|
43995
44067
|
const IOS_SHADOW_KEYS = new Set([
|
|
43996
44068
|
"shadowColor",
|
|
43997
44069
|
"shadowOffset",
|
|
@@ -44077,7 +44149,7 @@ const rnStylePreferBoxShadow = defineRule({
|
|
|
44077
44149
|
severity: "warn",
|
|
44078
44150
|
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
44151
|
create: (context) => {
|
|
44080
|
-
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$
|
|
44152
|
+
if (context.filename && isLegacyArchReactNativeFile(normalizeFilename(context.filename))) return EMPTY_VISITORS$2;
|
|
44081
44153
|
return {
|
|
44082
44154
|
JSXAttribute(node) {
|
|
44083
44155
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
@@ -44174,9 +44246,9 @@ const roleHasRequiredAriaProps = defineRule({
|
|
|
44174
44246
|
if (!HTML_TAGS.has(elementType)) return;
|
|
44175
44247
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
44176
44248
|
if (!roleAttribute) return;
|
|
44177
|
-
const
|
|
44178
|
-
if (
|
|
44179
|
-
const roles =
|
|
44249
|
+
const roleCandidates = getJsxPropStaticStringValues(roleAttribute, context.scopes);
|
|
44250
|
+
if (roleCandidates === null) return;
|
|
44251
|
+
const roles = new Set(roleCandidates.flatMap((candidate) => candidate.split(/\s+/).filter((token) => token.length > 0)));
|
|
44180
44252
|
for (const role of roles) {
|
|
44181
44253
|
const required = ROLE_REQUIRED_PROPS.get(role);
|
|
44182
44254
|
if (!required) continue;
|
|
@@ -47293,7 +47365,9 @@ const ROLE_SUPPORTS_ARIA_PROPS = {
|
|
|
47293
47365
|
};
|
|
47294
47366
|
//#endregion
|
|
47295
47367
|
//#region src/plugin/rules/a11y/role-supports-aria-props.ts
|
|
47296
|
-
const buildMessageDefault = (
|
|
47368
|
+
const buildMessageDefault = (roles, propName) => {
|
|
47369
|
+
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.`;
|
|
47370
|
+
};
|
|
47297
47371
|
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
47372
|
const roleSupportsAriaProps = defineRule({
|
|
47299
47373
|
id: "role-supports-aria-props",
|
|
@@ -47321,17 +47395,21 @@ const roleSupportsAriaProps = defineRule({
|
|
|
47321
47395
|
if (!ariaAttributes) return;
|
|
47322
47396
|
const elementType = getElementType(node, context.settings);
|
|
47323
47397
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
47324
|
-
const
|
|
47325
|
-
if (
|
|
47326
|
-
|
|
47398
|
+
const roleCandidates = roleAttribute ? getJsxPropStaticStringValues(roleAttribute, context.scopes) : [getImplicitRole(node, elementType)].filter((role) => role !== null);
|
|
47399
|
+
if (roleCandidates === null || roleCandidates.length === 0) return;
|
|
47400
|
+
const supportedSets = [];
|
|
47401
|
+
for (const role of roleCandidates) {
|
|
47402
|
+
if (!VALID_ARIA_ROLES.has(role)) return;
|
|
47403
|
+
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47404
|
+
if (!supported) return;
|
|
47405
|
+
supportedSets.push(supported);
|
|
47406
|
+
}
|
|
47327
47407
|
const isImplicit = !roleAttribute;
|
|
47328
|
-
const supported = ROLE_SUPPORTS_ARIA_PROPS[role];
|
|
47329
|
-
if (!supported) return;
|
|
47330
47408
|
for (const { attribute, propName } of ariaAttributes) {
|
|
47331
|
-
if (supported.has(propName)) continue;
|
|
47409
|
+
if (supportedSets.some((supported) => supported.has(propName))) continue;
|
|
47332
47410
|
context.report({
|
|
47333
47411
|
node: attribute,
|
|
47334
|
-
message: isImplicit ? buildMessageImplicit(
|
|
47412
|
+
message: isImplicit ? buildMessageImplicit(roleCandidates[0], propName, elementType) : buildMessageDefault(roleCandidates, propName)
|
|
47335
47413
|
});
|
|
47336
47414
|
}
|
|
47337
47415
|
} })
|
|
@@ -47683,11 +47761,15 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
47683
47761
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47684
47762
|
return getHookNameFromCallee(initializer.callee) === "useEffectEvent";
|
|
47685
47763
|
};
|
|
47686
|
-
const
|
|
47687
|
-
const
|
|
47764
|
+
const resolvesToLocalNonImportBinding = (identifier, scopes) => {
|
|
47765
|
+
const symbol = scopes.referenceFor(identifier)?.resolvedSymbol;
|
|
47766
|
+
return Boolean(symbol && symbol.kind !== "import");
|
|
47767
|
+
};
|
|
47768
|
+
const isNonReactEffectEventCallee = (callee, contextNode, scopes) => isNodeOfType(callee, "Identifier") && (isImportedFromNonReactModule(contextNode, callee.name) || resolvesToLocalNonImportBinding(callee, scopes));
|
|
47769
|
+
const isNonReactEffectEventSymbol = (symbol, contextNode, scopes) => {
|
|
47688
47770
|
const initializer = symbol.initializer;
|
|
47689
47771
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
47690
|
-
return isNonReactEffectEventCallee(initializer.callee, contextNode);
|
|
47772
|
+
return isNonReactEffectEventCallee(initializer.callee, contextNode, scopes);
|
|
47691
47773
|
};
|
|
47692
47774
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
47693
47775
|
let current = node.parent;
|
|
@@ -47766,7 +47848,7 @@ const rulesOfHooks = defineRule({
|
|
|
47766
47848
|
const hookContext = isHookCall$1(node, context.scopes, settings);
|
|
47767
47849
|
if (!hookContext) return;
|
|
47768
47850
|
const { hookName } = hookContext;
|
|
47769
|
-
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node)) {
|
|
47851
|
+
if (hookName === "useEffectEvent" && !isUseEffectEventInitializer(node) && !isNonReactEffectEventCallee(node.callee, node, context.scopes)) {
|
|
47770
47852
|
context.report({
|
|
47771
47853
|
node: node.callee,
|
|
47772
47854
|
message: buildEffectEventPassedDownMessage()
|
|
@@ -47879,7 +47961,7 @@ const rulesOfHooks = defineRule({
|
|
|
47879
47961
|
Identifier(node) {
|
|
47880
47962
|
const symbol = context.scopes.referenceFor(node)?.resolvedSymbol;
|
|
47881
47963
|
if (!symbol || !isUseEffectEventSymbol(symbol)) return;
|
|
47882
|
-
if (isNonReactEffectEventSymbol(symbol, node)) return;
|
|
47964
|
+
if (isNonReactEffectEventSymbol(symbol, node, context.scopes)) return;
|
|
47883
47965
|
if (!isSameComponentOrHookScope(symbol, node)) return;
|
|
47884
47966
|
if (isInsideAllowedEffectEventCallback(node, additionalEffectHooksRegex)) return;
|
|
47885
47967
|
context.report({
|
|
@@ -55451,6 +55533,34 @@ const reactDoctorRules = [
|
|
|
55451
55533
|
];
|
|
55452
55534
|
const ruleRegistry = Object.fromEntries(reactDoctorRules.map((rule) => [rule.id, rule.rule]));
|
|
55453
55535
|
//#endregion
|
|
55536
|
+
//#region src/plugin/utils/is-next-file.ts
|
|
55537
|
+
const isNextFileActive = (context) => {
|
|
55538
|
+
const rawFilename = context.filename;
|
|
55539
|
+
if (!rawFilename) return true;
|
|
55540
|
+
const filename = normalizeFilename(rawFilename);
|
|
55541
|
+
const manifest = readNearestPackageManifest(filename);
|
|
55542
|
+
if (!manifest) return true;
|
|
55543
|
+
if (declaresDependency(manifest, "next")) return true;
|
|
55544
|
+
if (!declaresAnyDependency(manifest)) return true;
|
|
55545
|
+
const packageDirectory = findNearestPackageDirectory(filename);
|
|
55546
|
+
const rootDirectory = getReactDoctorStringSetting(context.settings, "rootDirectory");
|
|
55547
|
+
if (packageDirectory !== null && isPackageNestedBelowProjectRoot(packageDirectory, rootDirectory)) return false;
|
|
55548
|
+
return true;
|
|
55549
|
+
};
|
|
55550
|
+
//#endregion
|
|
55551
|
+
//#region src/plugin/utils/wrap-nextjs-rule.ts
|
|
55552
|
+
const EMPTY_VISITORS$1 = {};
|
|
55553
|
+
const wrapNextjsRule = (rule) => {
|
|
55554
|
+
const innerCreate = rule.create.bind(rule);
|
|
55555
|
+
return {
|
|
55556
|
+
...rule,
|
|
55557
|
+
create: (context) => {
|
|
55558
|
+
if (!isNextFileActive(context)) return EMPTY_VISITORS$1;
|
|
55559
|
+
return innerCreate(context);
|
|
55560
|
+
}
|
|
55561
|
+
};
|
|
55562
|
+
};
|
|
55563
|
+
//#endregion
|
|
55454
55564
|
//#region src/plugin/utils/wrap-react-native-rule.ts
|
|
55455
55565
|
const EMPTY_VISITORS = {};
|
|
55456
55566
|
const wrapReactNativeRule = (rule) => {
|
|
@@ -55920,9 +56030,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
55920
56030
|
});
|
|
55921
56031
|
//#endregion
|
|
55922
56032
|
//#region src/plugin/react-doctor-plugin.ts
|
|
56033
|
+
const applyFrameworkGate = (rule) => {
|
|
56034
|
+
if (rule.framework === "react-native") return wrapReactNativeRule(rule);
|
|
56035
|
+
if (rule.framework === "nextjs") return wrapNextjsRule(rule);
|
|
56036
|
+
return rule;
|
|
56037
|
+
};
|
|
55923
56038
|
const applyFrameworkRuleWrappers = (registry) => {
|
|
55924
56039
|
const wrapped = {};
|
|
55925
|
-
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(
|
|
56040
|
+
for (const [ruleId, rule] of Object.entries(registry)) wrapped[ruleId] = wrapWithSemanticContext(applyFrameworkGate(rule));
|
|
55926
56041
|
return wrapped;
|
|
55927
56042
|
};
|
|
55928
56043
|
const plugin = {
|