@sarj/eslint-plugin 15.2.0 → 15.4.0
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.cjs +154 -47
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +154 -47
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -104,6 +104,7 @@ ${missing.join("\n")}`);
|
|
|
104
104
|
function publicExample(example) {
|
|
105
105
|
return {
|
|
106
106
|
id: example.id,
|
|
107
|
+
scenarioId: example.scenarioId ?? "primary",
|
|
107
108
|
title: example.title,
|
|
108
109
|
outcome: example.outcome,
|
|
109
110
|
files: example.files.map(publicFile),
|
|
@@ -148,9 +149,14 @@ function nativeSpec(config, documentation) {
|
|
|
148
149
|
const examples = [...documentation.examples ?? []];
|
|
149
150
|
examples.forEach(validateExample);
|
|
150
151
|
assertUnique(examples.map((example) => example.id), "rule example IDs");
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const publicExamples = examples.filter((example) => example.public === true);
|
|
153
|
+
const scenarios = new Set(publicExamples.map((example) => example.scenarioId ?? "primary"));
|
|
154
|
+
for (const scenario of scenarios) {
|
|
155
|
+
const pair = publicExamples.filter((example) => (example.scenarioId ?? "primary") === scenario);
|
|
156
|
+
const outcomes = new Set(pair.map((example) => example.outcome));
|
|
157
|
+
if (pair.length !== 2 || !outcomes.has("match") || !outcomes.has("no-match")) {
|
|
158
|
+
throw new TypeError(`published example scenario ${scenario} must contain both matching and non-matching cases exactly once`);
|
|
159
|
+
}
|
|
154
160
|
}
|
|
155
161
|
const messageIds = Object.keys(meta2.messages).sort();
|
|
156
162
|
const schema = optionsSchema(meta2.schema);
|
|
@@ -171,7 +177,7 @@ function nativeSpec(config, documentation) {
|
|
|
171
177
|
references,
|
|
172
178
|
since: documentation.since ?? null,
|
|
173
179
|
examples,
|
|
174
|
-
publicExamples
|
|
180
|
+
publicExamples,
|
|
175
181
|
messageIds,
|
|
176
182
|
optionsSchema: schema
|
|
177
183
|
};
|
|
@@ -191,6 +197,9 @@ function validateExample(example) {
|
|
|
191
197
|
if (!KEBAB_CASE.test(example.id)) {
|
|
192
198
|
throw new TypeError("example ID must be lowercase kebab-case");
|
|
193
199
|
}
|
|
200
|
+
if (!KEBAB_CASE.test(example.scenarioId ?? "primary")) {
|
|
201
|
+
throw new TypeError("example scenario must be lowercase kebab-case");
|
|
202
|
+
}
|
|
194
203
|
if (example.title.trim().length === 0) {
|
|
195
204
|
throw new TypeError("example title must not be empty");
|
|
196
205
|
}
|
|
@@ -3264,14 +3273,12 @@ var noJsonStringifyErrorDocumentation = {
|
|
|
3264
3273
|
rationale: "Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.",
|
|
3265
3274
|
remediation: "Serialize explicit error fields or use an error-aware serializer.",
|
|
3266
3275
|
category: "correctness",
|
|
3267
|
-
limitations: ["The rule uses local
|
|
3276
|
+
limitations: ["The rule uses local catch-binding and constructor provenance rather than type information."],
|
|
3268
3277
|
examples: [
|
|
3269
3278
|
{ id: "explicit-error-message", title: "Serialize an enumerable error field", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err.message }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
|
|
3270
3279
|
{ id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
|
|
3271
3280
|
]
|
|
3272
3281
|
};
|
|
3273
|
-
var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
|
|
3274
|
-
var ERROR_PROP_PATTERN = /^(cause|lastError|error|err|exception|originalError|innerError)$/i;
|
|
3275
3282
|
var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
|
|
3276
3283
|
var PAYLOAD_PROPS = /* @__PURE__ */ new Set([
|
|
3277
3284
|
"data",
|
|
@@ -3299,6 +3306,21 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
|
3299
3306
|
"TypeError",
|
|
3300
3307
|
"URIError"
|
|
3301
3308
|
]);
|
|
3309
|
+
function identifierIsProvenError(identifier, scope) {
|
|
3310
|
+
if (isCatchBinding(scope, identifier.name)) return true;
|
|
3311
|
+
let current = scope;
|
|
3312
|
+
while (current !== null && !current.set.has(identifier.name)) {
|
|
3313
|
+
current = current.upper;
|
|
3314
|
+
}
|
|
3315
|
+
const variable = current?.set.get(identifier.name);
|
|
3316
|
+
if (variable === void 0 || variable.defs.length !== 1) return false;
|
|
3317
|
+
const definition = variable.defs[0];
|
|
3318
|
+
if (definition?.type !== "Variable") return false;
|
|
3319
|
+
const initializer = definition.node.init;
|
|
3320
|
+
return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
|
|
3321
|
+
(reference) => !reference.isWrite() || reference.init === true
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3302
3324
|
function isCatchBinding(scope, name) {
|
|
3303
3325
|
let current = scope;
|
|
3304
3326
|
while (current) {
|
|
@@ -3314,22 +3336,6 @@ function isCatchBinding(scope, name) {
|
|
|
3314
3336
|
}
|
|
3315
3337
|
return false;
|
|
3316
3338
|
}
|
|
3317
|
-
function memberSuggestsError(member, scope) {
|
|
3318
|
-
const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
|
|
3319
|
-
if (propName2 !== null && ERROR_PROP_PATTERN.test(propName2)) {
|
|
3320
|
-
return true;
|
|
3321
|
-
}
|
|
3322
|
-
const base = member.object;
|
|
3323
|
-
const baseSuggestsError = base.type === "Identifier" && (ERROR_NAME_PATTERN.test(base.name) || isCatchBinding(scope, base.name));
|
|
3324
|
-
if (baseSuggestsError) {
|
|
3325
|
-
if (propName2 === null) {
|
|
3326
|
-
return true;
|
|
3327
|
-
}
|
|
3328
|
-
const lowered = propName2.toLowerCase();
|
|
3329
|
-
return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
|
|
3330
|
-
}
|
|
3331
|
-
return false;
|
|
3332
|
-
}
|
|
3333
3339
|
function positiveErrorSubject(test) {
|
|
3334
3340
|
return instanceofErrorSubject(test) ?? typeGuardSubject(test);
|
|
3335
3341
|
}
|
|
@@ -3433,27 +3439,25 @@ function directLiteralValues(argument) {
|
|
|
3433
3439
|
}
|
|
3434
3440
|
function expressionSuggestsError(expression, scope) {
|
|
3435
3441
|
if (expression.type === "Identifier") {
|
|
3436
|
-
return
|
|
3442
|
+
return identifierIsProvenError(expression, scope);
|
|
3443
|
+
}
|
|
3444
|
+
if (expression.type === "NewExpression" && expression.callee.type === "Identifier") {
|
|
3445
|
+
return BUILTIN_ERROR_CONSTRUCTORS.has(expression.callee.name);
|
|
3437
3446
|
}
|
|
3438
3447
|
return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
|
|
3439
3448
|
}
|
|
3440
|
-
function
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
if (definition?.type !== "Variable") return false;
|
|
3451
|
-
const initializer = definition.node.init;
|
|
3452
|
-
return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
|
|
3453
|
-
(reference) => !reference.isWrite() || reference.init === true
|
|
3454
|
-
);
|
|
3449
|
+
function memberSuggestsError(member, scope) {
|
|
3450
|
+
const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
|
|
3451
|
+
const base = member.object;
|
|
3452
|
+
const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, scope);
|
|
3453
|
+
if (baseSuggestsError) {
|
|
3454
|
+
if (propName2 === null) {
|
|
3455
|
+
return true;
|
|
3456
|
+
}
|
|
3457
|
+
const lowered = propName2.toLowerCase();
|
|
3458
|
+
return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
|
|
3455
3459
|
}
|
|
3456
|
-
return
|
|
3460
|
+
return false;
|
|
3457
3461
|
}
|
|
3458
3462
|
var no_json_stringify_error_default = createRule({
|
|
3459
3463
|
name: "no-json-stringify-error",
|
|
@@ -3480,9 +3484,8 @@ var no_json_stringify_error_default = createRule({
|
|
|
3480
3484
|
return;
|
|
3481
3485
|
}
|
|
3482
3486
|
const scope = context.sourceCode.getScope(firstArg);
|
|
3483
|
-
const isNestedLiteral = firstArg.type === "ObjectExpression" || firstArg.type === "ArrayExpression";
|
|
3484
3487
|
const unsafeValue = directLiteralValues(firstArg).find(
|
|
3485
|
-
(value) =>
|
|
3488
|
+
(value) => expressionSuggestsError(value, scope) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
|
|
3486
3489
|
);
|
|
3487
3490
|
if (unsafeValue === void 0) {
|
|
3488
3491
|
return;
|
|
@@ -4941,6 +4944,7 @@ var ANALYTICS_SEGMENTS2 = /* @__PURE__ */ new Set([
|
|
|
4941
4944
|
]);
|
|
4942
4945
|
var SERVER_ACTION_SKIP_FILE_RE = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
4943
4946
|
var NON_REACT_FRAMEWORK_RE = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
|
|
4947
|
+
var NEXT_MODULE_PATH_RE = /(?:^|[/\\])(?:app|pages)[/\\]/u;
|
|
4944
4948
|
function isGlobalFetchCall(node, resolvesToGlobal) {
|
|
4945
4949
|
const callee = node.callee;
|
|
4946
4950
|
if (callee.type === "Identifier") {
|
|
@@ -5056,6 +5060,13 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
5056
5060
|
const nonReactFramework = context.sourceCode.ast.body.some(
|
|
5057
5061
|
(statement) => statement.type === import_utils25.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && NON_REACT_FRAMEWORK_RE.test(statement.source.value)
|
|
5058
5062
|
);
|
|
5063
|
+
const hasUseClientDirective = context.sourceCode.ast.body.some(
|
|
5064
|
+
(statement) => statement.type === import_utils25.AST_NODE_TYPES.ExpressionStatement && statement.expression.type === import_utils25.AST_NODE_TYPES.Literal && statement.expression.value === "use client"
|
|
5065
|
+
);
|
|
5066
|
+
const hasNextImport = context.sourceCode.ast.body.some(
|
|
5067
|
+
(statement) => statement.type === import_utils25.AST_NODE_TYPES.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "next" || statement.source.value.startsWith("next/"))
|
|
5068
|
+
);
|
|
5069
|
+
const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE.test(filename);
|
|
5059
5070
|
function resolvesToGlobal(identifier) {
|
|
5060
5071
|
const variable = import_utils25.ASTUtils.findVariable(
|
|
5061
5072
|
context.sourceCode.getScope(identifier),
|
|
@@ -5116,7 +5127,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
5116
5127
|
return resolved?.type === import_utils25.AST_NODE_TYPES.LogicalExpression && resolved.operator === "||" && (isMutationMethod2(resolved.left) || isMutationMethod2(resolved.right));
|
|
5117
5128
|
}
|
|
5118
5129
|
function serverActionOwns(node) {
|
|
5119
|
-
if (node.callee.type !== import_utils25.AST_NODE_TYPES.Identifier || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
|
|
5130
|
+
if (node.callee.type !== import_utils25.AST_NODE_TYPES.Identifier || !hasNextEvidence || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
|
|
5120
5131
|
return false;
|
|
5121
5132
|
}
|
|
5122
5133
|
const url = node.arguments[0];
|
|
@@ -6335,6 +6346,20 @@ var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
|
6335
6346
|
function isBodyDecodeNode(node) {
|
|
6336
6347
|
return node.type === import_utils32.AST_NODE_TYPES.CallExpression && node.callee.type === import_utils32.AST_NODE_TYPES.MemberExpression && !node.callee.computed && node.callee.property.type === import_utils32.AST_NODE_TYPES.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
|
|
6337
6348
|
}
|
|
6349
|
+
function isSafeParseSupportCall(node) {
|
|
6350
|
+
const callee = node.callee;
|
|
6351
|
+
if (callee.type !== import_utils32.AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== import_utils32.AST_NODE_TYPES.Identifier) {
|
|
6352
|
+
return false;
|
|
6353
|
+
}
|
|
6354
|
+
if (callee.property.name === "isArray" && callee.object.type === import_utils32.AST_NODE_TYPES.Identifier && callee.object.name === "Array") {
|
|
6355
|
+
return true;
|
|
6356
|
+
}
|
|
6357
|
+
if (callee.property.name !== "getItem") return false;
|
|
6358
|
+
if (callee.object.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.name === "localStorage" || callee.object.name === "sessionStorage")) {
|
|
6359
|
+
return true;
|
|
6360
|
+
}
|
|
6361
|
+
return callee.object.type === import_utils32.AST_NODE_TYPES.MemberExpression && !callee.object.computed && callee.object.object.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.object.name === "window" || callee.object.object.name === "globalThis") && callee.object.property.type === import_utils32.AST_NODE_TYPES.Identifier && (callee.object.property.name === "localStorage" || callee.object.property.name === "sessionStorage");
|
|
6362
|
+
}
|
|
6338
6363
|
var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
|
|
6339
6364
|
"json",
|
|
6340
6365
|
"text",
|
|
@@ -6403,6 +6428,7 @@ function tryReturnsSafeParse(catchNode) {
|
|
|
6403
6428
|
if (current.type === import_utils32.AST_NODE_TYPES.CallExpression || current.type === import_utils32.AST_NODE_TYPES.NewExpression) {
|
|
6404
6429
|
if (isParseShapedNode(current) || isBodyDecodeNode(current)) {
|
|
6405
6430
|
sawSafeParse = true;
|
|
6431
|
+
} else if (current.type === import_utils32.AST_NODE_TYPES.CallExpression && isSafeParseSupportCall(current)) {
|
|
6406
6432
|
} else {
|
|
6407
6433
|
sawUnsafeOperation = true;
|
|
6408
6434
|
return;
|
|
@@ -10926,7 +10952,70 @@ var bindingValidationPolarity = (test, bindingName) => {
|
|
|
10926
10952
|
}
|
|
10927
10953
|
return test.type === import_utils57.AST_NODE_TYPES.CallExpression && test.arguments.length === 1 && test.arguments[0]?.type === import_utils57.AST_NODE_TYPES.Identifier && test.arguments[0].name === bindingName && test.callee.type === import_utils57.AST_NODE_TYPES.MemberExpression && !test.callee.computed && test.callee.object.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.object.name === "Array" && test.callee.property.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
|
|
10928
10954
|
};
|
|
10955
|
+
var plainMemberAccess = (node) => node.type === import_utils57.AST_NODE_TYPES.MemberExpression && !node.computed && node.object.type === import_utils57.AST_NODE_TYPES.Identifier && node.property.type === import_utils57.AST_NODE_TYPES.Identifier ? { object: node.object.name, property: node.property.name } : null;
|
|
10956
|
+
var isSamePlainMember = (node, access) => {
|
|
10957
|
+
const candidate = plainMemberAccess(node);
|
|
10958
|
+
return candidate !== null && candidate.object === access.object && candidate.property === access.property;
|
|
10959
|
+
};
|
|
10929
10960
|
var nodeWithin2 = (node, container) => node.range[0] >= container.range[0] && node.range[1] <= container.range[1];
|
|
10961
|
+
var isUseWithinValidatedBranch = (node, bindingName) => {
|
|
10962
|
+
for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
10963
|
+
if (current.type === import_utils57.AST_NODE_TYPES.ConditionalExpression) {
|
|
10964
|
+
const polarity = bindingValidationPolarity(current.test, bindingName);
|
|
10965
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
|
|
10966
|
+
return true;
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
if (current.type === import_utils57.AST_NODE_TYPES.IfStatement) {
|
|
10970
|
+
const polarity = bindingValidationPolarity(current.test, bindingName);
|
|
10971
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
|
|
10972
|
+
return true;
|
|
10973
|
+
}
|
|
10974
|
+
}
|
|
10975
|
+
if (current.type === import_utils57.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils57.AST_NODE_TYPES.FunctionExpression || current.type === import_utils57.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
10976
|
+
return false;
|
|
10977
|
+
}
|
|
10978
|
+
}
|
|
10979
|
+
return false;
|
|
10980
|
+
};
|
|
10981
|
+
var isMemberUseWithinValidatedBranch = (node, access) => {
|
|
10982
|
+
for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
10983
|
+
if (current.type === import_utils57.AST_NODE_TYPES.ConditionalExpression) {
|
|
10984
|
+
const polarity = memberValidationPolarity(current.test, access);
|
|
10985
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
|
|
10986
|
+
return true;
|
|
10987
|
+
}
|
|
10988
|
+
}
|
|
10989
|
+
if (current.type === import_utils57.AST_NODE_TYPES.IfStatement) {
|
|
10990
|
+
const polarity = memberValidationPolarity(current.test, access);
|
|
10991
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
|
|
10992
|
+
return true;
|
|
10993
|
+
}
|
|
10994
|
+
}
|
|
10995
|
+
if (current.type === import_utils57.AST_NODE_TYPES.FunctionDeclaration || current.type === import_utils57.AST_NODE_TYPES.FunctionExpression || current.type === import_utils57.AST_NODE_TYPES.ArrowFunctionExpression) {
|
|
10996
|
+
return false;
|
|
10997
|
+
}
|
|
10998
|
+
}
|
|
10999
|
+
return false;
|
|
11000
|
+
};
|
|
11001
|
+
var memberValidationPolarity = (test, access) => {
|
|
11002
|
+
if (test.type === import_utils57.AST_NODE_TYPES.UnaryExpression && test.operator === "!") {
|
|
11003
|
+
const inner = memberValidationPolarity(test.argument, access);
|
|
11004
|
+
return inner === "valid-when-true" ? "valid-when-false" : inner === "valid-when-false" ? "valid-when-true" : null;
|
|
11005
|
+
}
|
|
11006
|
+
if (test.type === import_utils57.AST_NODE_TYPES.BinaryExpression) {
|
|
11007
|
+
const isMatchingTypeof = (node) => node.type === import_utils57.AST_NODE_TYPES.UnaryExpression && node.operator === "typeof" && isSamePlainMember(node.argument, access);
|
|
11008
|
+
const isPrimitiveType = (node) => node.type === import_utils57.AST_NODE_TYPES.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
|
|
11009
|
+
if (!(isMatchingTypeof(test.left) && isPrimitiveType(test.right) || isMatchingTypeof(test.right) && isPrimitiveType(test.left))) {
|
|
11010
|
+
return null;
|
|
11011
|
+
}
|
|
11012
|
+
if (test.operator === "===" || test.operator === "==") {
|
|
11013
|
+
return "valid-when-true";
|
|
11014
|
+
}
|
|
11015
|
+
return test.operator === "!==" || test.operator === "!=" ? "valid-when-false" : null;
|
|
11016
|
+
}
|
|
11017
|
+
return test.type === import_utils57.AST_NODE_TYPES.CallExpression && test.arguments.length === 1 && test.arguments[0] !== void 0 && test.arguments[0].type !== import_utils57.AST_NODE_TYPES.SpreadElement && isSamePlainMember(test.arguments[0], access) && test.callee.type === import_utils57.AST_NODE_TYPES.MemberExpression && !test.callee.computed && test.callee.object.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.object.name === "Array" && test.callee.property.type === import_utils57.AST_NODE_TYPES.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
|
|
11018
|
+
};
|
|
10930
11019
|
var isFullyValidatedExtractedBinding = (member, source, context) => {
|
|
10931
11020
|
const isValidationReference = (identifier) => {
|
|
10932
11021
|
for (let current = identifier.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
@@ -11196,7 +11285,14 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
11196
11285
|
return;
|
|
11197
11286
|
}
|
|
11198
11287
|
const variable = obj?.type === import_utils57.AST_NODE_TYPES.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
11199
|
-
if (variable !== null) {
|
|
11288
|
+
if (variable !== null && obj?.type === import_utils57.AST_NODE_TYPES.Identifier) {
|
|
11289
|
+
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
11290
|
+
return;
|
|
11291
|
+
}
|
|
11292
|
+
const access = plainMemberAccess(node);
|
|
11293
|
+
if (access !== null && isMemberUseWithinValidatedBranch(node, access)) {
|
|
11294
|
+
return;
|
|
11295
|
+
}
|
|
11200
11296
|
if (isFullyValidatedExtractedBinding(node, variable, context)) {
|
|
11201
11297
|
return;
|
|
11202
11298
|
}
|
|
@@ -11648,16 +11744,17 @@ var preferServerActionsDocumentation = {
|
|
|
11648
11744
|
rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
|
|
11649
11745
|
remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
|
|
11650
11746
|
category: "architecture",
|
|
11651
|
-
limitations: ["Only statically recognizable /api/ mutations in
|
|
11747
|
+
limitations: ["Only statically recognizable /api/ mutations in modules with positive Next.js evidence are reported: an explicit next import, or an app/pages path with a top-level use-client directive."],
|
|
11652
11748
|
examples: [
|
|
11653
11749
|
{ id: "server-action-call", title: "Call a Server Action", outcome: "no-match", files: [{ path: "app/tasks/page.tsx", source: "import { createTask } from './actions'; await createTask(input);" }], focusPath: "app/tasks/page.tsx", expectedCount: 0, public: true },
|
|
11654
|
-
{ id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
|
|
11750
|
+
{ id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "'use client'; await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
|
|
11655
11751
|
]
|
|
11656
11752
|
};
|
|
11657
11753
|
var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
11658
11754
|
var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
11659
11755
|
var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
11660
11756
|
var NON_REACT_FRAMEWORK_RE2 = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
|
|
11757
|
+
var NEXT_MODULE_PATH_RE2 = /(?:^|[/\\])(?:app|pages)[/\\]/u;
|
|
11661
11758
|
function getScope(context, node) {
|
|
11662
11759
|
return context.sourceCode.getScope(node);
|
|
11663
11760
|
}
|
|
@@ -11771,6 +11868,16 @@ var prefer_server_actions_default = createRule({
|
|
|
11771
11868
|
const isNonReactFramework = context.sourceCode.ast.body.some(
|
|
11772
11869
|
(node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && NON_REACT_FRAMEWORK_RE2.test(node.source.value)
|
|
11773
11870
|
);
|
|
11871
|
+
const hasUseClientDirective = context.sourceCode.ast.body.some(
|
|
11872
|
+
(node) => node.type === "ExpressionStatement" && node.expression.type === "Literal" && node.expression.value === "use client"
|
|
11873
|
+
);
|
|
11874
|
+
const hasNextImport = context.sourceCode.ast.body.some(
|
|
11875
|
+
(node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "next" || node.source.value.startsWith("next/"))
|
|
11876
|
+
);
|
|
11877
|
+
const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE2.test(filename);
|
|
11878
|
+
if (!hasNextEvidence) {
|
|
11879
|
+
return {};
|
|
11880
|
+
}
|
|
11774
11881
|
return {
|
|
11775
11882
|
CallExpression(node) {
|
|
11776
11883
|
if (isNonReactFramework) return;
|
|
@@ -14338,7 +14445,7 @@ var rules = {
|
|
|
14338
14445
|
};
|
|
14339
14446
|
var meta = {
|
|
14340
14447
|
name: "@sarj/eslint-plugin",
|
|
14341
|
-
version: "15.
|
|
14448
|
+
version: "15.4.0"
|
|
14342
14449
|
};
|
|
14343
14450
|
var applicationOnlyRules = [
|
|
14344
14451
|
"no-restricted-library-load",
|
package/dist/index.d.cts
CHANGED
|
@@ -25,6 +25,7 @@ interface ExampleFile {
|
|
|
25
25
|
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
26
26
|
interface RuleExample {
|
|
27
27
|
readonly id: string;
|
|
28
|
+
readonly scenarioId?: string;
|
|
28
29
|
readonly title: string;
|
|
29
30
|
readonly outcome: ExampleOutcome;
|
|
30
31
|
readonly files: readonly ExampleFile[];
|
|
@@ -414,7 +415,7 @@ type FlatPreset = {
|
|
|
414
415
|
declare const plugin: {
|
|
415
416
|
readonly meta: {
|
|
416
417
|
readonly name: "@sarj/eslint-plugin";
|
|
417
|
-
readonly version: "15.
|
|
418
|
+
readonly version: "15.4.0";
|
|
418
419
|
};
|
|
419
420
|
readonly rules: {
|
|
420
421
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ interface ExampleFile {
|
|
|
25
25
|
/** A reviewed example. It remains private unless `public: true` is explicit. */
|
|
26
26
|
interface RuleExample {
|
|
27
27
|
readonly id: string;
|
|
28
|
+
readonly scenarioId?: string;
|
|
28
29
|
readonly title: string;
|
|
29
30
|
readonly outcome: ExampleOutcome;
|
|
30
31
|
readonly files: readonly ExampleFile[];
|
|
@@ -414,7 +415,7 @@ type FlatPreset = {
|
|
|
414
415
|
declare const plugin: {
|
|
415
416
|
readonly meta: {
|
|
416
417
|
readonly name: "@sarj/eslint-plugin";
|
|
417
|
-
readonly version: "15.
|
|
418
|
+
readonly version: "15.4.0";
|
|
418
419
|
};
|
|
419
420
|
readonly rules: {
|
|
420
421
|
readonly "duplicate-test-body": DocumentedRule<readonly [], "duplicateTestBody">;
|
package/dist/index.js
CHANGED
|
@@ -61,6 +61,7 @@ ${missing.join("\n")}`);
|
|
|
61
61
|
function publicExample(example) {
|
|
62
62
|
return {
|
|
63
63
|
id: example.id,
|
|
64
|
+
scenarioId: example.scenarioId ?? "primary",
|
|
64
65
|
title: example.title,
|
|
65
66
|
outcome: example.outcome,
|
|
66
67
|
files: example.files.map(publicFile),
|
|
@@ -105,9 +106,14 @@ function nativeSpec(config, documentation) {
|
|
|
105
106
|
const examples = [...documentation.examples ?? []];
|
|
106
107
|
examples.forEach(validateExample);
|
|
107
108
|
assertUnique(examples.map((example) => example.id), "rule example IDs");
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
109
|
+
const publicExamples = examples.filter((example) => example.public === true);
|
|
110
|
+
const scenarios = new Set(publicExamples.map((example) => example.scenarioId ?? "primary"));
|
|
111
|
+
for (const scenario of scenarios) {
|
|
112
|
+
const pair = publicExamples.filter((example) => (example.scenarioId ?? "primary") === scenario);
|
|
113
|
+
const outcomes = new Set(pair.map((example) => example.outcome));
|
|
114
|
+
if (pair.length !== 2 || !outcomes.has("match") || !outcomes.has("no-match")) {
|
|
115
|
+
throw new TypeError(`published example scenario ${scenario} must contain both matching and non-matching cases exactly once`);
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
const messageIds = Object.keys(meta2.messages).sort();
|
|
113
119
|
const schema = optionsSchema(meta2.schema);
|
|
@@ -128,7 +134,7 @@ function nativeSpec(config, documentation) {
|
|
|
128
134
|
references,
|
|
129
135
|
since: documentation.since ?? null,
|
|
130
136
|
examples,
|
|
131
|
-
publicExamples
|
|
137
|
+
publicExamples,
|
|
132
138
|
messageIds,
|
|
133
139
|
optionsSchema: schema
|
|
134
140
|
};
|
|
@@ -148,6 +154,9 @@ function validateExample(example) {
|
|
|
148
154
|
if (!KEBAB_CASE.test(example.id)) {
|
|
149
155
|
throw new TypeError("example ID must be lowercase kebab-case");
|
|
150
156
|
}
|
|
157
|
+
if (!KEBAB_CASE.test(example.scenarioId ?? "primary")) {
|
|
158
|
+
throw new TypeError("example scenario must be lowercase kebab-case");
|
|
159
|
+
}
|
|
151
160
|
if (example.title.trim().length === 0) {
|
|
152
161
|
throw new TypeError("example title must not be empty");
|
|
153
162
|
}
|
|
@@ -3224,14 +3233,12 @@ var noJsonStringifyErrorDocumentation = {
|
|
|
3224
3233
|
rationale: "Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.",
|
|
3225
3234
|
remediation: "Serialize explicit error fields or use an error-aware serializer.",
|
|
3226
3235
|
category: "correctness",
|
|
3227
|
-
limitations: ["The rule uses local
|
|
3236
|
+
limitations: ["The rule uses local catch-binding and constructor provenance rather than type information."],
|
|
3228
3237
|
examples: [
|
|
3229
3238
|
{ id: "explicit-error-message", title: "Serialize an enumerable error field", outcome: "no-match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err.message }); }" }], focusPath: "src/report.ts", expectedCount: 0, public: true },
|
|
3230
3239
|
{ id: "stringified-error", title: "Do not stringify an Error object", outcome: "match", files: [{ path: "src/report.ts", source: "try { f(); } catch (err) { JSON.stringify({ error: err }); }" }], focusPath: "src/report.ts", expectedCount: 1, public: true }
|
|
3231
3240
|
]
|
|
3232
3241
|
};
|
|
3233
|
-
var ERROR_NAME_PATTERN = /^(e|err|error|ex|exc)$/i;
|
|
3234
|
-
var ERROR_PROP_PATTERN = /^(cause|lastError|error|err|exception|originalError|innerError)$/i;
|
|
3235
3242
|
var SAFE_STRING_PROPS = /* @__PURE__ */ new Set(["message", "stack", "name"]);
|
|
3236
3243
|
var PAYLOAD_PROPS = /* @__PURE__ */ new Set([
|
|
3237
3244
|
"data",
|
|
@@ -3259,6 +3266,21 @@ var BUILTIN_ERROR_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
|
3259
3266
|
"TypeError",
|
|
3260
3267
|
"URIError"
|
|
3261
3268
|
]);
|
|
3269
|
+
function identifierIsProvenError(identifier, scope) {
|
|
3270
|
+
if (isCatchBinding(scope, identifier.name)) return true;
|
|
3271
|
+
let current = scope;
|
|
3272
|
+
while (current !== null && !current.set.has(identifier.name)) {
|
|
3273
|
+
current = current.upper;
|
|
3274
|
+
}
|
|
3275
|
+
const variable = current?.set.get(identifier.name);
|
|
3276
|
+
if (variable === void 0 || variable.defs.length !== 1) return false;
|
|
3277
|
+
const definition = variable.defs[0];
|
|
3278
|
+
if (definition?.type !== "Variable") return false;
|
|
3279
|
+
const initializer = definition.node.init;
|
|
3280
|
+
return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
|
|
3281
|
+
(reference) => !reference.isWrite() || reference.init === true
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3262
3284
|
function isCatchBinding(scope, name) {
|
|
3263
3285
|
let current = scope;
|
|
3264
3286
|
while (current) {
|
|
@@ -3274,22 +3296,6 @@ function isCatchBinding(scope, name) {
|
|
|
3274
3296
|
}
|
|
3275
3297
|
return false;
|
|
3276
3298
|
}
|
|
3277
|
-
function memberSuggestsError(member, scope) {
|
|
3278
|
-
const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
|
|
3279
|
-
if (propName2 !== null && ERROR_PROP_PATTERN.test(propName2)) {
|
|
3280
|
-
return true;
|
|
3281
|
-
}
|
|
3282
|
-
const base = member.object;
|
|
3283
|
-
const baseSuggestsError = base.type === "Identifier" && (ERROR_NAME_PATTERN.test(base.name) || isCatchBinding(scope, base.name));
|
|
3284
|
-
if (baseSuggestsError) {
|
|
3285
|
-
if (propName2 === null) {
|
|
3286
|
-
return true;
|
|
3287
|
-
}
|
|
3288
|
-
const lowered = propName2.toLowerCase();
|
|
3289
|
-
return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
|
|
3290
|
-
}
|
|
3291
|
-
return false;
|
|
3292
|
-
}
|
|
3293
3299
|
function positiveErrorSubject(test) {
|
|
3294
3300
|
return instanceofErrorSubject(test) ?? typeGuardSubject(test);
|
|
3295
3301
|
}
|
|
@@ -3393,27 +3399,25 @@ function directLiteralValues(argument) {
|
|
|
3393
3399
|
}
|
|
3394
3400
|
function expressionSuggestsError(expression, scope) {
|
|
3395
3401
|
if (expression.type === "Identifier") {
|
|
3396
|
-
return
|
|
3402
|
+
return identifierIsProvenError(expression, scope);
|
|
3403
|
+
}
|
|
3404
|
+
if (expression.type === "NewExpression" && expression.callee.type === "Identifier") {
|
|
3405
|
+
return BUILTIN_ERROR_CONSTRUCTORS.has(expression.callee.name);
|
|
3397
3406
|
}
|
|
3398
3407
|
return expression.type === "MemberExpression" && memberSuggestsError(expression, scope);
|
|
3399
3408
|
}
|
|
3400
|
-
function
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
if (definition?.type !== "Variable") return false;
|
|
3411
|
-
const initializer = definition.node.init;
|
|
3412
|
-
return initializer?.type === "NewExpression" && initializer.callee.type === "Identifier" && BUILTIN_ERROR_CONSTRUCTORS.has(initializer.callee.name) && variable.references.every(
|
|
3413
|
-
(reference) => !reference.isWrite() || reference.init === true
|
|
3414
|
-
);
|
|
3409
|
+
function memberSuggestsError(member, scope) {
|
|
3410
|
+
const propName2 = !member.computed && member.property.type === "Identifier" ? member.property.name : null;
|
|
3411
|
+
const base = member.object;
|
|
3412
|
+
const baseSuggestsError = base.type === "Identifier" && identifierIsProvenError(base, scope);
|
|
3413
|
+
if (baseSuggestsError) {
|
|
3414
|
+
if (propName2 === null) {
|
|
3415
|
+
return true;
|
|
3416
|
+
}
|
|
3417
|
+
const lowered = propName2.toLowerCase();
|
|
3418
|
+
return !SAFE_STRING_PROPS.has(lowered) && !PAYLOAD_PROPS.has(lowered);
|
|
3415
3419
|
}
|
|
3416
|
-
return
|
|
3420
|
+
return false;
|
|
3417
3421
|
}
|
|
3418
3422
|
var no_json_stringify_error_default = createRule({
|
|
3419
3423
|
name: "no-json-stringify-error",
|
|
@@ -3440,9 +3444,8 @@ var no_json_stringify_error_default = createRule({
|
|
|
3440
3444
|
return;
|
|
3441
3445
|
}
|
|
3442
3446
|
const scope = context.sourceCode.getScope(firstArg);
|
|
3443
|
-
const isNestedLiteral = firstArg.type === "ObjectExpression" || firstArg.type === "ArrayExpression";
|
|
3444
3447
|
const unsafeValue = directLiteralValues(firstArg).find(
|
|
3445
|
-
(value) =>
|
|
3448
|
+
(value) => expressionSuggestsError(value, scope) && !isGuardedByInstanceofError(node, value, context.sourceCode) && !isNarrowedByEarlyReturn(node, value, context.sourceCode)
|
|
3446
3449
|
);
|
|
3447
3450
|
if (unsafeValue === void 0) {
|
|
3448
3451
|
return;
|
|
@@ -4903,6 +4906,7 @@ var ANALYTICS_SEGMENTS2 = /* @__PURE__ */ new Set([
|
|
|
4903
4906
|
]);
|
|
4904
4907
|
var SERVER_ACTION_SKIP_FILE_RE = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
4905
4908
|
var NON_REACT_FRAMEWORK_RE = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
|
|
4909
|
+
var NEXT_MODULE_PATH_RE = /(?:^|[/\\])(?:app|pages)[/\\]/u;
|
|
4906
4910
|
function isGlobalFetchCall(node, resolvesToGlobal) {
|
|
4907
4911
|
const callee = node.callee;
|
|
4908
4912
|
if (callee.type === "Identifier") {
|
|
@@ -5018,6 +5022,13 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
5018
5022
|
const nonReactFramework = context.sourceCode.ast.body.some(
|
|
5019
5023
|
(statement) => statement.type === AST_NODE_TYPES18.ImportDeclaration && typeof statement.source.value === "string" && NON_REACT_FRAMEWORK_RE.test(statement.source.value)
|
|
5020
5024
|
);
|
|
5025
|
+
const hasUseClientDirective = context.sourceCode.ast.body.some(
|
|
5026
|
+
(statement) => statement.type === AST_NODE_TYPES18.ExpressionStatement && statement.expression.type === AST_NODE_TYPES18.Literal && statement.expression.value === "use client"
|
|
5027
|
+
);
|
|
5028
|
+
const hasNextImport = context.sourceCode.ast.body.some(
|
|
5029
|
+
(statement) => statement.type === AST_NODE_TYPES18.ImportDeclaration && typeof statement.source.value === "string" && (statement.source.value === "next" || statement.source.value.startsWith("next/"))
|
|
5030
|
+
);
|
|
5031
|
+
const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE.test(filename);
|
|
5021
5032
|
function resolvesToGlobal(identifier) {
|
|
5022
5033
|
const variable = ASTUtils5.findVariable(
|
|
5023
5034
|
context.sourceCode.getScope(identifier),
|
|
@@ -5078,7 +5089,7 @@ var no_raw_fetch_outside_clients_default = createRule({
|
|
|
5078
5089
|
return resolved?.type === AST_NODE_TYPES18.LogicalExpression && resolved.operator === "||" && (isMutationMethod2(resolved.left) || isMutationMethod2(resolved.right));
|
|
5079
5090
|
}
|
|
5080
5091
|
function serverActionOwns(node) {
|
|
5081
|
-
if (node.callee.type !== AST_NODE_TYPES18.Identifier || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
|
|
5092
|
+
if (node.callee.type !== AST_NODE_TYPES18.Identifier || !hasNextEvidence || SERVER_ACTION_SKIP_FILE_RE.test(filename) || nonReactFramework) {
|
|
5082
5093
|
return false;
|
|
5083
5094
|
}
|
|
5084
5095
|
const url = node.arguments[0];
|
|
@@ -6297,6 +6308,20 @@ var SAFE_PARSE_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
|
6297
6308
|
function isBodyDecodeNode(node) {
|
|
6298
6309
|
return node.type === AST_NODE_TYPES23.CallExpression && node.callee.type === AST_NODE_TYPES23.MemberExpression && !node.callee.computed && node.callee.property.type === AST_NODE_TYPES23.Identifier && BODY_DECODE_METHODS.has(node.callee.property.name);
|
|
6299
6310
|
}
|
|
6311
|
+
function isSafeParseSupportCall(node) {
|
|
6312
|
+
const callee = node.callee;
|
|
6313
|
+
if (callee.type !== AST_NODE_TYPES23.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES23.Identifier) {
|
|
6314
|
+
return false;
|
|
6315
|
+
}
|
|
6316
|
+
if (callee.property.name === "isArray" && callee.object.type === AST_NODE_TYPES23.Identifier && callee.object.name === "Array") {
|
|
6317
|
+
return true;
|
|
6318
|
+
}
|
|
6319
|
+
if (callee.property.name !== "getItem") return false;
|
|
6320
|
+
if (callee.object.type === AST_NODE_TYPES23.Identifier && (callee.object.name === "localStorage" || callee.object.name === "sessionStorage")) {
|
|
6321
|
+
return true;
|
|
6322
|
+
}
|
|
6323
|
+
return callee.object.type === AST_NODE_TYPES23.MemberExpression && !callee.object.computed && callee.object.object.type === AST_NODE_TYPES23.Identifier && (callee.object.object.name === "window" || callee.object.object.name === "globalThis") && callee.object.property.type === AST_NODE_TYPES23.Identifier && (callee.object.property.name === "localStorage" || callee.object.property.name === "sessionStorage");
|
|
6324
|
+
}
|
|
6300
6325
|
var BODY_DECODE_METHODS = /* @__PURE__ */ new Set([
|
|
6301
6326
|
"json",
|
|
6302
6327
|
"text",
|
|
@@ -6365,6 +6390,7 @@ function tryReturnsSafeParse(catchNode) {
|
|
|
6365
6390
|
if (current.type === AST_NODE_TYPES23.CallExpression || current.type === AST_NODE_TYPES23.NewExpression) {
|
|
6366
6391
|
if (isParseShapedNode(current) || isBodyDecodeNode(current)) {
|
|
6367
6392
|
sawSafeParse = true;
|
|
6393
|
+
} else if (current.type === AST_NODE_TYPES23.CallExpression && isSafeParseSupportCall(current)) {
|
|
6368
6394
|
} else {
|
|
6369
6395
|
sawUnsafeOperation = true;
|
|
6370
6396
|
return;
|
|
@@ -10895,7 +10921,70 @@ var bindingValidationPolarity = (test, bindingName) => {
|
|
|
10895
10921
|
}
|
|
10896
10922
|
return test.type === AST_NODE_TYPES45.CallExpression && test.arguments.length === 1 && test.arguments[0]?.type === AST_NODE_TYPES45.Identifier && test.arguments[0].name === bindingName && test.callee.type === AST_NODE_TYPES45.MemberExpression && !test.callee.computed && test.callee.object.type === AST_NODE_TYPES45.Identifier && test.callee.object.name === "Array" && test.callee.property.type === AST_NODE_TYPES45.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
|
|
10897
10923
|
};
|
|
10924
|
+
var plainMemberAccess = (node) => node.type === AST_NODE_TYPES45.MemberExpression && !node.computed && node.object.type === AST_NODE_TYPES45.Identifier && node.property.type === AST_NODE_TYPES45.Identifier ? { object: node.object.name, property: node.property.name } : null;
|
|
10925
|
+
var isSamePlainMember = (node, access) => {
|
|
10926
|
+
const candidate = plainMemberAccess(node);
|
|
10927
|
+
return candidate !== null && candidate.object === access.object && candidate.property === access.property;
|
|
10928
|
+
};
|
|
10898
10929
|
var nodeWithin2 = (node, container) => node.range[0] >= container.range[0] && node.range[1] <= container.range[1];
|
|
10930
|
+
var isUseWithinValidatedBranch = (node, bindingName) => {
|
|
10931
|
+
for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
10932
|
+
if (current.type === AST_NODE_TYPES45.ConditionalExpression) {
|
|
10933
|
+
const polarity = bindingValidationPolarity(current.test, bindingName);
|
|
10934
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
|
|
10935
|
+
return true;
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
if (current.type === AST_NODE_TYPES45.IfStatement) {
|
|
10939
|
+
const polarity = bindingValidationPolarity(current.test, bindingName);
|
|
10940
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
|
|
10941
|
+
return true;
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10944
|
+
if (current.type === AST_NODE_TYPES45.FunctionDeclaration || current.type === AST_NODE_TYPES45.FunctionExpression || current.type === AST_NODE_TYPES45.ArrowFunctionExpression) {
|
|
10945
|
+
return false;
|
|
10946
|
+
}
|
|
10947
|
+
}
|
|
10948
|
+
return false;
|
|
10949
|
+
};
|
|
10950
|
+
var isMemberUseWithinValidatedBranch = (node, access) => {
|
|
10951
|
+
for (let current = node.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
10952
|
+
if (current.type === AST_NODE_TYPES45.ConditionalExpression) {
|
|
10953
|
+
const polarity = memberValidationPolarity(current.test, access);
|
|
10954
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && nodeWithin2(node, current.alternate)) {
|
|
10955
|
+
return true;
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
if (current.type === AST_NODE_TYPES45.IfStatement) {
|
|
10959
|
+
const polarity = memberValidationPolarity(current.test, access);
|
|
10960
|
+
if (polarity === "valid-when-true" && nodeWithin2(node, current.consequent) || polarity === "valid-when-false" && current.alternate !== null && nodeWithin2(node, current.alternate)) {
|
|
10961
|
+
return true;
|
|
10962
|
+
}
|
|
10963
|
+
}
|
|
10964
|
+
if (current.type === AST_NODE_TYPES45.FunctionDeclaration || current.type === AST_NODE_TYPES45.FunctionExpression || current.type === AST_NODE_TYPES45.ArrowFunctionExpression) {
|
|
10965
|
+
return false;
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
return false;
|
|
10969
|
+
};
|
|
10970
|
+
var memberValidationPolarity = (test, access) => {
|
|
10971
|
+
if (test.type === AST_NODE_TYPES45.UnaryExpression && test.operator === "!") {
|
|
10972
|
+
const inner = memberValidationPolarity(test.argument, access);
|
|
10973
|
+
return inner === "valid-when-true" ? "valid-when-false" : inner === "valid-when-false" ? "valid-when-true" : null;
|
|
10974
|
+
}
|
|
10975
|
+
if (test.type === AST_NODE_TYPES45.BinaryExpression) {
|
|
10976
|
+
const isMatchingTypeof = (node) => node.type === AST_NODE_TYPES45.UnaryExpression && node.operator === "typeof" && isSamePlainMember(node.argument, access);
|
|
10977
|
+
const isPrimitiveType = (node) => node.type === AST_NODE_TYPES45.Literal && typeof node.value === "string" && PRIMITIVE_TYPEOF_RESULTS.has(node.value);
|
|
10978
|
+
if (!(isMatchingTypeof(test.left) && isPrimitiveType(test.right) || isMatchingTypeof(test.right) && isPrimitiveType(test.left))) {
|
|
10979
|
+
return null;
|
|
10980
|
+
}
|
|
10981
|
+
if (test.operator === "===" || test.operator === "==") {
|
|
10982
|
+
return "valid-when-true";
|
|
10983
|
+
}
|
|
10984
|
+
return test.operator === "!==" || test.operator === "!=" ? "valid-when-false" : null;
|
|
10985
|
+
}
|
|
10986
|
+
return test.type === AST_NODE_TYPES45.CallExpression && test.arguments.length === 1 && test.arguments[0] !== void 0 && test.arguments[0].type !== AST_NODE_TYPES45.SpreadElement && isSamePlainMember(test.arguments[0], access) && test.callee.type === AST_NODE_TYPES45.MemberExpression && !test.callee.computed && test.callee.object.type === AST_NODE_TYPES45.Identifier && test.callee.object.name === "Array" && test.callee.property.type === AST_NODE_TYPES45.Identifier && test.callee.property.name === "isArray" ? "valid-when-true" : null;
|
|
10987
|
+
};
|
|
10899
10988
|
var isFullyValidatedExtractedBinding = (member, source, context) => {
|
|
10900
10989
|
const isValidationReference = (identifier) => {
|
|
10901
10990
|
for (let current = identifier.parent; current !== void 0 && current !== null; current = current.parent) {
|
|
@@ -11165,7 +11254,14 @@ var prefer_schema_for_api_payload_default = createRule({
|
|
|
11165
11254
|
return;
|
|
11166
11255
|
}
|
|
11167
11256
|
const variable = obj?.type === AST_NODE_TYPES45.Identifier ? unvalidatedVariableRef(obj, scope, unvalidatedVariables) : null;
|
|
11168
|
-
if (variable !== null) {
|
|
11257
|
+
if (variable !== null && obj?.type === AST_NODE_TYPES45.Identifier) {
|
|
11258
|
+
if (isUseWithinValidatedBranch(node, obj.name)) {
|
|
11259
|
+
return;
|
|
11260
|
+
}
|
|
11261
|
+
const access = plainMemberAccess(node);
|
|
11262
|
+
if (access !== null && isMemberUseWithinValidatedBranch(node, access)) {
|
|
11263
|
+
return;
|
|
11264
|
+
}
|
|
11169
11265
|
if (isFullyValidatedExtractedBinding(node, variable, context)) {
|
|
11170
11266
|
return;
|
|
11171
11267
|
}
|
|
@@ -11617,16 +11713,17 @@ var preferServerActionsDocumentation = {
|
|
|
11617
11713
|
rationale: "Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.",
|
|
11618
11714
|
remediation: "Move the mutation into a Server Action and invoke that action from the React client.",
|
|
11619
11715
|
category: "architecture",
|
|
11620
|
-
limitations: ["Only statically recognizable /api/ mutations in
|
|
11716
|
+
limitations: ["Only statically recognizable /api/ mutations in modules with positive Next.js evidence are reported: an explicit next import, or an app/pages path with a top-level use-client directive."],
|
|
11621
11717
|
examples: [
|
|
11622
11718
|
{ id: "server-action-call", title: "Call a Server Action", outcome: "no-match", files: [{ path: "app/tasks/page.tsx", source: "import { createTask } from './actions'; await createTask(input);" }], focusPath: "app/tasks/page.tsx", expectedCount: 0, public: true },
|
|
11623
|
-
{ id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
|
|
11719
|
+
{ id: "api-mutation", title: "Do not mutate through an API route", outcome: "match", files: [{ path: "app/tasks/page.tsx", source: "'use client'; await fetch('/api/tasks', { method: 'POST', body });" }], focusPath: "app/tasks/page.tsx", expectedCount: 1, public: true }
|
|
11624
11720
|
]
|
|
11625
11721
|
};
|
|
11626
11722
|
var MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
11627
11723
|
var AXIOS_MUTATION_METHODS = /* @__PURE__ */ new Set(["post", "put", "delete", "patch"]);
|
|
11628
11724
|
var SKIP_FILE_REGEX = /(?:\.test\.[jt]sx?$|\.spec\.[jt]sx?$|-(?:test|spec)\.[jt]sx?$|\/tests?\/|\/__tests__\/|\/__testfixtures__\/|\/scripts?\/|\/app\/api\/.*\/route\.[jt]sx?$|\/pages\/api\/)/;
|
|
11629
11725
|
var NON_REACT_FRAMEWORK_RE2 = /^(?:@angular\/|@nestjs\/|vue$|vue\/|svelte$|svelte\/|solid-js$|solid-js\/|@ember\/|rxjs$|rxjs\/)/;
|
|
11726
|
+
var NEXT_MODULE_PATH_RE2 = /(?:^|[/\\])(?:app|pages)[/\\]/u;
|
|
11630
11727
|
function getScope(context, node) {
|
|
11631
11728
|
return context.sourceCode.getScope(node);
|
|
11632
11729
|
}
|
|
@@ -11740,6 +11837,16 @@ var prefer_server_actions_default = createRule({
|
|
|
11740
11837
|
const isNonReactFramework = context.sourceCode.ast.body.some(
|
|
11741
11838
|
(node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && NON_REACT_FRAMEWORK_RE2.test(node.source.value)
|
|
11742
11839
|
);
|
|
11840
|
+
const hasUseClientDirective = context.sourceCode.ast.body.some(
|
|
11841
|
+
(node) => node.type === "ExpressionStatement" && node.expression.type === "Literal" && node.expression.value === "use client"
|
|
11842
|
+
);
|
|
11843
|
+
const hasNextImport = context.sourceCode.ast.body.some(
|
|
11844
|
+
(node) => node.type === "ImportDeclaration" && typeof node.source.value === "string" && (node.source.value === "next" || node.source.value.startsWith("next/"))
|
|
11845
|
+
);
|
|
11846
|
+
const hasNextEvidence = hasNextImport || hasUseClientDirective && NEXT_MODULE_PATH_RE2.test(filename);
|
|
11847
|
+
if (!hasNextEvidence) {
|
|
11848
|
+
return {};
|
|
11849
|
+
}
|
|
11743
11850
|
return {
|
|
11744
11851
|
CallExpression(node) {
|
|
11745
11852
|
if (isNonReactFramework) return;
|
|
@@ -14316,7 +14423,7 @@ var rules = {
|
|
|
14316
14423
|
};
|
|
14317
14424
|
var meta = {
|
|
14318
14425
|
name: "@sarj/eslint-plugin",
|
|
14319
|
-
version: "15.
|
|
14426
|
+
version: "15.4.0"
|
|
14320
14427
|
};
|
|
14321
14428
|
var applicationOnlyRules = [
|
|
14322
14429
|
"no-restricted-library-load",
|