oxlint-plugin-react-doctor 0.2.18-dev.4dc48d7 → 0.2.18-dev.6851a78
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 +172 -57
- 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: "Performance";
|
|
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: "Performance";
|
|
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$3 = (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$3(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$3(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$3(node)) return;
|
|
3064
3064
|
if (!node.async) return;
|
|
3065
3065
|
if (!isNodeOfType(node.body, "BlockStatement")) return;
|
|
3066
3066
|
inspectAllStatementBlocks(node.body);
|
|
@@ -4509,7 +4509,7 @@ const displayName = defineRule({
|
|
|
4509
4509
|
//#region src/plugin/utils/walk-inside-statement-blocks.ts
|
|
4510
4510
|
const walkInsideStatementBlocks = (node, visitor) => {
|
|
4511
4511
|
if (!node || typeof node !== "object") return;
|
|
4512
|
-
if (isFunctionLike$
|
|
4512
|
+
if (isFunctionLike$3(node)) return;
|
|
4513
4513
|
visitor(node);
|
|
4514
4514
|
const nodeRecord = node;
|
|
4515
4515
|
for (const key of Object.keys(nodeRecord)) {
|
|
@@ -4597,7 +4597,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4597
4597
|
let didFindRelease = false;
|
|
4598
4598
|
walkAst(node, (child) => {
|
|
4599
4599
|
if (didFindRelease) return false;
|
|
4600
|
-
if (child !== node && isFunctionLike$
|
|
4600
|
+
if (child !== node && isFunctionLike$3(child) && !isIteratorCallbackArgument(child)) return false;
|
|
4601
4601
|
if (isReleaseLikeCall(child, knownCleanupFunctionNames, knownBoundSubscriptionNames)) {
|
|
4602
4602
|
didFindRelease = true;
|
|
4603
4603
|
return false;
|
|
@@ -4606,7 +4606,7 @@ const containsReleaseLikeCall = (node, knownCleanupFunctionNames, knownBoundSubs
|
|
|
4606
4606
|
return didFindRelease;
|
|
4607
4607
|
};
|
|
4608
4608
|
const isCleanupFunctionLike = (node, knownCleanupFunctionNames, knownBoundSubscriptionNames) => {
|
|
4609
|
-
if (!isFunctionLike$
|
|
4609
|
+
if (!isFunctionLike$3(node)) return false;
|
|
4610
4610
|
return containsReleaseLikeCall(node.body, knownCleanupFunctionNames, knownBoundSubscriptionNames);
|
|
4611
4611
|
};
|
|
4612
4612
|
const isCleanupReturn = (returnedValue, knownCleanupFunctionNames, knownBoundSubscriptionNames, options = {}) => {
|
|
@@ -4870,7 +4870,7 @@ const recordReference = (state, identifier, flag) => {
|
|
|
4870
4870
|
};
|
|
4871
4871
|
const isFunctionBodyBlock = (block) => {
|
|
4872
4872
|
if (!block.parent) return false;
|
|
4873
|
-
return isFunctionLike$
|
|
4873
|
+
return isFunctionLike$3(block.parent);
|
|
4874
4874
|
};
|
|
4875
4875
|
const isCatchClauseBlock = (block) => block.parent !== null && block.parent !== void 0 && block.parent.type === "CatchClause";
|
|
4876
4876
|
const handleVariableDeclaration = (declaration, state) => {
|
|
@@ -5025,7 +5025,7 @@ const walkParameterReferences = (pattern, state) => {
|
|
|
5025
5025
|
if (isNodeOfType(pattern, "RestElement")) walkParameterReferences(pattern.argument, state);
|
|
5026
5026
|
};
|
|
5027
5027
|
const walk = (node, state) => {
|
|
5028
|
-
if (isFunctionLike$
|
|
5028
|
+
if (isFunctionLike$3(node)) {
|
|
5029
5029
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
5030
5030
|
setNodeScope(node, state);
|
|
5031
5031
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
@@ -5271,7 +5271,7 @@ const closureCaptures = (functionNode, scopes) => {
|
|
|
5271
5271
|
const out = [];
|
|
5272
5272
|
const seen = /* @__PURE__ */ new Set();
|
|
5273
5273
|
const visit = (node) => {
|
|
5274
|
-
if (node !== functionNode && isFunctionLike$
|
|
5274
|
+
if (node !== functionNode && isFunctionLike$3(node)) {
|
|
5275
5275
|
const innerCaptures = closureCaptures(node, scopes);
|
|
5276
5276
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
5277
5277
|
if (!seen.has(reference.id)) {
|
|
@@ -7391,7 +7391,7 @@ const isAtomFromJotai = (callExpression) => {
|
|
|
7391
7391
|
if (!isImportedFromModule(callExpression, localName, "jotai")) return false;
|
|
7392
7392
|
return getImportedNameFromModule(callExpression, localName, "jotai") === "atom";
|
|
7393
7393
|
};
|
|
7394
|
-
const isFunctionLike$
|
|
7394
|
+
const isFunctionLike$2 = (node) => Boolean(node && (isNodeOfType(node, "ArrowFunctionExpression") || isNodeOfType(node, "FunctionExpression")));
|
|
7395
7395
|
const getFirstParameterName = (fn) => {
|
|
7396
7396
|
const parameters = fn.params ?? [];
|
|
7397
7397
|
if (parameters.length !== 1) return null;
|
|
@@ -7510,7 +7510,7 @@ const jotaiDerivedAtomReturnsFreshObject = defineRule({
|
|
|
7510
7510
|
const args = node.arguments ?? [];
|
|
7511
7511
|
if (args.length === 0) return;
|
|
7512
7512
|
const reader = args[0];
|
|
7513
|
-
if (!isFunctionLike$
|
|
7513
|
+
if (!isFunctionLike$2(reader)) return;
|
|
7514
7514
|
const getParameterName = getFirstParameterName(reader);
|
|
7515
7515
|
if (!getParameterName) return;
|
|
7516
7516
|
const freshReturn = getFreshReturnForFunction(reader);
|
|
@@ -7766,7 +7766,7 @@ const isInsideLoopContext = (node) => {
|
|
|
7766
7766
|
let current = node.parent;
|
|
7767
7767
|
while (current) {
|
|
7768
7768
|
if (isNodeOfType(current, "ForStatement") || isNodeOfType(current, "ForInStatement") || isNodeOfType(current, "ForOfStatement") || isNodeOfType(current, "WhileStatement") || isNodeOfType(current, "DoWhileStatement")) return true;
|
|
7769
|
-
if (isFunctionLike$
|
|
7769
|
+
if (isFunctionLike$3(current)) {
|
|
7770
7770
|
if (isIteratorCallback(current)) return true;
|
|
7771
7771
|
return false;
|
|
7772
7772
|
}
|
|
@@ -8127,7 +8127,7 @@ const jsHoistIntl = defineRule({
|
|
|
8127
8127
|
let cursor = node.parent ?? null;
|
|
8128
8128
|
let inFunctionBody = false;
|
|
8129
8129
|
while (cursor) {
|
|
8130
|
-
if (isFunctionLike$
|
|
8130
|
+
if (isFunctionLike$3(cursor)) {
|
|
8131
8131
|
inFunctionBody = true;
|
|
8132
8132
|
const fnParent = cursor.parent;
|
|
8133
8133
|
if (fnParent && isNodeOfType(fnParent, "CallExpression") && fnParent.arguments?.[0] === cursor) {
|
|
@@ -13082,7 +13082,7 @@ const collectChainedGetHandlerBodies = (initNode) => {
|
|
|
13082
13082
|
};
|
|
13083
13083
|
const resolveBodiesFromExpression = (expression, resolveBinding, remainingDepth) => {
|
|
13084
13084
|
if (remainingDepth <= 0) return [];
|
|
13085
|
-
if (isFunctionLike$
|
|
13085
|
+
if (isFunctionLike$3(expression)) return expression.body ? [expression.body] : [];
|
|
13086
13086
|
if (isNodeOfType(expression, "CallExpression")) {
|
|
13087
13087
|
for (const callArgument of expression.arguments ?? []) {
|
|
13088
13088
|
if (isNodeOfType(callArgument, "ArrowFunctionExpression") || isNodeOfType(callArgument, "FunctionExpression")) {
|
|
@@ -13552,7 +13552,7 @@ const getEffectFn = (analysis, node) => {
|
|
|
13552
13552
|
if (isNodeOfType(fn, "ArrowFunctionExpression") || isNodeOfType(fn, "FunctionExpression")) return fn;
|
|
13553
13553
|
if (isNodeOfType(fn, "Identifier")) {
|
|
13554
13554
|
const definitionNode = getRef(analysis, fn)?.resolved?.defs[0]?.node;
|
|
13555
|
-
if (definitionNode && isFunctionLike$
|
|
13555
|
+
if (definitionNode && isFunctionLike$3(definitionNode)) return definitionNode;
|
|
13556
13556
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13557
13557
|
const initializer = definitionNode.init;
|
|
13558
13558
|
if (isNodeOfType(initializer, "ArrowFunctionExpression") || isNodeOfType(initializer, "FunctionExpression")) return initializer;
|
|
@@ -13645,14 +13645,14 @@ const getUseStateDecl = (analysis, ref) => {
|
|
|
13645
13645
|
return node ?? null;
|
|
13646
13646
|
};
|
|
13647
13647
|
const isCleanupReturnArgument = (analysis, node) => {
|
|
13648
|
-
if (isFunctionLike$
|
|
13648
|
+
if (isFunctionLike$3(node)) return true;
|
|
13649
13649
|
if (isNodeOfType(node, "MemberExpression")) return true;
|
|
13650
13650
|
if (isNodeOfType(node, "Identifier")) {
|
|
13651
13651
|
const definitionNode = getRef(analysis, node)?.resolved?.defs[0]?.node;
|
|
13652
|
-
if (definitionNode && isFunctionLike$
|
|
13652
|
+
if (definitionNode && isFunctionLike$3(definitionNode)) return true;
|
|
13653
13653
|
if (definitionNode && isNodeOfType(definitionNode, "VariableDeclarator")) {
|
|
13654
13654
|
const initializer = definitionNode.init;
|
|
13655
|
-
return isFunctionLike$
|
|
13655
|
+
return isFunctionLike$3(initializer);
|
|
13656
13656
|
}
|
|
13657
13657
|
}
|
|
13658
13658
|
if (isNodeOfType(node, "ConditionalExpression")) return isCleanupReturnArgument(analysis, node.consequent) || isCleanupReturnArgument(analysis, node.alternate);
|
|
@@ -13662,7 +13662,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13662
13662
|
if (visited.has(node)) return false;
|
|
13663
13663
|
visited.add(node);
|
|
13664
13664
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
13665
|
-
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$
|
|
13665
|
+
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$3(node)) return false;
|
|
13666
13666
|
const record = node;
|
|
13667
13667
|
for (const [key, value] of Object.entries(record)) {
|
|
13668
13668
|
if (key === "parent") continue;
|
|
@@ -13674,7 +13674,7 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
13674
13674
|
};
|
|
13675
13675
|
const hasCleanup = (analysis, node) => {
|
|
13676
13676
|
const fn = getEffectFn(analysis, node);
|
|
13677
|
-
if (!isFunctionLike$
|
|
13677
|
+
if (!isFunctionLike$3(fn)) return false;
|
|
13678
13678
|
if (!isNodeOfType(fn.body, "BlockStatement")) return false;
|
|
13679
13679
|
return hasCleanupReturn(analysis, fn.body);
|
|
13680
13680
|
};
|
|
@@ -14024,7 +14024,7 @@ const isInsideStaticPlaceholderMap = (node) => {
|
|
|
14024
14024
|
let current = node;
|
|
14025
14025
|
while (current.parent) {
|
|
14026
14026
|
const parent = current.parent;
|
|
14027
|
-
if (isFunctionLike$
|
|
14027
|
+
if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
14028
14028
|
const callee = parent.callee;
|
|
14029
14029
|
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
14030
|
if (isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current) return isArrayFromLengthObjectCall(parent);
|
|
@@ -14043,7 +14043,7 @@ const findIteratorItemName$1 = (node) => {
|
|
|
14043
14043
|
let current = node;
|
|
14044
14044
|
while (current.parent) {
|
|
14045
14045
|
const parent = current.parent;
|
|
14046
|
-
if (isFunctionLike$
|
|
14046
|
+
if (isFunctionLike$3(current) && isNodeOfType(parent, "CallExpression") && parent.arguments.includes(current)) {
|
|
14047
14047
|
const callee = parent.callee;
|
|
14048
14048
|
const isIteratorMethodCall = isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") && (callee.property.name === "map" || callee.property.name === "flatMap" || callee.property.name === "forEach");
|
|
14049
14049
|
const isArrayFromCallback = isArrayFromCall(parent) && parent.arguments.length >= 2 && parent.arguments[1] === current;
|
|
@@ -14988,7 +14988,7 @@ const componentOrHookDisplayNameForFunction = (functionNode) => {
|
|
|
14988
14988
|
const nearestEnclosingFunction = (node) => {
|
|
14989
14989
|
let cursor = node.parent;
|
|
14990
14990
|
while (cursor) {
|
|
14991
|
-
if (isFunctionLike$
|
|
14991
|
+
if (isFunctionLike$3(cursor)) return cursor;
|
|
14992
14992
|
cursor = cursor.parent ?? null;
|
|
14993
14993
|
}
|
|
14994
14994
|
return null;
|
|
@@ -15671,7 +15671,7 @@ const extractDestructuredPropNames = (params) => {
|
|
|
15671
15671
|
};
|
|
15672
15672
|
const getInlineFunctionNode = (node) => {
|
|
15673
15673
|
if (!node) return null;
|
|
15674
|
-
if (isFunctionLike$
|
|
15674
|
+
if (isFunctionLike$3(node)) return node;
|
|
15675
15675
|
if (!isNodeOfType(node, "CallExpression")) return null;
|
|
15676
15676
|
for (const argument of node.arguments ?? []) {
|
|
15677
15677
|
const inlineFunctionNode = getInlineFunctionNode(argument);
|
|
@@ -15682,7 +15682,7 @@ const getInlineFunctionNode = (node) => {
|
|
|
15682
15682
|
const getNearestComponentFunction = (node) => {
|
|
15683
15683
|
let cursor = node.parent ?? null;
|
|
15684
15684
|
while (cursor) {
|
|
15685
|
-
if (isFunctionLike$
|
|
15685
|
+
if (isFunctionLike$3(cursor)) return cursor;
|
|
15686
15686
|
cursor = cursor.parent ?? null;
|
|
15687
15687
|
}
|
|
15688
15688
|
return null;
|
|
@@ -16012,7 +16012,7 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
16012
16012
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit) => {
|
|
16013
16013
|
if (!node || typeof node !== "object") return;
|
|
16014
16014
|
let nextShadowedStateNames = shadowedStateNames;
|
|
16015
|
-
if (isFunctionLike$
|
|
16015
|
+
if (isFunctionLike$3(node)) {
|
|
16016
16016
|
const localBindings = collectFunctionLocalBindings(node);
|
|
16017
16017
|
if (localBindings.size > 0) {
|
|
16018
16018
|
const merged = new Set(shadowedStateNames);
|
|
@@ -18269,7 +18269,7 @@ const isInsideClassBody = (node) => {
|
|
|
18269
18269
|
let current = node.parent;
|
|
18270
18270
|
while (current) {
|
|
18271
18271
|
if (isNodeOfType(current, "ClassBody")) return true;
|
|
18272
|
-
if (isFunctionLike$
|
|
18272
|
+
if (isFunctionLike$3(current)) return false;
|
|
18273
18273
|
current = current.parent;
|
|
18274
18274
|
}
|
|
18275
18275
|
return false;
|
|
@@ -18973,7 +18973,7 @@ const isLodashMutatorCall = (callExpression) => {
|
|
|
18973
18973
|
};
|
|
18974
18974
|
//#endregion
|
|
18975
18975
|
//#region src/plugin/utils/find-exported-function-body.ts
|
|
18976
|
-
const isFunctionLike = (node) => {
|
|
18976
|
+
const isFunctionLike$1 = (node) => {
|
|
18977
18977
|
if (!node) return false;
|
|
18978
18978
|
return isNodeOfType(node, "FunctionDeclaration") || isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "ArrowFunctionExpression");
|
|
18979
18979
|
};
|
|
@@ -18988,7 +18988,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
18988
18988
|
if (!isNodeOfType(declarator, "VariableDeclarator")) continue;
|
|
18989
18989
|
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
18990
18990
|
const initializer = declarator.init ? stripParenExpression(declarator.init) : null;
|
|
18991
|
-
if (initializer && isFunctionLike(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18991
|
+
if (initializer && isFunctionLike$1(initializer)) localBindings.set(declarator.id.name, initializer);
|
|
18992
18992
|
}
|
|
18993
18993
|
};
|
|
18994
18994
|
for (const statement of programRoot.body ?? []) {
|
|
@@ -19032,7 +19032,7 @@ const findExportedFunctionBody = (programRoot, exportedName) => {
|
|
|
19032
19032
|
defaultExport = declaration;
|
|
19033
19033
|
continue;
|
|
19034
19034
|
}
|
|
19035
|
-
if (isFunctionLike(declaration)) {
|
|
19035
|
+
if (isFunctionLike$1(declaration)) {
|
|
19036
19036
|
defaultExport = declaration;
|
|
19037
19037
|
continue;
|
|
19038
19038
|
}
|
|
@@ -19193,7 +19193,7 @@ const resolveCrossFileFunctionExport = (fromFilename, source, exportedName) => {
|
|
|
19193
19193
|
const resolveReducerFunction = (node, currentFilename) => {
|
|
19194
19194
|
if (!node) return null;
|
|
19195
19195
|
const unwrappedNode = stripParenExpression(node);
|
|
19196
|
-
if (isFunctionLike$
|
|
19196
|
+
if (isFunctionLike$3(unwrappedNode)) return {
|
|
19197
19197
|
functionNode: unwrappedNode,
|
|
19198
19198
|
crossFileSourceDisplay: null
|
|
19199
19199
|
};
|
|
@@ -19201,7 +19201,7 @@ const resolveReducerFunction = (node, currentFilename) => {
|
|
|
19201
19201
|
const initializer = findVariableInitializer(unwrappedNode, unwrappedNode.name)?.initializer;
|
|
19202
19202
|
if (!initializer) return null;
|
|
19203
19203
|
const unwrappedInitializer = stripParenExpression(initializer);
|
|
19204
|
-
if (isFunctionLike$
|
|
19204
|
+
if (isFunctionLike$3(unwrappedInitializer)) return {
|
|
19205
19205
|
functionNode: unwrappedInitializer,
|
|
19206
19206
|
crossFileSourceDisplay: null
|
|
19207
19207
|
};
|
|
@@ -19315,11 +19315,11 @@ const canExpressionReturnOriginalReducerStateReference = (node, state) => {
|
|
|
19315
19315
|
return false;
|
|
19316
19316
|
};
|
|
19317
19317
|
const collectReducerStateMutationsInExpressionOrStatement = (node, state) => {
|
|
19318
|
-
if (isFunctionLike$
|
|
19318
|
+
if (isFunctionLike$3(node)) return [];
|
|
19319
19319
|
const mutations = [];
|
|
19320
19320
|
walkAst(node, (child) => {
|
|
19321
19321
|
const unwrappedChild = stripParenExpression(child);
|
|
19322
|
-
if (child !== node && isFunctionLike$
|
|
19322
|
+
if (child !== node && isFunctionLike$3(unwrappedChild)) return false;
|
|
19323
19323
|
if (isNodeOfType(unwrappedChild, "AssignmentExpression")) {
|
|
19324
19324
|
if (isNodeOfType(stripParenExpression(unwrappedChild.left), "MemberExpression") && isExpressionRootedInMutableReducerStateSource(unwrappedChild.left, state)) mutations.push({ node: unwrappedChild });
|
|
19325
19325
|
return;
|
|
@@ -19424,7 +19424,7 @@ const updateReducerStateIdentityForIdentifierAssignment = (assignment, state) =>
|
|
|
19424
19424
|
if (isExpressionReachableFromOriginalReducerState(assignment.right, state)) state.mutableStateSourceNames.add(name);
|
|
19425
19425
|
};
|
|
19426
19426
|
const analyzeReactUseReducerFunctionForStateMutation = (context, functionNode, reportedNodes, options) => {
|
|
19427
|
-
if (!isFunctionLike$
|
|
19427
|
+
if (!isFunctionLike$3(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return;
|
|
19428
19428
|
const firstParam = functionNode.params?.[0];
|
|
19429
19429
|
const stateName = isNodeOfType(firstParam, "Identifier") ? firstParam.name : isNodeOfType(firstParam, "AssignmentPattern") && isNodeOfType(firstParam.left, "Identifier") ? firstParam.left.name : null;
|
|
19430
19430
|
if (!stateName) return;
|
|
@@ -21273,7 +21273,7 @@ const isTanStackServerFnHandler = (node) => {
|
|
|
21273
21273
|
const isInsideServerOnlyScope = (node) => {
|
|
21274
21274
|
let currentNode = node.parent ?? null;
|
|
21275
21275
|
while (currentNode) {
|
|
21276
|
-
if (isFunctionLike$
|
|
21276
|
+
if (isFunctionLike$3(currentNode)) {
|
|
21277
21277
|
if (hasUseServerDirective(currentNode) || isTanStackServerFnHandler(currentNode)) return true;
|
|
21278
21278
|
}
|
|
21279
21279
|
currentNode = currentNode.parent ?? null;
|
|
@@ -23428,7 +23428,7 @@ const isReactClassComponent = (classNode) => {
|
|
|
23428
23428
|
const findEnclosingComponent = (node) => {
|
|
23429
23429
|
let walker = node.parent;
|
|
23430
23430
|
while (walker) {
|
|
23431
|
-
if (isFunctionLike$
|
|
23431
|
+
if (isFunctionLike$3(walker)) {
|
|
23432
23432
|
const componentName = inferFunctionLikeName(walker);
|
|
23433
23433
|
if (componentName && isReactComponentName(componentName) && expressionContainsJsxOrCreateElement(walker)) return {
|
|
23434
23434
|
component: walker,
|
|
@@ -23680,6 +23680,35 @@ const noUsememoSimpleExpression = defineRule({
|
|
|
23680
23680
|
});
|
|
23681
23681
|
//#endregion
|
|
23682
23682
|
//#region src/plugin/rules/design/no-wide-letter-spacing.ts
|
|
23683
|
+
const getJsxAttributeStringValue = (attributeValue) => {
|
|
23684
|
+
if (!attributeValue) return null;
|
|
23685
|
+
if (isNodeOfType(attributeValue, "Literal") && typeof attributeValue.value === "string") return attributeValue.value;
|
|
23686
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23687
|
+
const expression = attributeValue.expression;
|
|
23688
|
+
if (isNodeOfType(expression, "Literal") && typeof expression.value === "string") return expression.value;
|
|
23689
|
+
}
|
|
23690
|
+
return null;
|
|
23691
|
+
};
|
|
23692
|
+
const isTruthyBooleanJsxAttribute = (attributeValue) => {
|
|
23693
|
+
if (attributeValue === null) return true;
|
|
23694
|
+
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
23695
|
+
const expression = attributeValue.expression;
|
|
23696
|
+
return isNodeOfType(expression, "Literal") && expression.value === true;
|
|
23697
|
+
}
|
|
23698
|
+
return false;
|
|
23699
|
+
};
|
|
23700
|
+
const hasUppercaseSiblingProp = (styleAttribute) => {
|
|
23701
|
+
const openingElement = styleAttribute.parent;
|
|
23702
|
+
if (!openingElement || !isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
23703
|
+
for (const attribute of openingElement.attributes ?? []) {
|
|
23704
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
23705
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
23706
|
+
const attributeName = attribute.name.name;
|
|
23707
|
+
if (attributeName === "uppercase" && isTruthyBooleanJsxAttribute(attribute.value)) return true;
|
|
23708
|
+
if (attributeName === "textTransform" && getJsxAttributeStringValue(attribute.value) === "uppercase") return true;
|
|
23709
|
+
}
|
|
23710
|
+
return false;
|
|
23711
|
+
};
|
|
23683
23712
|
const noWideLetterSpacing = defineRule({
|
|
23684
23713
|
id: "no-wide-letter-spacing",
|
|
23685
23714
|
title: "Wide letter spacing on body text",
|
|
@@ -23689,6 +23718,7 @@ const noWideLetterSpacing = defineRule({
|
|
|
23689
23718
|
create: (context) => ({ JSXAttribute(node) {
|
|
23690
23719
|
const expression = getInlineStyleExpression(node);
|
|
23691
23720
|
if (!expression) return;
|
|
23721
|
+
if (hasUppercaseSiblingProp(node)) return;
|
|
23692
23722
|
let isUppercase = false;
|
|
23693
23723
|
let letterSpacingProperty = null;
|
|
23694
23724
|
let letterSpacingEm = null;
|
|
@@ -24851,6 +24881,7 @@ const preferModuleScopeStaticValue = defineRule({
|
|
|
24851
24881
|
tags: ["test-noise"],
|
|
24852
24882
|
severity: "warn",
|
|
24853
24883
|
category: "Architecture",
|
|
24884
|
+
disabledBy: ["react-compiler"],
|
|
24854
24885
|
recommendation: "Move the value above the component, at the top of the file. It doesn't use local state, so rebuilding it each update is wasted and makes it look new every time.",
|
|
24855
24886
|
create: (context) => ({ VariableDeclarator(node) {
|
|
24856
24887
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
@@ -25663,7 +25694,7 @@ const reduxUseselectorInlineDerivation = defineRule({
|
|
|
25663
25694
|
if (!body) return;
|
|
25664
25695
|
const returnedExpressions = [];
|
|
25665
25696
|
if (isNodeOfType(body, "BlockStatement")) walkAst(body, (node) => {
|
|
25666
|
-
if (node !== body && isFunctionLike$
|
|
25697
|
+
if (node !== body && isFunctionLike$3(node)) return false;
|
|
25667
25698
|
if (isNodeOfType(node, "ReturnStatement")) {
|
|
25668
25699
|
if (node.argument) returnedExpressions.push(node.argument);
|
|
25669
25700
|
return false;
|
|
@@ -26006,7 +26037,7 @@ const hasOwnAwait = (functionBody) => {
|
|
|
26006
26037
|
let found = false;
|
|
26007
26038
|
walkAst(functionBody, (child) => {
|
|
26008
26039
|
if (found) return;
|
|
26009
|
-
if (child !== functionBody && isFunctionLike$
|
|
26040
|
+
if (child !== functionBody && isFunctionLike$3(child)) return false;
|
|
26010
26041
|
if (isNodeOfType(child, "AwaitExpression")) found = true;
|
|
26011
26042
|
});
|
|
26012
26043
|
return found;
|
|
@@ -26025,7 +26056,7 @@ const setterIsCalledInAsyncContext = (componentBody, setterName) => {
|
|
|
26025
26056
|
let found = false;
|
|
26026
26057
|
walkAst(componentBody, (child) => {
|
|
26027
26058
|
if (found) return;
|
|
26028
|
-
if (!isFunctionLike$
|
|
26059
|
+
if (!isFunctionLike$3(child)) return;
|
|
26029
26060
|
const functionBody = child.body;
|
|
26030
26061
|
if (!(Boolean(child.async) || hasOwnAwait(functionBody))) return;
|
|
26031
26062
|
if (callsIdentifier(functionBody, setterName)) found = true;
|
|
@@ -26808,7 +26839,8 @@ const rerenderTransitionsScroll = defineRule({
|
|
|
26808
26839
|
});
|
|
26809
26840
|
//#endregion
|
|
26810
26841
|
//#region src/plugin/rules/react-native/rn-animate-layout-property.ts
|
|
26811
|
-
const
|
|
26842
|
+
const REANIMATED_MODULE = "react-native-reanimated";
|
|
26843
|
+
const REANIMATED_LAYOUT_STYLE_PROPERTIES = new Set([
|
|
26812
26844
|
"width",
|
|
26813
26845
|
"height",
|
|
26814
26846
|
"top",
|
|
@@ -26846,12 +26878,91 @@ const REANIMATED_LAYOUT_KEYS = new Set([
|
|
|
26846
26878
|
"lineHeight",
|
|
26847
26879
|
"letterSpacing"
|
|
26848
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
|
+
};
|
|
26849
26950
|
const findReturnedObject = (callback) => {
|
|
26850
26951
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return null;
|
|
26851
|
-
const body = callback.body;
|
|
26952
|
+
const body = stripParenExpression(callback.body);
|
|
26852
26953
|
if (isNodeOfType(body, "ObjectExpression")) return body;
|
|
26853
26954
|
if (!isNodeOfType(body, "BlockStatement")) return null;
|
|
26854
|
-
for (const
|
|
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;
|
|
26855
26966
|
return null;
|
|
26856
26967
|
};
|
|
26857
26968
|
const rnAnimateLayoutProperty = defineRule({
|
|
@@ -26859,21 +26970,25 @@ const rnAnimateLayoutProperty = defineRule({
|
|
|
26859
26970
|
title: "Animating a layout property",
|
|
26860
26971
|
tags: ["test-noise"],
|
|
26861
26972
|
requires: ["react-native"],
|
|
26862
|
-
severity: "
|
|
26863
|
-
|
|
26973
|
+
severity: "warn",
|
|
26974
|
+
category: "Performance",
|
|
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.",
|
|
26864
26976
|
create: (context) => ({ CallExpression(node) {
|
|
26865
|
-
if (!
|
|
26977
|
+
if (!isUseAnimatedStyleCallee(node.callee, context)) return;
|
|
26866
26978
|
const callback = node.arguments?.[0];
|
|
26867
26979
|
if (!callback) return;
|
|
26868
26980
|
const returnedObject = findReturnedObject(callback);
|
|
26869
26981
|
if (!returnedObject) return;
|
|
26870
26982
|
for (const property of returnedObject.properties ?? []) {
|
|
26871
26983
|
if (!isNodeOfType(property, "Property")) continue;
|
|
26872
|
-
|
|
26873
|
-
if (
|
|
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;
|
|
26874
26989
|
context.report({
|
|
26875
26990
|
node: property,
|
|
26876
|
-
message: `
|
|
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.`
|
|
26877
26992
|
});
|
|
26878
26993
|
}
|
|
26879
26994
|
} })
|
|
@@ -32308,7 +32423,7 @@ const isUseEffectEventSymbol = (symbol) => {
|
|
|
32308
32423
|
const findEnclosingComponentOrHookFunction = (node) => {
|
|
32309
32424
|
let current = node.parent;
|
|
32310
32425
|
while (current) {
|
|
32311
|
-
if (isFunctionLike$
|
|
32426
|
+
if (isFunctionLike$3(current)) {
|
|
32312
32427
|
const resolvedName = inferFunctionName(current);
|
|
32313
32428
|
if (resolvedName !== null && isReactComponentOrHookName(resolvedName)) return current;
|
|
32314
32429
|
}
|
|
@@ -32329,7 +32444,7 @@ const isCallbackArgumentForAllowedEffectEventHook = (functionNode, additionalEff
|
|
|
32329
32444
|
const isInsideAllowedEffectEventCallback = (node, additionalEffectHooksRegex) => {
|
|
32330
32445
|
let current = node.parent;
|
|
32331
32446
|
while (current) {
|
|
32332
|
-
if (isFunctionLike$
|
|
32447
|
+
if (isFunctionLike$3(current) && isCallbackArgumentForAllowedEffectEventHook(current, additionalEffectHooksRegex)) return true;
|
|
32333
32448
|
current = current.parent ?? null;
|
|
32334
32449
|
}
|
|
32335
32450
|
return false;
|
|
@@ -32667,7 +32782,7 @@ const containsAuthCheck = (rootNodes, allowedFunctionNames, genericMethodNames)
|
|
|
32667
32782
|
let foundAuthCall = false;
|
|
32668
32783
|
for (const rootNode of rootNodes) walkAst(rootNode, (child) => {
|
|
32669
32784
|
if (foundAuthCall) return;
|
|
32670
|
-
if (isFunctionLike$
|
|
32785
|
+
if (isFunctionLike$3(child)) return false;
|
|
32671
32786
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
32672
32787
|
if (getAuthCallName(child, allowedFunctionNames, genericMethodNames)) foundAuthCall = true;
|
|
32673
32788
|
});
|
|
@@ -37281,7 +37396,7 @@ const reactDoctorRules = [
|
|
|
37281
37396
|
rule: {
|
|
37282
37397
|
...rnAnimateLayoutProperty,
|
|
37283
37398
|
framework: "react-native",
|
|
37284
|
-
category: "
|
|
37399
|
+
category: "Performance",
|
|
37285
37400
|
tags: [...new Set(["react-native", ...rnAnimateLayoutProperty.tags ?? []])]
|
|
37286
37401
|
}
|
|
37287
37402
|
},
|
|
@@ -38146,7 +38261,7 @@ const appendNode = (builder, block, node) => {
|
|
|
38146
38261
|
};
|
|
38147
38262
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
38148
38263
|
builder.nodeBlock.set(node, block);
|
|
38149
|
-
if (isFunctionLike$
|
|
38264
|
+
if (isFunctionLike$3(node)) return;
|
|
38150
38265
|
const record = node;
|
|
38151
38266
|
for (const key of Object.keys(record)) {
|
|
38152
38267
|
if (key === "parent") continue;
|
|
@@ -38484,7 +38599,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38484
38599
|
body: program.body
|
|
38485
38600
|
});
|
|
38486
38601
|
const visit = (node) => {
|
|
38487
|
-
if (isFunctionLike$
|
|
38602
|
+
if (isFunctionLike$3(node)) {
|
|
38488
38603
|
const body = node.body;
|
|
38489
38604
|
if (body) buildFor(node, body);
|
|
38490
38605
|
}
|
|
@@ -38501,7 +38616,7 @@ const analyzeControlFlow = (program) => {
|
|
|
38501
38616
|
const enclosingFunction = (node) => {
|
|
38502
38617
|
let current = node;
|
|
38503
38618
|
while (current) {
|
|
38504
|
-
if (isFunctionLike$
|
|
38619
|
+
if (isFunctionLike$3(current)) return current;
|
|
38505
38620
|
if (isNodeOfType(current, "Program")) return current;
|
|
38506
38621
|
current = current.parent ?? null;
|
|
38507
38622
|
}
|
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.6851a78",
|
|
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",
|