oxlint-plugin-react-doctor 0.2.18-dev.5d7b36b → 0.2.18-dev.67be206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -2
- package/dist/index.js +120 -209
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -5047,7 +5047,7 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
5047
5047
|
readonly originallyExternal: false;
|
|
5048
5048
|
readonly rule: {
|
|
5049
5049
|
readonly framework: "react-native";
|
|
5050
|
-
readonly category: "
|
|
5050
|
+
readonly category: "Bugs";
|
|
5051
5051
|
readonly tags: readonly string[];
|
|
5052
5052
|
readonly id: string;
|
|
5053
5053
|
readonly title?: string;
|
|
@@ -11345,7 +11345,7 @@ declare const RULES: readonly [{
|
|
|
11345
11345
|
readonly originallyExternal: false;
|
|
11346
11346
|
readonly rule: {
|
|
11347
11347
|
readonly framework: "react-native";
|
|
11348
|
-
readonly category: "
|
|
11348
|
+
readonly category: "Bugs";
|
|
11349
11349
|
readonly tags: readonly string[];
|
|
11350
11350
|
readonly id: string;
|
|
11351
11351
|
readonly title?: string;
|
package/dist/index.js
CHANGED
|
@@ -2574,7 +2574,7 @@ const INTENTIONAL_SEQUENCING_CALLEE_NAMES = new Set([
|
|
|
2574
2574
|
* (`FUNCTION_LIKE_TYPES.has(node.type)`) and as a type-guard. The
|
|
2575
2575
|
* type-guard form covers both shapes without callers paying a cast.
|
|
2576
2576
|
*/
|
|
2577
|
-
const isFunctionLike$
|
|
2577
|
+
const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")));
|
|
2578
2578
|
//#endregion
|
|
2579
2579
|
//#region src/plugin/utils/is-inline-function-expression.ts
|
|
2580
2580
|
/**
|
|
@@ -2597,7 +2597,7 @@ const findFirstAwaitOutsideNestedFunctions = (block) => {
|
|
|
2597
2597
|
let firstAwait = null;
|
|
2598
2598
|
walkAst(block, (child) => {
|
|
2599
2599
|
if (firstAwait) return false;
|
|
2600
|
-
if (child !== block && isFunctionLike$
|
|
2600
|
+
if (child !== block && isFunctionLike$2(child)) return false;
|
|
2601
2601
|
if (isNodeOfType(child, "AwaitExpression")) firstAwait = child;
|
|
2602
2602
|
});
|
|
2603
2603
|
return firstAwait;
|
|
@@ -3054,13 +3054,13 @@ const asyncDeferAwait = defineRule({
|
|
|
3054
3054
|
const inspectAllStatementBlocks = (functionBody) => {
|
|
3055
3055
|
if (!functionBody) return;
|
|
3056
3056
|
walkAst(functionBody, (descendant) => {
|
|
3057
|
-
if (isFunctionLike$
|
|
3057
|
+
if (isFunctionLike$2(descendant)) return false;
|
|
3058
3058
|
if (isNodeOfType(descendant, "BlockStatement")) inspectStatements(descendant.body ?? []);
|
|
3059
3059
|
else if (isNodeOfType(descendant, "SwitchCase")) inspectStatements(descendant.consequent ?? []);
|
|
3060
3060
|
});
|
|
3061
3061
|
};
|
|
3062
3062
|
const enterFunction = (node) => {
|
|
3063
|
-
if (!isFunctionLike$
|
|
3063
|
+
if (!isFunctionLike$2(node)) return;
|
|
3064
3064
|
if (!node.async) return;
|
|
3065
3065
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
3066
3066
|
inspectAllStatementBlocks(node.body);
|
|
@@ -3954,6 +3954,7 @@ const noEmDashInJsxText = defineRule({
|
|
|
3954
3954
|
title: "Em dash in JSX text",
|
|
3955
3955
|
tags: ["design", "test-noise"],
|
|
3956
3956
|
severity: "warn",
|
|
3957
|
+
defaultEnabled: false,
|
|
3957
3958
|
category: "Architecture",
|
|
3958
3959
|
recommendation: "Replace em dashes in UI text with commas, colons, semicolons, or parentheses so the copy reads less like AI output.",
|
|
3959
3960
|
create: (context) => ({ JSXText(jsxTextNode) {
|
|
@@ -4000,6 +4001,7 @@ const noRedundantPaddingAxes = defineRule({
|
|
|
4000
4001
|
title: "Redundant padding axes",
|
|
4001
4002
|
tags: ["design", "test-noise"],
|
|
4002
4003
|
severity: "warn",
|
|
4004
|
+
defaultEnabled: false,
|
|
4003
4005
|
category: "Architecture",
|
|
4004
4006
|
recommendation: "Collapse `px-N py-N` to `p-N` when both sides match. Keep them split only when one side changes at a breakpoint (`py-2 md:py-3`).",
|
|
4005
4007
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
@@ -4023,6 +4025,7 @@ const noRedundantSizeAxes = defineRule({
|
|
|
4023
4025
|
requires: ["tailwind:3.4"],
|
|
4024
4026
|
tags: ["design", "test-noise"],
|
|
4025
4027
|
severity: "warn",
|
|
4028
|
+
defaultEnabled: false,
|
|
4026
4029
|
category: "Architecture",
|
|
4027
4030
|
recommendation: "Collapse `w-N h-N` to `size-N` (Tailwind v3.4+) when both sides match.",
|
|
4028
4031
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
@@ -4046,6 +4049,7 @@ const noSpaceOnFlexChildren = defineRule({
|
|
|
4046
4049
|
title: "space-* utility on flex children",
|
|
4047
4050
|
tags: ["design", "test-noise"],
|
|
4048
4051
|
severity: "warn",
|
|
4052
|
+
defaultEnabled: false,
|
|
4049
4053
|
category: "Architecture",
|
|
4050
4054
|
recommendation: "Use `gap-*` on the flex or grid parent. `space-x-*` and `space-y-*` leave gaps when a child is hidden, miss spacing on wrapped lines, and don't flip in right-to-left layouts.",
|
|
4051
4055
|
create: (context) => ({ JSXAttribute(jsxAttribute) {
|
|
@@ -4079,6 +4083,7 @@ const noThreePeriodEllipsis = defineRule({
|
|
|
4079
4083
|
title: "Three dots instead of ellipsis",
|
|
4080
4084
|
tags: ["design", "test-noise"],
|
|
4081
4085
|
severity: "warn",
|
|
4086
|
+
defaultEnabled: false,
|
|
4082
4087
|
category: "Architecture",
|
|
4083
4088
|
recommendation: "Use the real ellipsis \"…\" (or `…`) instead of three dots. Good for labels like \"Rename…\" and \"Loading…\".",
|
|
4084
4089
|
create: (context) => ({ JSXText(jsxTextNode) {
|
|
@@ -4138,6 +4143,7 @@ const noVagueButtonLabel = defineRule({
|
|
|
4138
4143
|
title: "Vague button label",
|
|
4139
4144
|
tags: ["design", "test-noise"],
|
|
4140
4145
|
severity: "warn",
|
|
4146
|
+
defaultEnabled: false,
|
|
4141
4147
|
recommendation: "Name the action: \"Save changes\" instead of \"Continue\", \"Send invite\" instead of \"Submit\". The label is the button's accessible name.",
|
|
4142
4148
|
create: (context) => ({ JSXElement(jsxElementNode) {
|
|
4143
4149
|
const tagName = getOpeningElementTagName(jsxElementNode.openingElement);
|
|
@@ -4509,7 +4515,7 @@ const displayName = defineRule({
|
|
|
4509
4515
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4510
4516
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4511
4517
|
if (!node || typeof node !== "object") return;
|
|
4512
|
-
if (isFunctionLike$
|
|
4518
|
+
if (isFunctionLike$2(node)) return;
|
|
4513
4519
|
visitor(node);
|
|
4514
4520
|
const nodeRecord = node;
|
|
4515
4521
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4597,7 +4603,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4597
4603
|
let didFindRelease = false;
|
|
4598
4604
|
walkAst(node, (child) => {
|
|
4599
4605
|
if (didFindRelease) return false;
|
|
4600
|
-
if (child !== node && isFunctionLike$
|
|
4606
|
+
if (child !== node && isFunctionLike$2(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4601
4607
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4602
4608
|
didFindRelease = true;
|
|
4603
4609
|
return false;
|
|
@@ -4606,7 +4612,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4606
4612
|
return didFindRelease;
|
|
4607
4613
|
};
|
|
4608
4614
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4609
|
-
if (!isFunctionLike$
|
|
4615
|
+
if (!isFunctionLike$2(node)) return false;
|
|
4610
4616
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4611
4617
|
};
|
|
4612
4618
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
@@ -4870,7 +4876,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4870
4876
|
};
|
|
4871
4877
|
const isFunctionBodyBlock = (block) => {
|
|
4872
4878
|
if (!block.parent) return false;
|
|
4873
|
-
return isFunctionLike$
|
|
4879
|
+
return isFunctionLike$2(block.parent);
|
|
4874
4880
|
};
|
|
4875
4881
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4876
4882
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -5025,7 +5031,7 @@ const walkParameterReferences = (pattern, state) => {
|
|
|
5025
5031
|
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
5026
5032
|
};
|
|
5027
5033
|
const walk = (node, state) => {
|
|
5028
|
-
if (isFunctionLike$
|
|
5034
|
+
if (isFunctionLike$2(node)) {
|
|
5029
5035
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
5030
5036
|
setNodeScope(node, state);
|
|
5031
5037
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -5271,7 +5277,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5271
5277
|
const out = [];
|
|
5272
5278
|
const seen = /* @__PURE__ */ new Set();
|
|
5273
5279
|
const visit = (node) => {
|
|
5274
|
-
if (node !== functionNode && isFunctionLike$
|
|
5280
|
+
if (node !== functionNode && isFunctionLike$2(node)) {
|
|
5275
5281
|
const innerCaptures = closureCaptures(node, scopes);
|
|
5276
5282
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
5277
5283
|
if (!seen.has(reference.id)) {
|
|
@@ -7391,7 +7397,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
7391
7397
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7392
7398
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7393
7399
|
};
|
|
7394
|
-
const isFunctionLike$
|
|
7400
|
+
const isFunctionLike$1 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7395
7401
|
const getFirstParameterName = (fn) => {
|
|
7396
7402
|
const parameters = fn.params ?? [];
|
|
7397
7403
|
if (parameters.length !== 1) return null;
|
|
@@ -7510,7 +7516,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7510
7516
|
const args = node.arguments ?? [];
|
|
7511
7517
|
if (args.length === 0) return;
|
|
7512
7518
|
const reader = args[0];
|
|
7513
|
-
if (!isFunctionLike$
|
|
7519
|
+
if (!isFunctionLike$1(reader)) return;
|
|
7514
7520
|
const getParameterName = getFirstParameterName(reader);
|
|
7515
7521
|
if (!getParameterName) return;
|
|
7516
7522
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -7766,7 +7772,7 @@ const isInsideLoopContext = (node) => {
|
|
|
7766
7772
|
let current = node.parent;
|
|
7767
7773
|
while (current) {
|
|
7768
7774
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
7769
|
-
if (isFunctionLike$
|
|
7775
|
+
if (isFunctionLike$2(current)) {
|
|
7770
7776
|
if (isIteratorCallback(current)) return true;
|
|
7771
7777
|
return false;
|
|
7772
7778
|
}
|
|
@@ -8127,7 +8133,7 @@ const jsHoistIntl = defineRule({
|
|
|
8127
8133
|
let cursor = node.parent ?? null;
|
|
8128
8134
|
let inFunctionBody = false;
|
|
8129
8135
|
while (cursor) {
|
|
8130
|
-
if (isFunctionLike$
|
|
8136
|
+
if (isFunctionLike$2(cursor)) {
|
|
8131
8137
|
inFunctionBody = true;
|
|
8132
8138
|
const fnParent = cursor.parent;
|
|
8133
8139
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -11808,6 +11814,7 @@ const jsxPascalCase = defineRule({
|
|
|
11808
11814
|
id: "jsx-pascal-case",
|
|
11809
11815
|
title: "Component name not PascalCase",
|
|
11810
11816
|
severity: "warn",
|
|
11817
|
+
defaultEnabled: false,
|
|
11811
11818
|
tags: ["test-noise"],
|
|
11812
11819
|
recommendation: "Rename custom JSX components to PascalCase.",
|
|
11813
11820
|
category: "Architecture",
|
|
@@ -13082,7 +13089,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
13082
13089
|
};
|
|
13083
13090
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
13084
13091
|
if (remainingDepth <= 0) return [];
|
|
13085
|
-
if (isFunctionLike$
|
|
13092
|
+
if (isFunctionLike$2(expression)) return expression.body ? [expression.body] : [];
|
|
13086
13093
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
13087
13094
|
for (const callArgument of expression.arguments ?? []) {
|
|
13088
13095
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -13552,7 +13559,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
13552
13559
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
13553
13560
|
if (isNodeOfType(fn, "Identifier")) {
|
|
13554
13561
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
13555
|
-
if (definitionNode && isFunctionLike$
|
|
13562
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return definitionNode;
|
|
13556
13563
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13557
13564
|
const initializer = definitionNode.init;
|
|
13558
13565
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -13645,14 +13652,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
13645
13652
|
return node ?? null;
|
|
13646
13653
|
};
|
|
13647
13654
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
13648
|
-
if (isFunctionLike$
|
|
13655
|
+
if (isFunctionLike$2(node)) return true;
|
|
13649
13656
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
13650
13657
|
if (isNodeOfType(node, "Identifier")) {
|
|
13651
13658
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
13652
|
-
if (definitionNode && isFunctionLike$
|
|
13659
|
+
if (definitionNode && isFunctionLike$2(definitionNode)) return true;
|
|
13653
13660
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13654
13661
|
const initializer = definitionNode.init;
|
|
13655
|
-
return isFunctionLike$
|
|
13662
|
+
return isFunctionLike$2(initializer);
|
|
13656
13663
|
}
|
|
13657
13664
|
}
|
|
13658
13665
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -13662,7 +13669,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13662
13669
|
if (visited.has(node)) return false;
|
|
13663
13670
|
visited.add(node);
|
|
13664
13671
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
13665
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$
|
|
13672
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$2(node)) return false;
|
|
13666
13673
|
const record = node;
|
|
13667
13674
|
for (const [key, value] of Object.entries(record)) {
|
|
13668
13675
|
if (key === "parent") continue;
|
|
@@ -13674,7 +13681,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13674
13681
|
};
|
|
13675
13682
|
const hasCleanup = (analysis, node) => {
|
|
13676
13683
|
const fn = getEffectFn(analysis, node);
|
|
13677
|
-
if (!isFunctionLike$
|
|
13684
|
+
if (!isFunctionLike$2(fn)) return false;
|
|
13678
13685
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
13679
13686
|
return hasCleanupReturn(analysis, fn.body);
|
|
13680
13687
|
};
|
|
@@ -14024,7 +14031,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
14024
14031
|
let current = node;
|
|
14025
14032
|
while (current.parent) {
|
|
14026
14033
|
const parent = current.parent;
|
|
14027
|
-
if (isFunctionLike$
|
|
14034
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
14028
14035
|
const callee = parent.callee;
|
|
14029
14036
|
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach")) return isStaticPlaceholderReceiver(callee.object);
|
|
14030
14037
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -14043,7 +14050,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
14043
14050
|
let current = node;
|
|
14044
14051
|
while (current.parent) {
|
|
14045
14052
|
const parent = current.parent;
|
|
14046
|
-
if (isFunctionLike$
|
|
14053
|
+
if (isFunctionLike$2(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
14047
14054
|
const callee = parent.callee;
|
|
14048
14055
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
14049
14056
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -14266,6 +14273,7 @@ const noArrayIndexKey = defineRule({
|
|
|
14266
14273
|
id: "no-array-index-key",
|
|
14267
14274
|
title: "Array index used as a key",
|
|
14268
14275
|
severity: "warn",
|
|
14276
|
+
defaultEnabled: false,
|
|
14269
14277
|
recommendation: "Use a stable `key` from your data instead of the array index.",
|
|
14270
14278
|
category: "Performance",
|
|
14271
14279
|
create: (context) => ({
|
|
@@ -14778,6 +14786,37 @@ const isSetterIdentifier = (name) => SETTER_PATTERN.test(name);
|
|
|
14778
14786
|
//#region src/plugin/utils/is-setter-call.ts
|
|
14779
14787
|
const isSetterCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && isSetterIdentifier(node.callee.name);
|
|
14780
14788
|
//#endregion
|
|
14789
|
+
//#region src/plugin/utils/is-hook-binding-in-scope.ts
|
|
14790
|
+
const isHookBindingInScope = (node, query) => {
|
|
14791
|
+
const { bindingName, hookName, destructureIndex } = query;
|
|
14792
|
+
let cursor = node;
|
|
14793
|
+
while (cursor) {
|
|
14794
|
+
if (isNodeOfType(cursor, "BlockStatement") || isNodeOfType(cursor, "Program")) for (const statement of cursor.body ?? []) {
|
|
14795
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
14796
|
+
for (const declarator of statement.declarations ?? []) {
|
|
14797
|
+
if (!isNodeOfType(declarator.init, "CallExpression")) continue;
|
|
14798
|
+
if (!isHookCall$1(declarator.init, hookName)) continue;
|
|
14799
|
+
if (destructureIndex !== void 0) {
|
|
14800
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) continue;
|
|
14801
|
+
const elements = declarator.id.elements ?? [];
|
|
14802
|
+
if (elements.length <= destructureIndex) continue;
|
|
14803
|
+
const element = elements[destructureIndex];
|
|
14804
|
+
if (isNodeOfType(element, "Identifier") && element.name === bindingName) return true;
|
|
14805
|
+
} else if (isNodeOfType(declarator.id, "Identifier") && declarator.id.name === bindingName) return true;
|
|
14806
|
+
}
|
|
14807
|
+
}
|
|
14808
|
+
cursor = cursor.parent ?? null;
|
|
14809
|
+
}
|
|
14810
|
+
return false;
|
|
14811
|
+
};
|
|
14812
|
+
//#endregion
|
|
14813
|
+
//#region src/plugin/utils/is-use-state-setter-in-scope.ts
|
|
14814
|
+
const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node, {
|
|
14815
|
+
bindingName: setterName,
|
|
14816
|
+
hookName: "useState",
|
|
14817
|
+
destructureIndex: 1
|
|
14818
|
+
});
|
|
14819
|
+
//#endregion
|
|
14781
14820
|
//#region src/plugin/rules/state-and-effects/no-cascading-set-state.ts
|
|
14782
14821
|
const isAsyncFunctionLike = (node) => {
|
|
14783
14822
|
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) return Boolean(node.async);
|
|
@@ -14819,7 +14858,7 @@ const countMaxPathSetStateCalls = (node) => {
|
|
|
14819
14858
|
const finallyCount = node.finalizer ? countMaxPathSetStateCalls(node.finalizer) : 0;
|
|
14820
14859
|
return Math.max(tryCount, catchCount) + finallyCount;
|
|
14821
14860
|
}
|
|
14822
|
-
if (isSetterCall(node)) {
|
|
14861
|
+
if (isNodeOfType(node, "CallExpression") && isSetterCall(node) && isNodeOfType(node.callee, "Identifier") && isUseStateSetterInScope(node, node.callee.name)) {
|
|
14823
14862
|
let nestedSettersInArgs = 0;
|
|
14824
14863
|
for (const argument of node.arguments ?? []) nestedSettersInArgs += countMaxPathSetStateCalls(argument);
|
|
14825
14864
|
return 1 + nestedSettersInArgs;
|
|
@@ -14988,7 +15027,7 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
|
14988
15027
|
const nearestEnclosingFunction = (node) => {
|
|
14989
15028
|
let cursor = node.parent;
|
|
14990
15029
|
while (cursor) {
|
|
14991
|
-
if (isFunctionLike$
|
|
15030
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
14992
15031
|
cursor = cursor.parent ?? null;
|
|
14993
15032
|
}
|
|
14994
15033
|
return null;
|
|
@@ -15405,6 +15444,7 @@ const noDarkModeGlow = defineRule({
|
|
|
15405
15444
|
title: "Colored glow on dark background",
|
|
15406
15445
|
tags: ["design", "test-noise"],
|
|
15407
15446
|
severity: "warn",
|
|
15447
|
+
defaultEnabled: false,
|
|
15408
15448
|
recommendation: "Use a subtle `box-shadow` in neutral colors for depth, or a faint `border`. Colored glows on dark backgrounds look overdone.",
|
|
15409
15449
|
create: (context) => ({ JSXAttribute(node) {
|
|
15410
15450
|
const expression = getInlineStyleExpression(node);
|
|
@@ -15439,6 +15479,7 @@ const noDefaultProps = defineRule({
|
|
|
15439
15479
|
requires: ["react:19"],
|
|
15440
15480
|
tags: ["test-noise"],
|
|
15441
15481
|
severity: "warn",
|
|
15482
|
+
defaultEnabled: false,
|
|
15442
15483
|
recommendation: "React 19 drops `Component.defaultProps` for function components. Set the defaults in the destructured props instead: `function Foo({ size = \"md\", variant = \"primary\" })` instead of `Foo.defaultProps = { size: \"md\", variant: \"primary\" }`.",
|
|
15443
15484
|
create: (context) => ({ AssignmentExpression(node) {
|
|
15444
15485
|
if (node.operator !== "=") return;
|
|
@@ -15618,7 +15659,11 @@ const noDerivedStateEffect = defineRule({
|
|
|
15618
15659
|
if (statements.length === 0) return;
|
|
15619
15660
|
if (!statements.every((statement) => {
|
|
15620
15661
|
if (!isNodeOfType(statement, "ExpressionStatement")) return false;
|
|
15621
|
-
|
|
15662
|
+
const expression = statement.expression;
|
|
15663
|
+
if (!isSetterCall(expression)) return false;
|
|
15664
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
15665
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return false;
|
|
15666
|
+
return isUseStateSetterInScope(expression, expression.callee.name);
|
|
15622
15667
|
})) return;
|
|
15623
15668
|
let allArgumentsDeriveFromDeps = true;
|
|
15624
15669
|
let hasAnyDependencyReference = false;
|
|
@@ -15671,7 +15716,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
15671
15716
|
};
|
|
15672
15717
|
const getInlineFunctionNode = (node) => {
|
|
15673
15718
|
if (!node) return null;
|
|
15674
|
-
if (isFunctionLike$
|
|
15719
|
+
if (isFunctionLike$2(node)) return node;
|
|
15675
15720
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
15676
15721
|
for (const argument of node.arguments ?? []) {
|
|
15677
15722
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -15682,7 +15727,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
15682
15727
|
const getNearestComponentFunction = (node) => {
|
|
15683
15728
|
let cursor = node.parent ?? null;
|
|
15684
15729
|
while (cursor) {
|
|
15685
|
-
if (isFunctionLike$
|
|
15730
|
+
if (isFunctionLike$2(cursor)) return cursor;
|
|
15686
15731
|
cursor = cursor.parent ?? null;
|
|
15687
15732
|
}
|
|
15688
15733
|
return null;
|
|
@@ -16012,7 +16057,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
16012
16057
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
16013
16058
|
if (!node || typeof node !== "object") return;
|
|
16014
16059
|
let nextShadowedStateNames = shadowedStateNames;
|
|
16015
|
-
if (isFunctionLike$
|
|
16060
|
+
if (isFunctionLike$2(node)) {
|
|
16016
16061
|
const localBindings = collectFunctionLocalBindings(node);
|
|
16017
16062
|
if (localBindings.size > 0) {
|
|
16018
16063
|
const merged = new Set(shadowedStateNames);
|
|
@@ -17611,6 +17656,7 @@ const noGenericHandlerNames = defineRule({
|
|
|
17611
17656
|
id: "no-generic-handler-names",
|
|
17612
17657
|
title: "Vague event handler name",
|
|
17613
17658
|
severity: "warn",
|
|
17659
|
+
defaultEnabled: false,
|
|
17614
17660
|
tags: ["test-noise"],
|
|
17615
17661
|
recommendation: "Rename it to say what it does. For example `handleSubmit` could be `saveUserProfile`, and `handleClick` could be `toggleSidebar`.",
|
|
17616
17662
|
create: (context) => ({ JSXAttribute(node) {
|
|
@@ -17812,6 +17858,7 @@ const noGradientText = defineRule({
|
|
|
17812
17858
|
title: "Gradient text is hard to read",
|
|
17813
17859
|
tags: ["design", "test-noise"],
|
|
17814
17860
|
severity: "warn",
|
|
17861
|
+
defaultEnabled: false,
|
|
17815
17862
|
recommendation: "Use a solid text color so it stays readable. For emphasis, change the weight, size, or color instead of using a gradient.",
|
|
17816
17863
|
create: (context) => ({
|
|
17817
17864
|
JSXAttribute(node) {
|
|
@@ -18098,6 +18145,7 @@ const noJustifiedText = defineRule({
|
|
|
18098
18145
|
title: "Justified text without hyphens",
|
|
18099
18146
|
tags: ["test-noise"],
|
|
18100
18147
|
severity: "warn",
|
|
18148
|
+
defaultEnabled: false,
|
|
18101
18149
|
category: "Accessibility",
|
|
18102
18150
|
recommendation: "Use `text-align: left` for body text. If you must justify, add `hyphens: auto` and `overflow-wrap: break-word`.",
|
|
18103
18151
|
create: (context) => ({ JSXAttribute(node) {
|
|
@@ -18269,7 +18317,7 @@ const isInsideClassBody = (node) => {
|
|
|
18269
18317
|
let current = node.parent;
|
|
18270
18318
|
while (current) {
|
|
18271
18319
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
18272
|
-
if (isFunctionLike$
|
|
18320
|
+
if (isFunctionLike$2(current)) return false;
|
|
18273
18321
|
current = current.parent;
|
|
18274
18322
|
}
|
|
18275
18323
|
return false;
|
|
@@ -18973,7 +19021,7 @@ const isLodashMutatorCall = (callExpression) => {
|
|
|
18973
19021
|
};
|
|
18974
19022
|
//#endregion
|
|
18975
19023
|
//#region src/plugin/utils/find-exported-function-body.ts
|
|
18976
|
-
const isFunctionLike
|
|
19024
|
+
const isFunctionLike = (node) => {
|
|
18977
19025
|
if (!node) return false;
|
|
18978
19026
|
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
18979
19027
|
};
|
|
@@ -18988,7 +19036,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
18988
19036
|
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
18989
19037
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
18990
19038
|
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
18991
|
-
if (initializer && isFunctionLike
|
|
19039
|
+
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18992
19040
|
}
|
|
18993
19041
|
};
|
|
18994
19042
|
for (const statement of programRoot.body ?? []) {
|
|
@@ -19032,7 +19080,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
19032
19080
|
defaultExport = declaration;
|
|
19033
19081
|
continue;
|
|
19034
19082
|
}
|
|
19035
|
-
if (isFunctionLike
|
|
19083
|
+
if (isFunctionLike(declaration)) {
|
|
19036
19084
|
defaultExport = declaration;
|
|
19037
19085
|
continue;
|
|
19038
19086
|
}
|
|
@@ -19193,7 +19241,7 @@ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
|
19193
19241
|
const resolveReducerFunction = (node, currentFilename) => {
|
|
19194
19242
|
if (!node) return null;
|
|
19195
19243
|
const unwrappedNode = stripParenExpression(node);
|
|
19196
|
-
if (isFunctionLike$
|
|
19244
|
+
if (isFunctionLike$2(unwrappedNode)) return {
|
|
19197
19245
|
functionNode: unwrappedNode,
|
|
19198
19246
|
crossFileSourceDisplay: null
|
|
19199
19247
|
};
|
|
@@ -19201,7 +19249,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
19201
19249
|
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
19202
19250
|
if (!initializer) return null;
|
|
19203
19251
|
const unwrappedInitializer = stripParenExpression(initializer);
|
|
19204
|
-
if (isFunctionLike$
|
|
19252
|
+
if (isFunctionLike$2(unwrappedInitializer)) return {
|
|
19205
19253
|
functionNode: unwrappedInitializer,
|
|
19206
19254
|
crossFileSourceDisplay: null
|
|
19207
19255
|
};
|
|
@@ -19315,11 +19363,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
19315
19363
|
return false;
|
|
19316
19364
|
};
|
|
19317
19365
|
const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
19318
|
-
if (isFunctionLike$
|
|
19366
|
+
if (isFunctionLike$2(node)) return [];
|
|
19319
19367
|
const mutations = [];
|
|
19320
19368
|
walkAst(node, (child) => {
|
|
19321
19369
|
const unwrappedChild = stripParenExpression(child);
|
|
19322
|
-
if (child !== node && isFunctionLike$
|
|
19370
|
+
if (child !== node && isFunctionLike$2(unwrappedChild)) return false;
|
|
19323
19371
|
if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
|
|
19324
19372
|
if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
|
|
19325
19373
|
return;
|
|
@@ -19424,7 +19472,7 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
|
|
|
19424
19472
|
if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
|
|
19425
19473
|
};
|
|
19426
19474
|
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
|
|
19427
|
-
if (!isFunctionLike$
|
|
19475
|
+
if (!isFunctionLike$2(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
|
|
19428
19476
|
const firstParam = functionNode.params?.[0];
|
|
19429
19477
|
const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
|
|
19430
19478
|
if (!stateName) return;
|
|
@@ -20345,6 +20393,7 @@ const noPropTypes = defineRule({
|
|
|
20345
20393
|
requires: ["react:19"],
|
|
20346
20394
|
tags: ["test-noise"],
|
|
20347
20395
|
severity: "warn",
|
|
20396
|
+
defaultEnabled: false,
|
|
20348
20397
|
recommendation: "React 19 ignores `Component.propTypes`, so invalid props pass silently. Describe props with TypeScript types and add real runtime checks (or schema parsing) only where data can actually be wrong. Only runs on React 19+ projects.",
|
|
20349
20398
|
create: (context) => ({
|
|
20350
20399
|
AssignmentExpression(node) {
|
|
@@ -20373,6 +20422,7 @@ const noPureBlackBackground = defineRule({
|
|
|
20373
20422
|
title: "Pure black background",
|
|
20374
20423
|
tags: ["design", "test-noise"],
|
|
20375
20424
|
severity: "warn",
|
|
20425
|
+
defaultEnabled: false,
|
|
20376
20426
|
recommendation: "Nudge the background slightly toward your brand color, like `#0a0a0f` or Tailwind's `bg-gray-950`. Pure black looks harsh on modern screens.",
|
|
20377
20427
|
create: (context) => ({
|
|
20378
20428
|
JSXAttribute(node) {
|
|
@@ -21273,7 +21323,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
21273
21323
|
const isInsideServerOnlyScope = (node) => {
|
|
21274
21324
|
let currentNode = node.parent ?? null;
|
|
21275
21325
|
while (currentNode) {
|
|
21276
|
-
if (isFunctionLike$
|
|
21326
|
+
if (isFunctionLike$2(currentNode)) {
|
|
21277
21327
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
21278
21328
|
}
|
|
21279
21329
|
currentNode = currentNode.parent ?? null;
|
|
@@ -21802,6 +21852,7 @@ const noSideTabBorder = defineRule({
|
|
|
21802
21852
|
title: "Thick one-sided border",
|
|
21803
21853
|
tags: ["design", "test-noise"],
|
|
21804
21854
|
severity: "warn",
|
|
21855
|
+
defaultEnabled: false,
|
|
21805
21856
|
recommendation: "Use a softer accent like an inset box-shadow, a background, or a thin border-bottom instead of a thick one-sided border.",
|
|
21806
21857
|
create: (context) => ({
|
|
21807
21858
|
JSXAttribute(node) {
|
|
@@ -23428,7 +23479,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
23428
23479
|
const findEnclosingComponent = (node) => {
|
|
23429
23480
|
let walker = node.parent;
|
|
23430
23481
|
while (walker) {
|
|
23431
|
-
if (isFunctionLike$
|
|
23482
|
+
if (isFunctionLike$2(walker)) {
|
|
23432
23483
|
const componentName = inferFunctionLikeName(walker);
|
|
23433
23484
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
23434
23485
|
component: walker,
|
|
@@ -23713,6 +23764,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
23713
23764
|
id: "no-wide-letter-spacing",
|
|
23714
23765
|
title: "Wide letter spacing on body text",
|
|
23715
23766
|
severity: "warn",
|
|
23767
|
+
defaultEnabled: false,
|
|
23716
23768
|
tags: ["test-noise"],
|
|
23717
23769
|
recommendation: "Save wide letter-spacing (over 0.05em) for short uppercase labels, nav items, and buttons, not body text.",
|
|
23718
23770
|
create: (context) => ({ JSXAttribute(node) {
|
|
@@ -23796,6 +23848,7 @@ const noZIndex9999 = defineRule({
|
|
|
23796
23848
|
title: "Excessively high z-index",
|
|
23797
23849
|
tags: ["test-noise"],
|
|
23798
23850
|
severity: "warn",
|
|
23851
|
+
defaultEnabled: false,
|
|
23799
23852
|
recommendation: "Pick a small z-index scale, like dropdown 10, modal 20, toast 30. To layer something on top, use `isolation: isolate` instead of bigger numbers.",
|
|
23800
23853
|
create: (context) => ({
|
|
23801
23854
|
JSXAttribute(node) {
|
|
@@ -24571,6 +24624,7 @@ const preferEs6Class = defineRule({
|
|
|
24571
24624
|
id: "prefer-es6-class",
|
|
24572
24625
|
title: "createClass instead of ES6 class",
|
|
24573
24626
|
severity: "warn",
|
|
24627
|
+
defaultEnabled: false,
|
|
24574
24628
|
recommendation: "Pick one component style for the whole codebase: `class extends React.Component` (default) or `createReactClass` (legacy).",
|
|
24575
24629
|
category: "Architecture",
|
|
24576
24630
|
create: (context) => {
|
|
@@ -25694,7 +25748,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
25694
25748
|
if (!body) return;
|
|
25695
25749
|
const returnedExpressions = [];
|
|
25696
25750
|
if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
|
|
25697
|
-
if (node !== body && isFunctionLike$
|
|
25751
|
+
if (node !== body && isFunctionLike$2(node)) return false;
|
|
25698
25752
|
if (isNodeOfType(node, "ReturnStatement")) {
|
|
25699
25753
|
if (node.argument) returnedExpressions.push(node.argument);
|
|
25700
25754
|
return false;
|
|
@@ -25949,7 +26003,9 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
25949
26003
|
const bodyStatements = isNodeOfType(callback.body, "BlockStatement") ? callback.body.body : [callback.body];
|
|
25950
26004
|
if (!bodyStatements || bodyStatements.length !== 1) return;
|
|
25951
26005
|
const soleStatement = bodyStatements[0];
|
|
25952
|
-
if (isNodeOfType(soleStatement, "ExpressionStatement")
|
|
26006
|
+
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
26007
|
+
const expression = soleStatement.expression;
|
|
26008
|
+
if (isSetterCall(expression) && isNodeOfType(expression, "CallExpression") && isNodeOfType(expression.callee, "Identifier") && isUseStateSetterInScope(expression, expression.callee.name)) context.report({
|
|
25953
26009
|
node,
|
|
25954
26010
|
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
25955
26011
|
});
|
|
@@ -26037,7 +26093,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
26037
26093
|
let found = false;
|
|
26038
26094
|
walkAst(functionBody, (child) => {
|
|
26039
26095
|
if (found) return;
|
|
26040
|
-
if (child !== functionBody && isFunctionLike$
|
|
26096
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
26041
26097
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
26042
26098
|
});
|
|
26043
26099
|
return found;
|
|
@@ -26056,7 +26112,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
26056
26112
|
let found = false;
|
|
26057
26113
|
walkAst(componentBody, (child) => {
|
|
26058
26114
|
if (found) return;
|
|
26059
|
-
if (!isFunctionLike$
|
|
26115
|
+
if (!isFunctionLike$2(child)) return;
|
|
26060
26116
|
const functionBody = child.body;
|
|
26061
26117
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
26062
26118
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -26472,6 +26528,7 @@ const rerenderFunctionalSetstate = defineRule({
|
|
|
26472
26528
|
if (!isSetterCall(node)) return;
|
|
26473
26529
|
if (!node.arguments?.length) return;
|
|
26474
26530
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
26531
|
+
if (!isUseStateSetterInScope(node, node.callee.name)) return;
|
|
26475
26532
|
const calleeName = node.callee.name;
|
|
26476
26533
|
const argument = node.arguments[0];
|
|
26477
26534
|
const expectedStateName = deriveStateVariableName(calleeName);
|
|
@@ -26806,7 +26863,7 @@ const handlerCallsSetState = (handler) => {
|
|
|
26806
26863
|
let setStateCall = null;
|
|
26807
26864
|
walkAst(handler.body, (child) => {
|
|
26808
26865
|
if (setStateCall) return;
|
|
26809
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name)) setStateCall = child;
|
|
26866
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && /^set[A-Z]/.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCall = child;
|
|
26810
26867
|
});
|
|
26811
26868
|
return setStateCall;
|
|
26812
26869
|
};
|
|
@@ -26838,160 +26895,22 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
26838
26895
|
} })
|
|
26839
26896
|
});
|
|
26840
26897
|
//#endregion
|
|
26898
|
+
//#region src/plugin/utils/define-retired-rule.ts
|
|
26899
|
+
const defineRetiredRule = (rule) => defineRule({
|
|
26900
|
+
...rule,
|
|
26901
|
+
defaultEnabled: false,
|
|
26902
|
+
lifecycle: "retired",
|
|
26903
|
+
create: () => ({})
|
|
26904
|
+
});
|
|
26905
|
+
//#endregion
|
|
26841
26906
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
26842
|
-
const
|
|
26843
|
-
const REANIMATED_LAYOUT_STYLE_PROPERTIES = new Set([
|
|
26844
|
-
"width",
|
|
26845
|
-
"height",
|
|
26846
|
-
"top",
|
|
26847
|
-
"left",
|
|
26848
|
-
"right",
|
|
26849
|
-
"bottom",
|
|
26850
|
-
"minWidth",
|
|
26851
|
-
"minHeight",
|
|
26852
|
-
"maxWidth",
|
|
26853
|
-
"maxHeight",
|
|
26854
|
-
"margin",
|
|
26855
|
-
"marginTop",
|
|
26856
|
-
"marginBottom",
|
|
26857
|
-
"marginLeft",
|
|
26858
|
-
"marginRight",
|
|
26859
|
-
"marginHorizontal",
|
|
26860
|
-
"marginVertical",
|
|
26861
|
-
"padding",
|
|
26862
|
-
"paddingTop",
|
|
26863
|
-
"paddingBottom",
|
|
26864
|
-
"paddingLeft",
|
|
26865
|
-
"paddingRight",
|
|
26866
|
-
"paddingHorizontal",
|
|
26867
|
-
"paddingVertical",
|
|
26868
|
-
"flex",
|
|
26869
|
-
"flexBasis",
|
|
26870
|
-
"flexGrow",
|
|
26871
|
-
"flexShrink",
|
|
26872
|
-
"borderWidth",
|
|
26873
|
-
"borderTopWidth",
|
|
26874
|
-
"borderBottomWidth",
|
|
26875
|
-
"borderLeftWidth",
|
|
26876
|
-
"borderRightWidth",
|
|
26877
|
-
"fontSize",
|
|
26878
|
-
"lineHeight",
|
|
26879
|
-
"letterSpacing"
|
|
26880
|
-
]);
|
|
26881
|
-
const REANIMATED_ANIMATION_HELPERS = new Set([
|
|
26882
|
-
"withTiming",
|
|
26883
|
-
"withSpring",
|
|
26884
|
-
"withDecay",
|
|
26885
|
-
"withDelay",
|
|
26886
|
-
"withRepeat",
|
|
26887
|
-
"withSequence",
|
|
26888
|
-
"withClamp"
|
|
26889
|
-
]);
|
|
26890
|
-
const findImportDeclaration = (symbol) => {
|
|
26891
|
-
let currentNode = symbol.declarationNode.parent;
|
|
26892
|
-
while (currentNode && !isNodeOfType(currentNode, "ImportDeclaration")) currentNode = currentNode.parent ?? null;
|
|
26893
|
-
return currentNode && isNodeOfType(currentNode, "ImportDeclaration") ? currentNode : null;
|
|
26894
|
-
};
|
|
26895
|
-
const isReanimatedImport = (symbol) => {
|
|
26896
|
-
const importDeclaration = findImportDeclaration(symbol);
|
|
26897
|
-
if (!importDeclaration) return false;
|
|
26898
|
-
return importDeclaration.source.value === REANIMATED_MODULE;
|
|
26899
|
-
};
|
|
26900
|
-
const getReanimatedNamedImport = (node, context) => {
|
|
26901
|
-
if (!isNodeOfType(node, "Identifier")) return null;
|
|
26902
|
-
const symbol = context.scopes.symbolFor(node);
|
|
26903
|
-
if (!symbol || symbol.kind !== "import") return null;
|
|
26904
|
-
if (!isReanimatedImport(symbol)) return null;
|
|
26905
|
-
const declarationNode = symbol.declarationNode;
|
|
26906
|
-
if (!isNodeOfType(declarationNode, "ImportSpecifier")) return null;
|
|
26907
|
-
const importedName = declarationNode.imported;
|
|
26908
|
-
if (isNodeOfType(importedName, "Identifier")) return importedName.name;
|
|
26909
|
-
if (isNodeOfType(importedName, "Literal") && typeof importedName.value === "string") return importedName.value;
|
|
26910
|
-
return null;
|
|
26911
|
-
};
|
|
26912
|
-
const isReanimatedNamespace = (node, context) => {
|
|
26913
|
-
if (!isNodeOfType(node, "Identifier")) return false;
|
|
26914
|
-
const symbol = context.scopes.symbolFor(node);
|
|
26915
|
-
if (!symbol || symbol.kind !== "import") return false;
|
|
26916
|
-
if (!isReanimatedImport(symbol)) return false;
|
|
26917
|
-
return isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier");
|
|
26918
|
-
};
|
|
26919
|
-
const isUseAnimatedStyleCallee = (callee, context) => {
|
|
26920
|
-
if (isNodeOfType(callee, "Identifier")) return getReanimatedNamedImport(callee, context) === "useAnimatedStyle";
|
|
26921
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
26922
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
26923
|
-
if (callee.property.name !== "useAnimatedStyle") return false;
|
|
26924
|
-
return isReanimatedNamespace(callee.object, context);
|
|
26925
|
-
};
|
|
26926
|
-
const isReanimatedAnimationHelperCallee = (callee, context) => {
|
|
26927
|
-
if (isNodeOfType(callee, "Identifier")) {
|
|
26928
|
-
const importedName = getReanimatedNamedImport(callee, context);
|
|
26929
|
-
return importedName !== null && REANIMATED_ANIMATION_HELPERS.has(importedName);
|
|
26930
|
-
}
|
|
26931
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
26932
|
-
if (!isNodeOfType(callee.property, "Identifier")) return false;
|
|
26933
|
-
if (!REANIMATED_ANIMATION_HELPERS.has(callee.property.name)) return false;
|
|
26934
|
-
return isReanimatedNamespace(callee.object, context);
|
|
26935
|
-
};
|
|
26936
|
-
const isFunctionLike = (node) => isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration");
|
|
26937
|
-
const containsReanimatedAnimationHelperCall = (expression, context) => {
|
|
26938
|
-
const rootExpression = stripParenExpression(expression);
|
|
26939
|
-
let didFindAnimationHelper = false;
|
|
26940
|
-
walkAst(rootExpression, (node) => {
|
|
26941
|
-
if (didFindAnimationHelper) return false;
|
|
26942
|
-
if (node !== rootExpression && isFunctionLike(node)) return false;
|
|
26943
|
-
if (isNodeOfType(node, "CallExpression") && isReanimatedAnimationHelperCallee(node.callee, context)) {
|
|
26944
|
-
didFindAnimationHelper = true;
|
|
26945
|
-
return false;
|
|
26946
|
-
}
|
|
26947
|
-
});
|
|
26948
|
-
return didFindAnimationHelper;
|
|
26949
|
-
};
|
|
26950
|
-
const findReturnedObject = (callback) => {
|
|
26951
|
-
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
|
|
26952
|
-
const body = stripParenExpression(callback.body);
|
|
26953
|
-
if (isNodeOfType(body, "ObjectExpression")) return body;
|
|
26954
|
-
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
26955
|
-
for (const statement of body.body ?? []) {
|
|
26956
|
-
if (!isNodeOfType(statement, "ReturnStatement")) continue;
|
|
26957
|
-
if (!statement.argument) continue;
|
|
26958
|
-
const returnArgument = stripParenExpression(statement.argument);
|
|
26959
|
-
if (isNodeOfType(returnArgument, "ObjectExpression")) return returnArgument;
|
|
26960
|
-
}
|
|
26961
|
-
return null;
|
|
26962
|
-
};
|
|
26963
|
-
const getStaticPropertyName$1 = (property) => {
|
|
26964
|
-
if (!property.computed && isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
26965
|
-
if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
|
|
26966
|
-
return null;
|
|
26967
|
-
};
|
|
26968
|
-
const rnAnimateLayoutProperty = defineRule({
|
|
26907
|
+
const rnAnimateLayoutProperty = defineRetiredRule({
|
|
26969
26908
|
id: "rn-animate-layout-property",
|
|
26970
26909
|
title: "Animating a layout property",
|
|
26971
26910
|
tags: ["test-noise"],
|
|
26972
26911
|
requires: ["react-native"],
|
|
26973
26912
|
severity: "warn",
|
|
26974
|
-
|
|
26975
|
-
recommendation: "Prefer transform or opacity when a Reanimated animation helper drives purely visual motion; use layout-affecting styles only when the layout itself must change.",
|
|
26976
|
-
create: (context) => ({ CallExpression(node) {
|
|
26977
|
-
if (!isUseAnimatedStyleCallee(node.callee, context)) return;
|
|
26978
|
-
const callback = node.arguments?.[0];
|
|
26979
|
-
if (!callback) return;
|
|
26980
|
-
const returnedObject = findReturnedObject(callback);
|
|
26981
|
-
if (!returnedObject) return;
|
|
26982
|
-
for (const property of returnedObject.properties ?? []) {
|
|
26983
|
-
if (!isNodeOfType(property, "Property")) continue;
|
|
26984
|
-
const propertyName = getStaticPropertyName$1(property);
|
|
26985
|
-
if (propertyName === null) continue;
|
|
26986
|
-
if (!REANIMATED_LAYOUT_STYLE_PROPERTIES.has(propertyName)) continue;
|
|
26987
|
-
const propertyValue = property.value;
|
|
26988
|
-
if (!containsReanimatedAnimationHelperCall(propertyValue, context)) continue;
|
|
26989
|
-
context.report({
|
|
26990
|
-
node: property,
|
|
26991
|
-
message: `Reanimated can animate "${propertyName}", but this layout-affecting style recalculates layout while the animation runs; prefer transform or opacity when the motion is only visual.`
|
|
26992
|
-
});
|
|
26993
|
-
}
|
|
26994
|
-
} })
|
|
26913
|
+
recommendation: "Reanimated useAnimatedStyle runs on the UI thread; layout-affecting properties driven by animation helpers, interpolate, or shared values are valid."
|
|
26995
26914
|
});
|
|
26996
26915
|
//#endregion
|
|
26997
26916
|
//#region src/plugin/rules/react-native/rn-animation-reaction-as-derived.ts
|
|
@@ -28192,7 +28111,7 @@ const findSetStateInBody = (body) => {
|
|
|
28192
28111
|
let setStateCallNode = null;
|
|
28193
28112
|
walkAst(body, (child) => {
|
|
28194
28113
|
if (setStateCallNode) return;
|
|
28195
|
-
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name)) setStateCallNode = child;
|
|
28114
|
+
if (isNodeOfType(child, "CallExpression") && isNodeOfType(child.callee, "Identifier") && SET_STATE_PATTERN.test(child.callee.name) && isUseStateSetterInScope(child, child.callee.name)) setStateCallNode = child;
|
|
28196
28115
|
});
|
|
28197
28116
|
return setStateCallNode;
|
|
28198
28117
|
};
|
|
@@ -28318,14 +28237,6 @@ const rnNoSingleElementStyleArray = defineRule({
|
|
|
28318
28237
|
} })
|
|
28319
28238
|
});
|
|
28320
28239
|
//#endregion
|
|
28321
|
-
//#region src/plugin/utils/define-retired-rule.ts
|
|
28322
|
-
const defineRetiredRule = (rule) => defineRule({
|
|
28323
|
-
...rule,
|
|
28324
|
-
defaultEnabled: false,
|
|
28325
|
-
lifecycle: "retired",
|
|
28326
|
-
create: () => ({})
|
|
28327
|
-
});
|
|
28328
|
-
//#endregion
|
|
28329
28240
|
//#region src/plugin/rules/react-native/rn-prefer-content-inset-adjustment.ts
|
|
28330
28241
|
const rnPreferContentInsetAdjustment = defineRetiredRule({
|
|
28331
28242
|
id: "rn-prefer-content-inset-adjustment",
|
|
@@ -32423,7 +32334,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
32423
32334
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
32424
32335
|
let current = node.parent;
|
|
32425
32336
|
while (current) {
|
|
32426
|
-
if (isFunctionLike$
|
|
32337
|
+
if (isFunctionLike$2(current)) {
|
|
32427
32338
|
const resolvedName = inferFunctionName(current);
|
|
32428
32339
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
32429
32340
|
}
|
|
@@ -32444,7 +32355,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
32444
32355
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
32445
32356
|
let current = node.parent;
|
|
32446
32357
|
while (current) {
|
|
32447
|
-
if (isFunctionLike$
|
|
32358
|
+
if (isFunctionLike$2(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
32448
32359
|
current = current.parent ?? null;
|
|
32449
32360
|
}
|
|
32450
32361
|
return false;
|
|
@@ -32782,7 +32693,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
32782
32693
|
let foundAuthCall = false;
|
|
32783
32694
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
32784
32695
|
if (foundAuthCall) return;
|
|
32785
|
-
if (isFunctionLike$
|
|
32696
|
+
if (isFunctionLike$2(child)) return false;
|
|
32786
32697
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32787
32698
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
32788
32699
|
});
|
|
@@ -37396,7 +37307,7 @@ const reactDoctorRules = [
|
|
|
37396
37307
|
rule: {
|
|
37397
37308
|
...rnAnimateLayoutProperty,
|
|
37398
37309
|
framework: "react-native",
|
|
37399
|
-
category: "
|
|
37310
|
+
category: "Bugs",
|
|
37400
37311
|
tags: [...new Set(["react-native", ...rnAnimateLayoutProperty.tags ?? []])]
|
|
37401
37312
|
}
|
|
37402
37313
|
},
|
|
@@ -38261,7 +38172,7 @@ const appendNode = (builder, block, node) => {
|
|
|
38261
38172
|
};
|
|
38262
38173
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
38263
38174
|
builder.nodeBlock.set(node, block);
|
|
38264
|
-
if (isFunctionLike$
|
|
38175
|
+
if (isFunctionLike$2(node)) return;
|
|
38265
38176
|
const record = node;
|
|
38266
38177
|
for (const key of Object.keys(record)) {
|
|
38267
38178
|
if (key === "parent") continue;
|
|
@@ -38599,7 +38510,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38599
38510
|
body: program.body
|
|
38600
38511
|
});
|
|
38601
38512
|
const visit = (node) => {
|
|
38602
|
-
if (isFunctionLike$
|
|
38513
|
+
if (isFunctionLike$2(node)) {
|
|
38603
38514
|
const body = node.body;
|
|
38604
38515
|
if (body) buildFor(node, body);
|
|
38605
38516
|
}
|
|
@@ -38616,7 +38527,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38616
38527
|
const enclosingFunction = (node) => {
|
|
38617
38528
|
let current = node;
|
|
38618
38529
|
while (current) {
|
|
38619
|
-
if (isFunctionLike$
|
|
38530
|
+
if (isFunctionLike$2(current)) return current;
|
|
38620
38531
|
if (isNodeOfType(current, "Program")) return current;
|
|
38621
38532
|
current = current.parent ?? null;
|
|
38622
38533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.2.18-dev.
|
|
3
|
+
"version": "0.2.18-dev.67be206",
|
|
4
4
|
"description": "oxlint plugin for React Doctor: diagnose React codebases for security, performance, correctness, accessibility, bundle-size, and architecture issues",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"accessibility",
|