oxlint-plugin-react-doctor 0.2.11-dev.402c7ea → 0.2.11-dev.d0f5206
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 +34 -0
- package/dist/index.js +368 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -3400,6 +3400,23 @@ declare const REACT_DOCTOR_RULES: readonly [{
|
|
|
3400
3400
|
readonly recommendation?: string;
|
|
3401
3401
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3402
3402
|
};
|
|
3403
|
+
}, {
|
|
3404
|
+
readonly key: "react-doctor/no-self-updating-effect";
|
|
3405
|
+
readonly id: "no-self-updating-effect";
|
|
3406
|
+
readonly source: "react-doctor";
|
|
3407
|
+
readonly originallyExternal: false;
|
|
3408
|
+
readonly rule: {
|
|
3409
|
+
readonly framework: "global";
|
|
3410
|
+
readonly category: "State & Effects";
|
|
3411
|
+
readonly id: string;
|
|
3412
|
+
readonly severity: RuleSeverity;
|
|
3413
|
+
readonly requires?: ReadonlyArray<string>;
|
|
3414
|
+
readonly disabledBy?: ReadonlyArray<string>;
|
|
3415
|
+
readonly tags?: ReadonlyArray<string>;
|
|
3416
|
+
readonly defaultEnabled?: boolean;
|
|
3417
|
+
readonly recommendation?: string;
|
|
3418
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
3419
|
+
};
|
|
3403
3420
|
}, {
|
|
3404
3421
|
readonly key: "react-doctor/no-set-state";
|
|
3405
3422
|
readonly id: "no-set-state";
|
|
@@ -8855,6 +8872,23 @@ declare const RULES: readonly [{
|
|
|
8855
8872
|
readonly recommendation?: string;
|
|
8856
8873
|
readonly create: (context: RuleContext) => RuleVisitors;
|
|
8857
8874
|
};
|
|
8875
|
+
}, {
|
|
8876
|
+
readonly key: "react-doctor/no-self-updating-effect";
|
|
8877
|
+
readonly id: "no-self-updating-effect";
|
|
8878
|
+
readonly source: "react-doctor";
|
|
8879
|
+
readonly originallyExternal: false;
|
|
8880
|
+
readonly rule: {
|
|
8881
|
+
readonly framework: "global";
|
|
8882
|
+
readonly category: "State & Effects";
|
|
8883
|
+
readonly id: string;
|
|
8884
|
+
readonly severity: RuleSeverity;
|
|
8885
|
+
readonly requires?: ReadonlyArray<string>;
|
|
8886
|
+
readonly disabledBy?: ReadonlyArray<string>;
|
|
8887
|
+
readonly tags?: ReadonlyArray<string>;
|
|
8888
|
+
readonly defaultEnabled?: boolean;
|
|
8889
|
+
readonly recommendation?: string;
|
|
8890
|
+
readonly create: (context: RuleContext) => RuleVisitors;
|
|
8891
|
+
};
|
|
8858
8892
|
}, {
|
|
8859
8893
|
readonly key: "react-doctor/no-set-state";
|
|
8860
8894
|
readonly id: "no-set-state";
|
package/dist/index.js
CHANGED
|
@@ -21089,6 +21089,363 @@ const noSecretsInClientCode = defineRule({
|
|
|
21089
21089
|
}
|
|
21090
21090
|
});
|
|
21091
21091
|
//#endregion
|
|
21092
|
+
//#region src/plugin/rules/state-and-effects/no-self-updating-effect.ts
|
|
21093
|
+
const doesConstructFreshReference = (node) => isNodeOfType(node, "ArrayExpression") || isNodeOfType(node, "ObjectExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "Literal") && "regex" in node;
|
|
21094
|
+
const expressionReadsStateValue = (node, stateName) => {
|
|
21095
|
+
if (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")) return false;
|
|
21096
|
+
if (isNodeOfType(node, "Identifier")) return node.name === stateName;
|
|
21097
|
+
if (isNodeOfType(node, "MemberExpression")) {
|
|
21098
|
+
if (expressionReadsStateValue(node.object, stateName)) return true;
|
|
21099
|
+
return node.computed ? expressionReadsStateValue(node.property, stateName) : false;
|
|
21100
|
+
}
|
|
21101
|
+
if (isNodeOfType(node, "Property")) {
|
|
21102
|
+
if (node.computed && expressionReadsStateValue(node.key, stateName)) return true;
|
|
21103
|
+
return expressionReadsStateValue(node.value, stateName);
|
|
21104
|
+
}
|
|
21105
|
+
const nodeRecord = node;
|
|
21106
|
+
for (const childKey of Object.keys(nodeRecord)) {
|
|
21107
|
+
if (childKey === "parent" || childKey === "type") continue;
|
|
21108
|
+
const childValue = nodeRecord[childKey];
|
|
21109
|
+
if (Array.isArray(childValue)) {
|
|
21110
|
+
for (const childArrayItem of childValue) if (isAstNode(childArrayItem) && expressionReadsStateValue(childArrayItem, stateName)) return true;
|
|
21111
|
+
} else if (isAstNode(childValue) && expressionReadsStateValue(childValue, stateName)) return true;
|
|
21112
|
+
}
|
|
21113
|
+
return false;
|
|
21114
|
+
};
|
|
21115
|
+
const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
21116
|
+
const firstArgument = setterCall.arguments?.[0];
|
|
21117
|
+
if (!firstArgument) return false;
|
|
21118
|
+
const argument = stripParenExpression(firstArgument);
|
|
21119
|
+
if (isNodeOfType(argument, "Identifier") && argument.name === stateName) return false;
|
|
21120
|
+
if (isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression")) return true;
|
|
21121
|
+
if (doesConstructFreshReference(argument)) return true;
|
|
21122
|
+
return expressionReadsStateValue(argument, stateName);
|
|
21123
|
+
};
|
|
21124
|
+
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
21125
|
+
const expression = stripParenExpression(isNodeOfType(statement, "ExpressionStatement") ? statement.expression : statement);
|
|
21126
|
+
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
21127
|
+
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
21128
|
+
if (!setterNames.has(expression.callee.name)) return null;
|
|
21129
|
+
return expression;
|
|
21130
|
+
};
|
|
21131
|
+
const collectDependencyStateNames = (depsNode) => {
|
|
21132
|
+
const dependencyNames = /* @__PURE__ */ new Set();
|
|
21133
|
+
if (!isNodeOfType(depsNode, "ArrayExpression")) return dependencyNames;
|
|
21134
|
+
for (const element of depsNode.elements ?? []) if (isNodeOfType(element, "Identifier")) dependencyNames.add(element.name);
|
|
21135
|
+
return dependencyNames;
|
|
21136
|
+
};
|
|
21137
|
+
const isEarlyReturnGuard = (statement) => {
|
|
21138
|
+
if (!isNodeOfType(statement, "IfStatement")) return false;
|
|
21139
|
+
const consequent = statement.consequent;
|
|
21140
|
+
if (isNodeOfType(consequent, "ReturnStatement")) return true;
|
|
21141
|
+
if (isNodeOfType(consequent, "BlockStatement")) return (consequent.body ?? []).some((inner) => isNodeOfType(inner, "ReturnStatement"));
|
|
21142
|
+
return false;
|
|
21143
|
+
};
|
|
21144
|
+
const numericLiteralValue = (node) => {
|
|
21145
|
+
if (isNodeOfType(node, "Literal") && typeof node.value === "number") return node.value;
|
|
21146
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "-" && isNodeOfType(node.argument, "Literal") && typeof node.argument.value === "number") return -node.argument.value;
|
|
21147
|
+
return null;
|
|
21148
|
+
};
|
|
21149
|
+
const isStateLength = (node, stateName) => {
|
|
21150
|
+
const member = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
21151
|
+
return isNodeOfType(member, "MemberExpression") && !member.computed && isNodeOfType(member.property, "Identifier") && member.property.name === "length" && expressionReadsStateValue(member.object, stateName);
|
|
21152
|
+
};
|
|
21153
|
+
const isNullishLiteral = (node) => isNodeOfType(node, "Literal") && node.value === null || isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
21154
|
+
const numericComparisonHolds = (operator, left, right) => {
|
|
21155
|
+
switch (operator) {
|
|
21156
|
+
case "<": return left < right;
|
|
21157
|
+
case "<=": return left <= right;
|
|
21158
|
+
case ">": return left > right;
|
|
21159
|
+
case ">=": return left >= right;
|
|
21160
|
+
case "===":
|
|
21161
|
+
case "==": return left === right;
|
|
21162
|
+
case "!==":
|
|
21163
|
+
case "!=": return left !== right;
|
|
21164
|
+
default: return false;
|
|
21165
|
+
}
|
|
21166
|
+
};
|
|
21167
|
+
const guardExitsWhenStateEmpty = (test, stateName) => {
|
|
21168
|
+
const node = isNodeOfType(test, "ChainExpression") ? test.expression : test;
|
|
21169
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return isStateLength(node.argument, stateName);
|
|
21170
|
+
if (isNodeOfType(node, "LogicalExpression")) {
|
|
21171
|
+
if (node.operator === "||") return guardExitsWhenStateEmpty(node.left, stateName) || guardExitsWhenStateEmpty(node.right, stateName);
|
|
21172
|
+
if (node.operator === "&&") return guardExitsWhenStateEmpty(node.left, stateName) && guardExitsWhenStateEmpty(node.right, stateName);
|
|
21173
|
+
return false;
|
|
21174
|
+
}
|
|
21175
|
+
if (isNodeOfType(node, "BinaryExpression")) {
|
|
21176
|
+
const leftIsLength = isStateLength(node.left, stateName);
|
|
21177
|
+
const rightIsLength = isStateLength(node.right, stateName);
|
|
21178
|
+
if (leftIsLength || rightIsLength) {
|
|
21179
|
+
const other = numericLiteralValue(leftIsLength ? node.right : node.left);
|
|
21180
|
+
if (other === null) return false;
|
|
21181
|
+
return leftIsLength ? numericComparisonHolds(node.operator, 0, other) : numericComparisonHolds(node.operator, other, 0);
|
|
21182
|
+
}
|
|
21183
|
+
if (node.operator === "==" || node.operator === "===") {
|
|
21184
|
+
if (expressionReadsStateValue(node.left, stateName) && isNullishLiteral(node.right)) return true;
|
|
21185
|
+
if (expressionReadsStateValue(node.right, stateName) && isNullishLiteral(node.left)) return true;
|
|
21186
|
+
}
|
|
21187
|
+
return false;
|
|
21188
|
+
}
|
|
21189
|
+
return false;
|
|
21190
|
+
};
|
|
21191
|
+
const isEmptyOrFalsyValue = (node) => {
|
|
21192
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).length === 0;
|
|
21193
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).length === 0;
|
|
21194
|
+
if (isNodeOfType(node, "Literal")) return node.value === null || node.value === "" || node.value === 0 || node.value === false;
|
|
21195
|
+
if (isNodeOfType(node, "Identifier")) return node.name === "undefined";
|
|
21196
|
+
return false;
|
|
21197
|
+
};
|
|
21198
|
+
const functionReturnExpression = (fn) => {
|
|
21199
|
+
if (!isNodeOfType(fn, "ArrowFunctionExpression") && !isNodeOfType(fn, "FunctionExpression")) return null;
|
|
21200
|
+
if (!isNodeOfType(fn.body, "BlockStatement")) return fn.body ? stripParenExpression(fn.body) : null;
|
|
21201
|
+
for (const statement of fn.body.body ?? []) if (isNodeOfType(statement, "ReturnStatement") && statement.argument) return stripParenExpression(statement.argument);
|
|
21202
|
+
return null;
|
|
21203
|
+
};
|
|
21204
|
+
const isLengthReducingUpdater = (node) => {
|
|
21205
|
+
if (!isNodeOfType(node, "ArrowFunctionExpression") && !isNodeOfType(node, "FunctionExpression")) return false;
|
|
21206
|
+
const firstParameter = node.params?.[0];
|
|
21207
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
21208
|
+
const returned = functionReturnExpression(node);
|
|
21209
|
+
if (!returned || !isNodeOfType(returned, "CallExpression")) return false;
|
|
21210
|
+
const callee = returned.callee;
|
|
21211
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return false;
|
|
21212
|
+
if (!isNodeOfType(callee.object, "Identifier") || callee.object.name !== firstParameter.name) return false;
|
|
21213
|
+
if (!isNodeOfType(callee.property, "Identifier") || callee.property.name !== "slice") return false;
|
|
21214
|
+
const sliceStart = numericLiteralValue(returned.arguments?.[0]);
|
|
21215
|
+
return sliceStart !== null && sliceStart >= 1;
|
|
21216
|
+
};
|
|
21217
|
+
const writeProvablyConverges = (setterArgument, stateName, earlyReturnGuardTests) => {
|
|
21218
|
+
if (!isEmptyOrFalsyValue(setterArgument) && !isLengthReducingUpdater(setterArgument)) return false;
|
|
21219
|
+
return earlyReturnGuardTests.some((test) => guardExitsWhenStateEmpty(test, stateName));
|
|
21220
|
+
};
|
|
21221
|
+
const SYMBOLIC_DEPTH_LIMIT = 16;
|
|
21222
|
+
const unwrapChain = (node) => {
|
|
21223
|
+
let current = node;
|
|
21224
|
+
for (;;) {
|
|
21225
|
+
const withoutParens = stripParenExpression(current);
|
|
21226
|
+
if (withoutParens !== current) {
|
|
21227
|
+
current = withoutParens;
|
|
21228
|
+
continue;
|
|
21229
|
+
}
|
|
21230
|
+
if (isNodeOfType(current, "ChainExpression")) {
|
|
21231
|
+
current = current.expression;
|
|
21232
|
+
continue;
|
|
21233
|
+
}
|
|
21234
|
+
return current;
|
|
21235
|
+
}
|
|
21236
|
+
};
|
|
21237
|
+
const isUndefinedValue = (node) => isNodeOfType(node, "Identifier") && node.name === "undefined" || isNodeOfType(node, "Literal") && node.value === null;
|
|
21238
|
+
const literalsEqual = (a, b) => isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal") && a.value === b.value;
|
|
21239
|
+
const resolveValueNode = (node, writes, depth, seen) => {
|
|
21240
|
+
if (depth > SYMBOLIC_DEPTH_LIMIT) return null;
|
|
21241
|
+
const current = unwrapChain(node);
|
|
21242
|
+
if (isNodeOfType(current, "Identifier")) {
|
|
21243
|
+
if (seen.has(current.name)) return null;
|
|
21244
|
+
const written = writes.get(current.name);
|
|
21245
|
+
if (written) return resolveValueNode(written, writes, depth + 1, new Set(seen).add(current.name));
|
|
21246
|
+
return current;
|
|
21247
|
+
}
|
|
21248
|
+
if (isNodeOfType(current, "Literal") || isNodeOfType(current, "ArrayExpression") || isNodeOfType(current, "ObjectExpression")) return current;
|
|
21249
|
+
if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier")) {
|
|
21250
|
+
const objectValue = resolveValueNode(current.object, writes, depth + 1, seen);
|
|
21251
|
+
if (objectValue && isNodeOfType(objectValue, "ObjectExpression")) {
|
|
21252
|
+
const propertyKey = current.property.name;
|
|
21253
|
+
const properties = objectValue.properties ?? [];
|
|
21254
|
+
for (let index = properties.length - 1; index >= 0; index--) {
|
|
21255
|
+
const property = properties[index];
|
|
21256
|
+
if (isNodeOfType(property, "SpreadElement")) return null;
|
|
21257
|
+
if (isNodeOfType(property, "Property") && !property.computed && (isNodeOfType(property.key, "Identifier") && property.key.name === propertyKey || isNodeOfType(property.key, "Literal") && property.key.value === propertyKey)) return resolveValueNode(property.value, writes, depth + 1, seen);
|
|
21258
|
+
}
|
|
21259
|
+
}
|
|
21260
|
+
return null;
|
|
21261
|
+
}
|
|
21262
|
+
return null;
|
|
21263
|
+
};
|
|
21264
|
+
const resolveToNumber = (node, writes, depth, seen) => {
|
|
21265
|
+
const value = resolveValueNode(node, writes, depth, seen);
|
|
21266
|
+
if (value && isNodeOfType(value, "Literal") && typeof value.value === "number") return value.value;
|
|
21267
|
+
const current = unwrapChain(node);
|
|
21268
|
+
if (isNodeOfType(current, "MemberExpression") && !current.computed && isNodeOfType(current.property, "Identifier") && current.property.name === "length") {
|
|
21269
|
+
const objectValue = resolveValueNode(current.object, writes, depth, seen);
|
|
21270
|
+
if (objectValue && isNodeOfType(objectValue, "ArrayExpression")) {
|
|
21271
|
+
const elements = objectValue.elements ?? [];
|
|
21272
|
+
if (!elements.some((element) => element && isNodeOfType(element, "SpreadElement"))) return elements.length;
|
|
21273
|
+
}
|
|
21274
|
+
}
|
|
21275
|
+
return null;
|
|
21276
|
+
};
|
|
21277
|
+
const provablyEqualAfterWrites = (left, right, writes, depth, seen) => {
|
|
21278
|
+
const leftNumber = resolveToNumber(left, writes, depth, seen);
|
|
21279
|
+
const rightNumber = resolveToNumber(right, writes, depth, seen);
|
|
21280
|
+
if (leftNumber !== null && rightNumber !== null) return leftNumber === rightNumber;
|
|
21281
|
+
const a = resolveValueNode(left, writes, depth, seen);
|
|
21282
|
+
const b = resolveValueNode(right, writes, depth, seen);
|
|
21283
|
+
if (!a || !b) return false;
|
|
21284
|
+
if (literalsEqual(a, b)) return true;
|
|
21285
|
+
if (isUndefinedValue(a) && isUndefinedValue(b)) return true;
|
|
21286
|
+
return isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier") && a.name === b.name;
|
|
21287
|
+
};
|
|
21288
|
+
const provablyFalsyAfterWrites = (node, writes, depth, seen) => {
|
|
21289
|
+
const value = resolveValueNode(node, writes, depth, seen);
|
|
21290
|
+
if (value) {
|
|
21291
|
+
if (isUndefinedValue(value)) return true;
|
|
21292
|
+
if (isNodeOfType(value, "Literal")) return value.value === null || value.value === 0 || value.value === false || value.value === "";
|
|
21293
|
+
}
|
|
21294
|
+
return resolveToNumber(node, writes, depth, seen) === 0;
|
|
21295
|
+
};
|
|
21296
|
+
const guardProvenAfterWrites = (test, writes, depth, seen) => {
|
|
21297
|
+
if (depth > SYMBOLIC_DEPTH_LIMIT) return false;
|
|
21298
|
+
const node = unwrapChain(test);
|
|
21299
|
+
if (isNodeOfType(node, "LogicalExpression")) {
|
|
21300
|
+
if (node.operator === "&&") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) && guardProvenAfterWrites(node.right, writes, depth + 1, seen);
|
|
21301
|
+
if (node.operator === "||") return guardProvenAfterWrites(node.left, writes, depth + 1, seen) || guardProvenAfterWrites(node.right, writes, depth + 1, seen);
|
|
21302
|
+
return false;
|
|
21303
|
+
}
|
|
21304
|
+
if (isNodeOfType(node, "UnaryExpression") && node.operator === "!") return provablyFalsyAfterWrites(node.argument, writes, depth + 1, seen);
|
|
21305
|
+
if (isNodeOfType(node, "BinaryExpression")) {
|
|
21306
|
+
if (node.operator === "===" || node.operator === "==") return provablyEqualAfterWrites(node.left, node.right, writes, depth + 1, seen);
|
|
21307
|
+
const leftNumber = resolveToNumber(node.left, writes, depth + 1, seen);
|
|
21308
|
+
const rightNumber = resolveToNumber(node.right, writes, depth + 1, seen);
|
|
21309
|
+
if (leftNumber !== null && rightNumber !== null) return numericComparisonHolds(node.operator, leftNumber, rightNumber);
|
|
21310
|
+
return false;
|
|
21311
|
+
}
|
|
21312
|
+
const value = resolveValueNode(node, writes, depth + 1, seen);
|
|
21313
|
+
if (value) {
|
|
21314
|
+
if (isNodeOfType(value, "ArrayExpression") || isNodeOfType(value, "ObjectExpression")) return true;
|
|
21315
|
+
if (isNodeOfType(value, "Literal")) return Boolean(value.value);
|
|
21316
|
+
}
|
|
21317
|
+
return false;
|
|
21318
|
+
};
|
|
21319
|
+
const collectTopLevelWrites = (statements, setterNameToStateName, setterNames) => {
|
|
21320
|
+
const writes = /* @__PURE__ */ new Map();
|
|
21321
|
+
const setterCallNodes = /* @__PURE__ */ new Set();
|
|
21322
|
+
for (const statement of statements) {
|
|
21323
|
+
const setterCall = getUnconditionalSetterCall(statement, setterNames);
|
|
21324
|
+
if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
|
|
21325
|
+
setterCallNodes.add(setterCall);
|
|
21326
|
+
const stateName = setterNameToStateName.get(setterCall.callee.name);
|
|
21327
|
+
if (!stateName) continue;
|
|
21328
|
+
const argument = setterCall.arguments?.[0];
|
|
21329
|
+
if (!argument) continue;
|
|
21330
|
+
const newValue = isNodeOfType(argument, "ArrowFunctionExpression") || isNodeOfType(argument, "FunctionExpression") ? functionReturnExpression(argument) : stripParenExpression(argument);
|
|
21331
|
+
if (newValue) writes.set(stateName, newValue);
|
|
21332
|
+
}
|
|
21333
|
+
return {
|
|
21334
|
+
writes,
|
|
21335
|
+
setterCallNodes
|
|
21336
|
+
};
|
|
21337
|
+
};
|
|
21338
|
+
const everySetterCall = (root, setterName, inspect) => {
|
|
21339
|
+
let ok = true;
|
|
21340
|
+
const visit = (node) => {
|
|
21341
|
+
if (!ok) return;
|
|
21342
|
+
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && node.callee.name === setterName && !inspect(node)) {
|
|
21343
|
+
ok = false;
|
|
21344
|
+
return;
|
|
21345
|
+
}
|
|
21346
|
+
const record = node;
|
|
21347
|
+
for (const key of Object.keys(record)) {
|
|
21348
|
+
if (key === "parent" || key === "type") continue;
|
|
21349
|
+
const child = record[key];
|
|
21350
|
+
if (Array.isArray(child)) {
|
|
21351
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
21352
|
+
} else if (isAstNode(child)) visit(child);
|
|
21353
|
+
if (!ok) return;
|
|
21354
|
+
}
|
|
21355
|
+
};
|
|
21356
|
+
visit(root);
|
|
21357
|
+
return ok;
|
|
21358
|
+
};
|
|
21359
|
+
const everyWriteToStateDrivesTowardEmpty = (callbackBody, setterName) => everySetterCall(callbackBody, setterName, (call) => {
|
|
21360
|
+
const argument = call.arguments?.[0];
|
|
21361
|
+
if (!argument) return true;
|
|
21362
|
+
const value = stripParenExpression(argument);
|
|
21363
|
+
return isEmptyOrFalsyValue(value) || isLengthReducingUpdater(value);
|
|
21364
|
+
});
|
|
21365
|
+
const everySetterCallIsTopLevel = (callbackBody, setterNames, topLevelSetterCalls) => {
|
|
21366
|
+
let safe = true;
|
|
21367
|
+
const visit = (node) => {
|
|
21368
|
+
if (!safe) return;
|
|
21369
|
+
if (isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && setterNames.has(node.callee.name) && !topLevelSetterCalls.has(node)) {
|
|
21370
|
+
safe = false;
|
|
21371
|
+
return;
|
|
21372
|
+
}
|
|
21373
|
+
const record = node;
|
|
21374
|
+
for (const key of Object.keys(record)) {
|
|
21375
|
+
if (key === "parent" || key === "type") continue;
|
|
21376
|
+
const child = record[key];
|
|
21377
|
+
if (Array.isArray(child)) {
|
|
21378
|
+
for (const item of child) if (isAstNode(item)) visit(item);
|
|
21379
|
+
} else if (isAstNode(child)) visit(child);
|
|
21380
|
+
if (!safe) return;
|
|
21381
|
+
}
|
|
21382
|
+
};
|
|
21383
|
+
visit(callbackBody);
|
|
21384
|
+
return safe;
|
|
21385
|
+
};
|
|
21386
|
+
const noSelfUpdatingEffect = defineRule({
|
|
21387
|
+
id: "no-self-updating-effect",
|
|
21388
|
+
severity: "warn",
|
|
21389
|
+
tags: ["test-noise"],
|
|
21390
|
+
recommendation: "Remove the feedback loop: derive the value during render, move the write into an event handler, or guard the update so it reaches a fixed point. See https://react.dev/learn/you-might-not-need-an-effect",
|
|
21391
|
+
create: (context) => {
|
|
21392
|
+
const checkFunctionScope = (functionBody) => {
|
|
21393
|
+
if (!functionBody || !isNodeOfType(functionBody, "BlockStatement")) return;
|
|
21394
|
+
const useStateBindings = collectUseStateBindings(functionBody);
|
|
21395
|
+
if (useStateBindings.length === 0) return;
|
|
21396
|
+
const setterNameToStateName = /* @__PURE__ */ new Map();
|
|
21397
|
+
for (const binding of useStateBindings) setterNameToStateName.set(binding.setterName, binding.valueName);
|
|
21398
|
+
const setterNames = new Set(setterNameToStateName.keys());
|
|
21399
|
+
for (const statement of functionBody.body ?? []) {
|
|
21400
|
+
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
21401
|
+
const effectCall = statement.expression;
|
|
21402
|
+
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
21403
|
+
if (!isHookCall$1(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
21404
|
+
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
21405
|
+
const dependencyStateNames = collectDependencyStateNames(effectCall.arguments[1]);
|
|
21406
|
+
if (dependencyStateNames.size === 0) continue;
|
|
21407
|
+
const callback = getEffectCallback(effectCall);
|
|
21408
|
+
if (!callback) continue;
|
|
21409
|
+
const callbackStatements = getCallbackStatements(callback);
|
|
21410
|
+
const firstWriteIndex = callbackStatements.findIndex((candidate) => getUnconditionalSetterCall(candidate, setterNames) !== null);
|
|
21411
|
+
const guardCutoff = firstWriteIndex < 0 ? callbackStatements.length : firstWriteIndex;
|
|
21412
|
+
const earlyReturnGuardTests = callbackStatements.slice(0, guardCutoff).filter(isEarlyReturnGuard).map((guard) => guard.test);
|
|
21413
|
+
const { writes: topLevelWrites, setterCallNodes } = collectTopLevelWrites(callbackStatements, setterNameToStateName, setterNames);
|
|
21414
|
+
if (everySetterCallIsTopLevel(callback, setterNames, setterCallNodes) && earlyReturnGuardTests.some((test) => guardProvenAfterWrites(test, topLevelWrites, 0, /* @__PURE__ */ new Set()))) continue;
|
|
21415
|
+
const reportedStateNames = /* @__PURE__ */ new Set();
|
|
21416
|
+
for (const callbackStatement of callbackStatements) {
|
|
21417
|
+
const setterCall = getUnconditionalSetterCall(callbackStatement, setterNames);
|
|
21418
|
+
if (!setterCall || !isNodeOfType(setterCall.callee, "Identifier")) continue;
|
|
21419
|
+
const stateName = setterNameToStateName.get(setterCall.callee.name);
|
|
21420
|
+
if (!stateName || !dependencyStateNames.has(stateName)) continue;
|
|
21421
|
+
if (reportedStateNames.has(stateName)) continue;
|
|
21422
|
+
if (!isNonSettlingSetterArgument(setterCall, stateName)) continue;
|
|
21423
|
+
const firstArgument = setterCall.arguments?.[0];
|
|
21424
|
+
if (firstArgument && writeProvablyConverges(stripParenExpression(firstArgument), stateName, earlyReturnGuardTests) && everyWriteToStateDrivesTowardEmpty(callback, setterCall.callee.name)) continue;
|
|
21425
|
+
reportedStateNames.add(stateName);
|
|
21426
|
+
context.report({
|
|
21427
|
+
node: setterCall,
|
|
21428
|
+
message: `${setterCall.callee.name}() runs unconditionally inside this effect, which depends on \`${stateName}\` — setting the same state the effect reacts to re-runs the effect on every commit and causes a render loop. Derive the value during render, move the write into an event handler, or guard the update so it settles.`
|
|
21429
|
+
});
|
|
21430
|
+
}
|
|
21431
|
+
}
|
|
21432
|
+
};
|
|
21433
|
+
return {
|
|
21434
|
+
FunctionDeclaration(node) {
|
|
21435
|
+
const functionName = node.id?.name;
|
|
21436
|
+
if (!functionName || !isUppercaseName(functionName) && !isReactHookName(functionName)) return;
|
|
21437
|
+
checkFunctionScope(node.body);
|
|
21438
|
+
},
|
|
21439
|
+
VariableDeclarator(node) {
|
|
21440
|
+
const isHookAssignment = isNodeOfType(node.id, "Identifier") && isReactHookName(node.id.name) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
21441
|
+
if (!isComponentAssignment(node) && !isHookAssignment) return;
|
|
21442
|
+
if (!isNodeOfType(node.init, "ArrowFunctionExpression") && !isNodeOfType(node.init, "FunctionExpression")) return;
|
|
21443
|
+
checkFunctionScope(node.init.body);
|
|
21444
|
+
}
|
|
21445
|
+
};
|
|
21446
|
+
}
|
|
21447
|
+
});
|
|
21448
|
+
//#endregion
|
|
21092
21449
|
//#region src/plugin/utils/get-parent-component.ts
|
|
21093
21450
|
const getParentComponent = (node) => {
|
|
21094
21451
|
let ancestor = node.parent;
|
|
@@ -35115,6 +35472,17 @@ const reactDoctorRules = [
|
|
|
35115
35472
|
category: "Security"
|
|
35116
35473
|
}
|
|
35117
35474
|
},
|
|
35475
|
+
{
|
|
35476
|
+
key: "react-doctor/no-self-updating-effect",
|
|
35477
|
+
id: "no-self-updating-effect",
|
|
35478
|
+
source: "react-doctor",
|
|
35479
|
+
originallyExternal: false,
|
|
35480
|
+
rule: {
|
|
35481
|
+
...noSelfUpdatingEffect,
|
|
35482
|
+
framework: "global",
|
|
35483
|
+
category: "State & Effects"
|
|
35484
|
+
}
|
|
35485
|
+
},
|
|
35118
35486
|
{
|
|
35119
35487
|
key: "react-doctor/no-set-state",
|
|
35120
35488
|
id: "no-set-state",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oxlint-plugin-react-doctor",
|
|
3
|
-
"version": "0.2.11-dev.
|
|
3
|
+
"version": "0.2.11-dev.d0f5206",
|
|
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",
|