oxlint-plugin-react-doctor 0.5.8-dev.e2393c4 → 0.5.8-dev.f028d8b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +140 -71
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,6 +3,48 @@ import * as fs from "node:fs";
|
|
|
3
3
|
import { parseSync } from "oxc-parser";
|
|
4
4
|
import { analyze } from "eslint-scope";
|
|
5
5
|
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
6
|
+
//#region src/plugin/utils/has-type-property.ts
|
|
7
|
+
const hasTypeProperty = (value) => Boolean(value && typeof value === "object" && "type" in value);
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/plugin/utils/is-node-of-type.ts
|
|
10
|
+
const isNodeOfType = (node, type) => Boolean(hasTypeProperty(node) && node.type === type);
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/plugin/utils/non-react-jsx-dialect.ts
|
|
13
|
+
const NON_REACT_JSX_DIALECT_PACKAGES = new Set([
|
|
14
|
+
"solid-js",
|
|
15
|
+
"solid-js/web",
|
|
16
|
+
"solid-js/store",
|
|
17
|
+
"solid-js/h",
|
|
18
|
+
"solid-js/html",
|
|
19
|
+
"@builder.io/qwik",
|
|
20
|
+
"@builder.io/qwik-city",
|
|
21
|
+
"@builder.io/qwik-react",
|
|
22
|
+
"voby",
|
|
23
|
+
"vidode"
|
|
24
|
+
]);
|
|
25
|
+
const NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES = ["solid-js", "@builder.io/qwik"];
|
|
26
|
+
const startsWithAny = (source, prefixes) => prefixes.some((prefix) => source === prefix || source.startsWith(`${prefix}/`));
|
|
27
|
+
const fileImportsNonReactJsxDialect = (program) => {
|
|
28
|
+
for (const statement of program.body) {
|
|
29
|
+
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
30
|
+
const source = statement.source;
|
|
31
|
+
const value = source && typeof source.value === "string" ? source.value : null;
|
|
32
|
+
if (!value) continue;
|
|
33
|
+
if (NON_REACT_JSX_DIALECT_PACKAGES.has(value)) return true;
|
|
34
|
+
if (startsWithAny(value, NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES)) return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
|
+
const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
39
|
+
for (const attribute of openingNode.attributes) {
|
|
40
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
41
|
+
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
42
|
+
if (attribute.name.name === "classList") return true;
|
|
43
|
+
if (attribute.name.name.startsWith("class:") || attribute.name.name.startsWith("bind:")) return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
};
|
|
47
|
+
//#endregion
|
|
6
48
|
//#region src/plugin/utils/is-testlike-filename.ts
|
|
7
49
|
const NON_PRODUCTION_PATH_SEGMENTS = [
|
|
8
50
|
"/test/",
|
|
@@ -180,53 +222,10 @@ const isTestlikeFilename = (rawFilename) => {
|
|
|
180
222
|
return false;
|
|
181
223
|
};
|
|
182
224
|
//#endregion
|
|
183
|
-
//#region src/plugin/utils/
|
|
184
|
-
const
|
|
185
|
-
//#endregion
|
|
186
|
-
//#region src/plugin/utils/is-node-of-type.ts
|
|
187
|
-
const isNodeOfType = (node, type) => Boolean(hasTypeProperty(node) && node.type === type);
|
|
188
|
-
//#endregion
|
|
189
|
-
//#region src/plugin/utils/non-react-jsx-dialect.ts
|
|
190
|
-
const NON_REACT_JSX_DIALECT_PACKAGES = new Set([
|
|
191
|
-
"solid-js",
|
|
192
|
-
"solid-js/web",
|
|
193
|
-
"solid-js/store",
|
|
194
|
-
"solid-js/h",
|
|
195
|
-
"solid-js/html",
|
|
196
|
-
"@builder.io/qwik",
|
|
197
|
-
"@builder.io/qwik-city",
|
|
198
|
-
"@builder.io/qwik-react",
|
|
199
|
-
"voby",
|
|
200
|
-
"vidode"
|
|
201
|
-
]);
|
|
202
|
-
const NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES = ["solid-js", "@builder.io/qwik"];
|
|
203
|
-
const startsWithAny = (source, prefixes) => prefixes.some((prefix) => source === prefix || source.startsWith(`${prefix}/`));
|
|
204
|
-
const fileImportsNonReactJsxDialect = (program) => {
|
|
205
|
-
for (const statement of program.body) {
|
|
206
|
-
if (!isNodeOfType(statement, "ImportDeclaration")) continue;
|
|
207
|
-
const source = statement.source;
|
|
208
|
-
const value = source && typeof source.value === "string" ? source.value : null;
|
|
209
|
-
if (!value) continue;
|
|
210
|
-
if (NON_REACT_JSX_DIALECT_PACKAGES.has(value)) return true;
|
|
211
|
-
if (startsWithAny(value, NON_REACT_JSX_DIALECT_PACKAGE_PREFIXES)) return true;
|
|
212
|
-
}
|
|
213
|
-
return false;
|
|
214
|
-
};
|
|
215
|
-
const jsxAttributeIsNonReactDialectMarker = (openingNode) => {
|
|
216
|
-
for (const attribute of openingNode.attributes) {
|
|
217
|
-
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
218
|
-
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
219
|
-
if (attribute.name.name === "classList") return true;
|
|
220
|
-
if (attribute.name.name.startsWith("class:") || attribute.name.name.startsWith("bind:")) return true;
|
|
221
|
-
}
|
|
222
|
-
return false;
|
|
223
|
-
};
|
|
225
|
+
//#region src/plugin/utils/skip-non-production-files.ts
|
|
226
|
+
const skipNonProductionFiles = (create) => (context) => isTestlikeFilename(context.filename) ? {} : create(context);
|
|
224
227
|
//#endregion
|
|
225
228
|
//#region src/plugin/utils/define-rule.ts
|
|
226
|
-
const wrapCreateForTestNoise = (create) => ((context) => {
|
|
227
|
-
if (isTestlikeFilename(context.filename)) return {};
|
|
228
|
-
return create(context);
|
|
229
|
-
});
|
|
230
229
|
const VISITOR_NODE_NAME_PATTERN = /^[A-Z]/;
|
|
231
230
|
const wrapCreateForReactJsxOnly = (create) => ((context) => {
|
|
232
231
|
const innerVisitors = create(context);
|
|
@@ -273,7 +272,7 @@ const defineRule = (rule) => {
|
|
|
273
272
|
};
|
|
274
273
|
const tags = rule.tags;
|
|
275
274
|
let wrappedCreate = rule.create;
|
|
276
|
-
if (tags?.includes("test-noise") && !tags?.includes("migration-hint")) wrappedCreate =
|
|
275
|
+
if (tags?.includes("test-noise") && !tags?.includes("migration-hint")) wrappedCreate = skipNonProductionFiles(wrappedCreate);
|
|
277
276
|
if (tags?.includes("react-jsx-only")) wrappedCreate = wrapCreateForReactJsxOnly(wrappedCreate);
|
|
278
277
|
if (wrappedCreate === rule.create) return rule;
|
|
279
278
|
return {
|
|
@@ -1428,6 +1427,14 @@ const NEXTJS_NAVIGATION_FUNCTIONS = new Set([
|
|
|
1428
1427
|
"forbidden",
|
|
1429
1428
|
"unauthorized"
|
|
1430
1429
|
]);
|
|
1430
|
+
const CACHE_REVALIDATION_FUNCTION_NAMES = new Set([
|
|
1431
|
+
"revalidateTag",
|
|
1432
|
+
"revalidatePath",
|
|
1433
|
+
"expireTag",
|
|
1434
|
+
"expirePath",
|
|
1435
|
+
"unstable_expireTag",
|
|
1436
|
+
"unstable_expirePath"
|
|
1437
|
+
]);
|
|
1431
1438
|
const GOOGLE_FONTS_PATTERN = /fonts\.googleapis\.com/;
|
|
1432
1439
|
const POLYFILL_SCRIPT_PATTERN = /polyfill\.io|polyfill\.min\.js|cdn\.polyfill/;
|
|
1433
1440
|
const APP_DIRECTORY_PATTERN = /\/app\//;
|
|
@@ -4363,7 +4370,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4363
4370
|
title: "Auth token in web storage",
|
|
4364
4371
|
severity: "warn",
|
|
4365
4372
|
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
4366
|
-
create: (context) => ({
|
|
4373
|
+
create: skipNonProductionFiles((context) => ({
|
|
4367
4374
|
CallExpression(node) {
|
|
4368
4375
|
const callee = node.callee;
|
|
4369
4376
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed) return;
|
|
@@ -4388,7 +4395,7 @@ const authTokenInWebStorage = defineRule({
|
|
|
4388
4395
|
message: MESSAGE$57
|
|
4389
4396
|
});
|
|
4390
4397
|
}
|
|
4391
|
-
})
|
|
4398
|
+
}))
|
|
4392
4399
|
});
|
|
4393
4400
|
//#endregion
|
|
4394
4401
|
//#region src/plugin/rules/a11y/autocomplete-valid.ts
|
|
@@ -6735,7 +6742,7 @@ const TRANSPARENT_WRAPPER_TYPES = new Set([
|
|
|
6735
6742
|
"ParenthesizedExpression",
|
|
6736
6743
|
"ChainExpression"
|
|
6737
6744
|
]);
|
|
6738
|
-
const unwrapExpression$
|
|
6745
|
+
const unwrapExpression$2 = (node) => {
|
|
6739
6746
|
let current = node;
|
|
6740
6747
|
while (TRANSPARENT_WRAPPER_TYPES.has(current.type)) {
|
|
6741
6748
|
const inner = current.expression;
|
|
@@ -6842,7 +6849,7 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
6842
6849
|
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return false;
|
|
6843
6850
|
const initializerRaw = declarator.init;
|
|
6844
6851
|
if (!initializerRaw) return false;
|
|
6845
|
-
const initializer = unwrapExpression$
|
|
6852
|
+
const initializer = unwrapExpression$2(initializerRaw);
|
|
6846
6853
|
if (symbol.kind === "const") {
|
|
6847
6854
|
if (isNodeOfType(initializer, "Literal") && (initializer.value === null || typeof initializer.value === "number" || typeof initializer.value === "string" || typeof initializer.value === "boolean")) return true;
|
|
6848
6855
|
if (isNodeOfType(initializer, "TemplateLiteral") && getStaticTemplateLiteralValue(initializer) !== null) return true;
|
|
@@ -6862,13 +6869,13 @@ const symbolHasStableHookOrigin = (symbol) => {
|
|
|
6862
6869
|
return false;
|
|
6863
6870
|
};
|
|
6864
6871
|
const symbolHasUseEffectEventOrigin = (symbol) => {
|
|
6865
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
6872
|
+
const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
|
|
6866
6873
|
if (!initializer || !isNodeOfType(initializer, "CallExpression")) return false;
|
|
6867
6874
|
return getHookName(initializer.callee) === "useEffectEvent";
|
|
6868
6875
|
};
|
|
6869
6876
|
const getFunctionValueNode = (symbol) => {
|
|
6870
6877
|
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration")) return symbol.declarationNode;
|
|
6871
|
-
const initializer = symbol.initializer ? unwrapExpression$
|
|
6878
|
+
const initializer = symbol.initializer ? unwrapExpression$2(symbol.initializer) : null;
|
|
6872
6879
|
if (initializer && (isNodeOfType(initializer, "FunctionExpression") || isNodeOfType(initializer, "ArrowFunctionExpression"))) return initializer;
|
|
6873
6880
|
return null;
|
|
6874
6881
|
};
|
|
@@ -7004,23 +7011,23 @@ const computeDepKey = (reference) => {
|
|
|
7004
7011
|
return fullName;
|
|
7005
7012
|
};
|
|
7006
7013
|
const computeDeclaredDepKey = (entry) => {
|
|
7007
|
-
const stripped = unwrapExpression$
|
|
7014
|
+
const stripped = unwrapExpression$2(entry);
|
|
7008
7015
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7009
7016
|
if (isNodeOfType(stripped, "MemberExpression")) return stringifyMemberChain(stripped);
|
|
7010
7017
|
return null;
|
|
7011
7018
|
};
|
|
7012
7019
|
const depsArrayContainsIdentifier = (depsArgument, identifierName) => {
|
|
7013
7020
|
if (!depsArgument) return false;
|
|
7014
|
-
const strippedDepsArgument = unwrapExpression$
|
|
7021
|
+
const strippedDepsArgument = unwrapExpression$2(depsArgument);
|
|
7015
7022
|
if (!isNodeOfType(strippedDepsArgument, "ArrayExpression")) return false;
|
|
7016
7023
|
return strippedDepsArgument.elements.some((element) => {
|
|
7017
7024
|
if (!element) return false;
|
|
7018
|
-
const strippedElement = unwrapExpression$
|
|
7025
|
+
const strippedElement = unwrapExpression$2(element);
|
|
7019
7026
|
return isNodeOfType(strippedElement, "Identifier") && strippedElement.name === identifierName;
|
|
7020
7027
|
});
|
|
7021
7028
|
};
|
|
7022
7029
|
const stringifyMemberChain = (node) => {
|
|
7023
|
-
const stripped = unwrapExpression$
|
|
7030
|
+
const stripped = unwrapExpression$2(node);
|
|
7024
7031
|
if (isNodeOfType(stripped, "Identifier")) return stripped.name;
|
|
7025
7032
|
if (isNodeOfType(stripped, "ThisExpression")) return "this";
|
|
7026
7033
|
if (isNodeOfType(stripped, "MemberExpression")) {
|
|
@@ -7062,13 +7069,13 @@ const hasBroaderDeclaredDependency = (declaredKey, declaredKeys) => {
|
|
|
7062
7069
|
return false;
|
|
7063
7070
|
};
|
|
7064
7071
|
const getMemberRootIdentifier = (node) => {
|
|
7065
|
-
const stripped = unwrapExpression$
|
|
7072
|
+
const stripped = unwrapExpression$2(node);
|
|
7066
7073
|
if (isNodeOfType(stripped, "Identifier")) return stripped;
|
|
7067
7074
|
if (isNodeOfType(stripped, "MemberExpression")) return getMemberRootIdentifier(stripped.object);
|
|
7068
7075
|
return null;
|
|
7069
7076
|
};
|
|
7070
7077
|
const hasComputedMemberExpression = (node) => {
|
|
7071
|
-
const stripped = unwrapExpression$
|
|
7078
|
+
const stripped = unwrapExpression$2(node);
|
|
7072
7079
|
if (!isNodeOfType(stripped, "MemberExpression")) return false;
|
|
7073
7080
|
if (stripped.computed) return true;
|
|
7074
7081
|
return hasComputedMemberExpression(stripped.object);
|
|
@@ -7089,7 +7096,7 @@ const isRegExpLiteral = (node) => {
|
|
|
7089
7096
|
};
|
|
7090
7097
|
const isUnstableInitializer = (node) => {
|
|
7091
7098
|
if (!node) return false;
|
|
7092
|
-
const stripped = unwrapExpression$
|
|
7099
|
+
const stripped = unwrapExpression$2(node);
|
|
7093
7100
|
if (isRegExpLiteral(stripped)) return true;
|
|
7094
7101
|
if (isNodeOfType(stripped, "ConditionalExpression")) return isUnstableInitializer(stripped.consequent) || isUnstableInitializer(stripped.alternate);
|
|
7095
7102
|
if (isNodeOfType(stripped, "LogicalExpression")) return isUnstableInitializer(stripped.left) || isUnstableInitializer(stripped.right);
|
|
@@ -7245,7 +7252,7 @@ const hasMemberCallForRoot = (node, rootName) => {
|
|
|
7245
7252
|
const visit = (current) => {
|
|
7246
7253
|
if (didFindMemberCall) return;
|
|
7247
7254
|
if (isNodeOfType(current, "CallExpression")) {
|
|
7248
|
-
if (getMemberRootIdentifier(unwrapExpression$
|
|
7255
|
+
if (getMemberRootIdentifier(unwrapExpression$2(current.callee))?.name === rootName) {
|
|
7249
7256
|
didFindMemberCall = true;
|
|
7250
7257
|
return;
|
|
7251
7258
|
}
|
|
@@ -7308,7 +7315,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7308
7315
|
return;
|
|
7309
7316
|
}
|
|
7310
7317
|
const depsArgumentRaw = node.arguments[depsArgumentIndex];
|
|
7311
|
-
const callbackExpression = unwrapExpression$
|
|
7318
|
+
const callbackExpression = unwrapExpression$2(callbackArgument);
|
|
7312
7319
|
let callbackToAnalyze = null;
|
|
7313
7320
|
const forcedCaptureKeys = /* @__PURE__ */ new Set();
|
|
7314
7321
|
if (isNodeOfType(callbackExpression, "ArrowFunctionExpression") || isNodeOfType(callbackExpression, "FunctionExpression")) callbackToAnalyze = callbackExpression;
|
|
@@ -7316,7 +7323,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7316
7323
|
const callbackSymbol = context.scopes.symbolFor(callbackExpression);
|
|
7317
7324
|
const functionValueNode = callbackSymbol ? getFunctionValueNode(callbackSymbol) : null;
|
|
7318
7325
|
if (functionValueNode) callbackToAnalyze = functionValueNode;
|
|
7319
|
-
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$
|
|
7326
|
+
else if (callbackSymbol?.initializer && isNodeOfType(unwrapExpression$2(callbackSymbol.initializer), "CallExpression")) forcedCaptureKeys.add(callbackExpression.name);
|
|
7320
7327
|
else if (depsArgumentRaw) {
|
|
7321
7328
|
if (depsArrayContainsIdentifier(depsArgumentRaw, callbackExpression.name)) return;
|
|
7322
7329
|
context.report({
|
|
@@ -7373,7 +7380,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7373
7380
|
});
|
|
7374
7381
|
return;
|
|
7375
7382
|
}
|
|
7376
|
-
const depsArgument = unwrapExpression$
|
|
7383
|
+
const depsArgument = unwrapExpression$2(depsArgumentRaw);
|
|
7377
7384
|
if (isNodeOfType(depsArgument, "Literal") && depsArgument.value === null || isNodeOfType(depsArgument, "Identifier") && depsArgument.name === "undefined") {
|
|
7378
7385
|
if (isAutoDependenciesHook(hookName)) return;
|
|
7379
7386
|
if (HOOKS_REQUIRING_DEPS_ARRAY.has(hookName)) {
|
|
@@ -7414,11 +7421,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7414
7421
|
for (const forcedCaptureKey of forcedCaptureKeys) captureKeys.add(forcedCaptureKey);
|
|
7415
7422
|
const hasLiteralDepElement = depsArgument.elements.some((element) => {
|
|
7416
7423
|
if (!element) return false;
|
|
7417
|
-
return isLiteralOrEmptyTemplate(unwrapExpression$
|
|
7424
|
+
return isLiteralOrEmptyTemplate(unwrapExpression$2(element));
|
|
7418
7425
|
});
|
|
7419
7426
|
const hasNonStringLiteralDep = depsArgument.elements.some((element) => {
|
|
7420
7427
|
if (!element) return false;
|
|
7421
|
-
return isNonStringLiteral(unwrapExpression$
|
|
7428
|
+
return isNonStringLiteral(unwrapExpression$2(element));
|
|
7422
7429
|
});
|
|
7423
7430
|
if (hasNonStringLiteralDep) context.report({
|
|
7424
7431
|
node: depsArgument,
|
|
@@ -7439,7 +7446,7 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
7439
7446
|
});
|
|
7440
7447
|
continue;
|
|
7441
7448
|
}
|
|
7442
|
-
const stripped = unwrapExpression$
|
|
7449
|
+
const stripped = unwrapExpression$2(elementNode);
|
|
7443
7450
|
if (isLiteralOrEmptyTemplate(stripped)) continue;
|
|
7444
7451
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
7445
7452
|
const depSymbol = context.scopes.symbolFor(stripped);
|
|
@@ -18960,7 +18967,7 @@ const noEval = defineRule({
|
|
|
18960
18967
|
title: "eval() runs untrusted code strings",
|
|
18961
18968
|
severity: "error",
|
|
18962
18969
|
recommendation: "Use `JSON.parse` for data, or rewrite the code so it doesn't build and run code from strings.",
|
|
18963
|
-
create: (context) => ({
|
|
18970
|
+
create: skipNonProductionFiles((context) => ({
|
|
18964
18971
|
CallExpression(node) {
|
|
18965
18972
|
if (isNodeOfType(node.callee, "Identifier") && node.callee.name === "eval") {
|
|
18966
18973
|
context.report({
|
|
@@ -18980,7 +18987,7 @@ const noEval = defineRule({
|
|
|
18980
18987
|
message: "new Function() is a code-injection vulnerability: it builds & runs code from a string."
|
|
18981
18988
|
});
|
|
18982
18989
|
}
|
|
18983
|
-
})
|
|
18990
|
+
}))
|
|
18984
18991
|
});
|
|
18985
18992
|
//#endregion
|
|
18986
18993
|
//#region src/plugin/rules/state-and-effects/no-event-handler.ts
|
|
@@ -27034,7 +27041,7 @@ const preferHtmlDialog = defineRule({
|
|
|
27034
27041
|
});
|
|
27035
27042
|
//#endregion
|
|
27036
27043
|
//#region src/plugin/utils/function-returns-object-literal.ts
|
|
27037
|
-
const unwrapExpression = (node) => {
|
|
27044
|
+
const unwrapExpression$1 = (node) => {
|
|
27038
27045
|
let current = node;
|
|
27039
27046
|
for (;;) {
|
|
27040
27047
|
if ((current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") && "expression" in current && isAstNode(current.expression)) {
|
|
@@ -27047,7 +27054,7 @@ const unwrapExpression = (node) => {
|
|
|
27047
27054
|
const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
27048
27055
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
27049
27056
|
const body = functionNode.body;
|
|
27050
|
-
if (body && body.type !== "BlockStatement") return unwrapExpression(body).type === "ObjectExpression";
|
|
27057
|
+
if (body && body.type !== "BlockStatement") return unwrapExpression$1(body).type === "ObjectExpression";
|
|
27051
27058
|
}
|
|
27052
27059
|
const body = functionNode.body;
|
|
27053
27060
|
if (!body || body.type !== "BlockStatement") return false;
|
|
@@ -27055,7 +27062,7 @@ const doesFunctionReturnsObjectLiteral = (functionNode) => {
|
|
|
27055
27062
|
const visit = (node) => {
|
|
27056
27063
|
if (returnsObject) return;
|
|
27057
27064
|
if (node.type === "ReturnStatement" && "argument" in node && node.argument != null) {
|
|
27058
|
-
if (unwrapExpression(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
27065
|
+
if (unwrapExpression$1(node.argument).type === "ObjectExpression") returnsObject = true;
|
|
27059
27066
|
return;
|
|
27060
27067
|
}
|
|
27061
27068
|
const nodeRecord = node;
|
|
@@ -35242,6 +35249,67 @@ const isAuthGuardName = (calleeName) => {
|
|
|
35242
35249
|
return false;
|
|
35243
35250
|
};
|
|
35244
35251
|
//#endregion
|
|
35252
|
+
//#region src/plugin/utils/is-non-privileged-server-action.ts
|
|
35253
|
+
const NON_DATA_EFFECT_FUNCTION_NAMES = new Set([...CACHE_REVALIDATION_FUNCTION_NAMES, ...NEXTJS_NAVIGATION_FUNCTIONS]);
|
|
35254
|
+
const isCacheOrNavigationCall = (node) => isNodeOfType(node, "CallExpression") && isNodeOfType(node.callee, "Identifier") && NON_DATA_EFFECT_FUNCTION_NAMES.has(node.callee.name);
|
|
35255
|
+
const unwrapExpression = (node) => {
|
|
35256
|
+
let current = node;
|
|
35257
|
+
while (current) {
|
|
35258
|
+
if (isNodeOfType(current, "TSAsExpression") || isNodeOfType(current, "TSNonNullExpression") || isNodeOfType(current, "TSSatisfiesExpression") || isNodeOfType(current, "ChainExpression")) {
|
|
35259
|
+
current = current.expression;
|
|
35260
|
+
continue;
|
|
35261
|
+
}
|
|
35262
|
+
if (isNodeOfType(current, "SequenceExpression")) {
|
|
35263
|
+
current = current.expressions?.[current.expressions.length - 1];
|
|
35264
|
+
continue;
|
|
35265
|
+
}
|
|
35266
|
+
return current;
|
|
35267
|
+
}
|
|
35268
|
+
return null;
|
|
35269
|
+
};
|
|
35270
|
+
const isDataExposingValue = (node) => {
|
|
35271
|
+
const value = unwrapExpression(node);
|
|
35272
|
+
if (!value) return false;
|
|
35273
|
+
if (isCacheOrNavigationCall(value)) return false;
|
|
35274
|
+
return !isLiteralOnlyExpression(value);
|
|
35275
|
+
};
|
|
35276
|
+
const isLiteralOnlyExpression = (node) => {
|
|
35277
|
+
if (!node) return false;
|
|
35278
|
+
if (isNodeOfType(node, "Literal")) return true;
|
|
35279
|
+
if (isNodeOfType(node, "TemplateLiteral")) return (node.expressions ?? []).every(isLiteralOnlyExpression);
|
|
35280
|
+
if (isNodeOfType(node, "UnaryExpression")) return isLiteralOnlyExpression(node.argument);
|
|
35281
|
+
if (isNodeOfType(node, "ArrayExpression")) return (node.elements ?? []).every((element) => element === null || !isNodeOfType(element, "SpreadElement") && isLiteralOnlyExpression(element));
|
|
35282
|
+
if (isNodeOfType(node, "ObjectExpression")) return (node.properties ?? []).every((property) => isNodeOfType(property, "Property") && (!property.computed || isLiteralOnlyExpression(property.key)) && isLiteralOnlyExpression(property.value));
|
|
35283
|
+
return false;
|
|
35284
|
+
};
|
|
35285
|
+
const getReturnedOrThrownArgument = (node) => {
|
|
35286
|
+
if (isNodeOfType(node, "ReturnStatement")) return node.argument ?? null;
|
|
35287
|
+
if (isNodeOfType(node, "ThrowStatement")) return node.argument ?? null;
|
|
35288
|
+
return null;
|
|
35289
|
+
};
|
|
35290
|
+
const isDataExposingReturnOrThrow = (node) => isDataExposingValue(getReturnedOrThrownArgument(node));
|
|
35291
|
+
const isPrivilegedEffect = (node) => isNodeOfType(node, "CallExpression") || isNodeOfType(node, "TaggedTemplateExpression") || isNodeOfType(node, "NewExpression") || isNodeOfType(node, "AssignmentExpression") || isNodeOfType(node, "UpdateExpression") || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" || isDataExposingReturnOrThrow(node);
|
|
35292
|
+
const isNonPrivilegedServerAction = (functionNode) => {
|
|
35293
|
+
const functionBody = functionNode.body;
|
|
35294
|
+
if (!functionBody) return false;
|
|
35295
|
+
if (!isNodeOfType(functionBody, "BlockStatement") && isDataExposingValue(functionBody)) return false;
|
|
35296
|
+
let hasNonDataEffectCall = false;
|
|
35297
|
+
let hasPrivilegedEffect = false;
|
|
35298
|
+
walkAst(functionBody, (child) => {
|
|
35299
|
+
if (hasPrivilegedEffect) return false;
|
|
35300
|
+
if (child !== functionBody && isFunctionLike$2(child)) return false;
|
|
35301
|
+
if (isCacheOrNavigationCall(child)) {
|
|
35302
|
+
hasNonDataEffectCall = true;
|
|
35303
|
+
return;
|
|
35304
|
+
}
|
|
35305
|
+
if (isPrivilegedEffect(child)) {
|
|
35306
|
+
hasPrivilegedEffect = true;
|
|
35307
|
+
return false;
|
|
35308
|
+
}
|
|
35309
|
+
});
|
|
35310
|
+
return hasNonDataEffectCall && !hasPrivilegedEffect;
|
|
35311
|
+
};
|
|
35312
|
+
//#endregion
|
|
35245
35313
|
//#region src/plugin/rules/server/server-auth-actions.ts
|
|
35246
35314
|
const isAsyncFunctionLikeNode = (node) => {
|
|
35247
35315
|
if (!node) return false;
|
|
@@ -35316,6 +35384,7 @@ const getAuthScanRoots = (functionNode) => {
|
|
|
35316
35384
|
const inspectServerAction = (candidate, fileHasUseServerDirective, allowedFunctionNames, context) => {
|
|
35317
35385
|
if (!(fileHasUseServerDirective || hasUseServerDirective(candidate.functionNode))) return;
|
|
35318
35386
|
if (containsAuthCheck(getAuthScanRoots(candidate.functionNode), allowedFunctionNames, GENERIC_AUTH_METHOD_NAMES)) return;
|
|
35387
|
+
if (isNonPrivilegedServerAction(candidate.functionNode)) return;
|
|
35319
35388
|
context.report({
|
|
35320
35389
|
node: candidate.reportNode,
|
|
35321
35390
|
message: `Anyone can call server action "${candidate.displayName}" without logging in, since it has no auth check.`
|