@yasainet/eslint 0.0.64 → 0.0.66
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/package.json
CHANGED
package/src/common/layers.mjs
CHANGED
|
@@ -94,7 +94,7 @@ export function createLayersConfigs(featureRoot, { typeAware = true } = {}) {
|
|
|
94
94
|
// into their public API. Uses type-aware inspection of the inferred
|
|
95
95
|
// return type so unannotated functions are still checked.
|
|
96
96
|
...(typeAware ? [noAnyReturnConfig] : []),
|
|
97
|
-
// Services: try-catch + logger + dead error fallbacks
|
|
97
|
+
// Services: try-catch + logger + throw + dead error fallbacks
|
|
98
98
|
{
|
|
99
99
|
name: "layers/services",
|
|
100
100
|
files: [`${featureRoot}/**/services/*.ts`],
|
|
@@ -106,6 +106,11 @@ export function createLayersConfigs(featureRoot, { typeAware = true } = {}) {
|
|
|
106
106
|
message:
|
|
107
107
|
"try-catch is not allowed in services. Error handling belongs in entries.",
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
selector: "ThrowStatement",
|
|
111
|
+
message:
|
|
112
|
+
"throw is not allowed in services. Communicate failures via T | null / { data, error } / empty default. Native exceptions from libs auto-propagate to entry's catch.",
|
|
113
|
+
},
|
|
109
114
|
{ selector: loggerSelector, message: loggerMessage },
|
|
110
115
|
{
|
|
111
116
|
selector:
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Enforce the canonical entry template for `**\/entries/*.ts` exports
|
|
2
|
+
* Enforce the canonical entry template for `**\/entries/*.ts` exports.
|
|
3
|
+
*
|
|
4
|
+
* Two body shapes are accepted:
|
|
5
|
+
*
|
|
6
|
+
* - **Pattern A** (read / mutation entries): body is a single try/catch.
|
|
7
|
+
* - **Pattern B** (redirect entries): body contains exactly one try/catch and
|
|
8
|
+
* ends with a terminal Next.js navigation call (`redirect`, `notFound`,
|
|
9
|
+
* `permanentRedirect`) — placed *outside* try/catch per the Next.js docs,
|
|
10
|
+
* since these helpers throw `NEXT_REDIRECT` / `NEXT_NOT_FOUND` and must not
|
|
11
|
+
* be intercepted by the entry's own catch. Pattern B does not require a
|
|
12
|
+
* `return { data, error: null }` in the try block (success is the redirect).
|
|
13
|
+
*
|
|
14
|
+
* Both patterns share the same try/catch contract:
|
|
3
15
|
*
|
|
4
|
-
* - body must be a single try/catch
|
|
5
16
|
* - try first statement: `logger.info(<obj>, "Start <funcName>")`
|
|
6
17
|
* - try success return preceded by: `logger.info(<obj>, "Success <funcName>")`
|
|
18
|
+
* (Pattern A only — Pattern B's success path terminates via redirect)
|
|
7
19
|
* - try failed branch (when present): `logger.error(<obj>, "Failed <funcName>")`
|
|
8
20
|
* followed by a return with the proper error shape
|
|
9
21
|
* - catch param: `error: unknown`
|
|
@@ -18,13 +30,49 @@
|
|
|
18
30
|
*/
|
|
19
31
|
|
|
20
32
|
const CATCH_RETURN_MESSAGE = "An unexpected error occurred";
|
|
33
|
+
const TERMINAL_CALLEES = new Set([
|
|
34
|
+
"redirect",
|
|
35
|
+
"permanentRedirect",
|
|
36
|
+
"notFound",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Param identifier names that are excluded from log propagation. These hold
|
|
41
|
+
* secrets that must never be written to logs (Vercel logs are forwarded to
|
|
42
|
+
* external drains, so treating them as sensitive is the conservative default).
|
|
43
|
+
*/
|
|
44
|
+
const REDACT_PARAM_NAMES = new Set([
|
|
45
|
+
"password",
|
|
46
|
+
"newPassword",
|
|
47
|
+
"currentPassword",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Param TypeScript type names that are excluded from log propagation. Supabase
|
|
52
|
+
* credential types contain `password` as a field; logging the whole param leaks
|
|
53
|
+
* the secret. Listed types are matched on the type annotation's identifier
|
|
54
|
+
* name (no type-info resolution; aliases must match by name).
|
|
55
|
+
*/
|
|
56
|
+
const REDACT_PARAM_TYPES = new Set([
|
|
57
|
+
"SignUpWithPasswordCredentials",
|
|
58
|
+
"SignInWithPasswordCredentials",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
function isRedactedParamType(param) {
|
|
62
|
+
const ann = param.typeAnnotation?.typeAnnotation;
|
|
63
|
+
if (ann?.type !== "TSTypeReference") return false;
|
|
64
|
+
const name = ann.typeName;
|
|
65
|
+
if (name?.type !== "Identifier") return false;
|
|
66
|
+
return REDACT_PARAM_TYPES.has(name.name);
|
|
67
|
+
}
|
|
21
68
|
|
|
22
69
|
function getInputArgNames(params) {
|
|
23
70
|
const names = [];
|
|
24
71
|
for (const p of params) {
|
|
25
|
-
if (p.type
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
if (p.type !== "Identifier") continue;
|
|
73
|
+
if (REDACT_PARAM_NAMES.has(p.name)) continue;
|
|
74
|
+
if (isRedactedParamType(p)) continue;
|
|
75
|
+
names.push(p.name);
|
|
28
76
|
}
|
|
29
77
|
return names;
|
|
30
78
|
}
|
|
@@ -192,7 +240,76 @@ function getReturnErrorMessageLiteral(ret) {
|
|
|
192
240
|
return null;
|
|
193
241
|
}
|
|
194
242
|
|
|
195
|
-
function
|
|
243
|
+
function isTerminalCallExpression(node) {
|
|
244
|
+
if (node?.type !== "CallExpression") return false;
|
|
245
|
+
const callee = node.callee;
|
|
246
|
+
return callee.type === "Identifier" && TERMINAL_CALLEES.has(callee.name);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isTerminalStatement(stmt) {
|
|
250
|
+
if (!stmt) return false;
|
|
251
|
+
if (
|
|
252
|
+
stmt.type === "ExpressionStatement" &&
|
|
253
|
+
isTerminalCallExpression(stmt.expression)
|
|
254
|
+
) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
if (
|
|
258
|
+
stmt.type === "ReturnStatement" &&
|
|
259
|
+
isTerminalCallExpression(stmt.argument)
|
|
260
|
+
) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function endsWithTerminal(node) {
|
|
267
|
+
if (!node) return false;
|
|
268
|
+
if (isTerminalStatement(node)) return true;
|
|
269
|
+
if (node.type === "BlockStatement") {
|
|
270
|
+
return statementsEndWithTerminal(node.body);
|
|
271
|
+
}
|
|
272
|
+
if (node.type === "IfStatement") {
|
|
273
|
+
if (!node.alternate) return false;
|
|
274
|
+
return (
|
|
275
|
+
endsWithTerminal(node.consequent) && endsWithTerminal(node.alternate)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (node.type === "SwitchStatement") {
|
|
279
|
+
if (node.cases.length === 0) return false;
|
|
280
|
+
return node.cases.every((c, i) => caseEndsWithTerminal(c, node.cases, i));
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function caseEndsWithTerminal(switchCase, allCases, idx) {
|
|
286
|
+
// Empty consequent = fallthrough; inherit next case's terminator.
|
|
287
|
+
if (switchCase.consequent.length === 0) {
|
|
288
|
+
const next = allCases[idx + 1];
|
|
289
|
+
if (!next) return false;
|
|
290
|
+
return caseEndsWithTerminal(next, allCases, idx + 1);
|
|
291
|
+
}
|
|
292
|
+
return statementsEndWithTerminal(switchCase.consequent);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function statementsEndWithTerminal(body) {
|
|
296
|
+
const last = body[body.length - 1];
|
|
297
|
+
return endsWithTerminal(last);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function classifyBody(body) {
|
|
301
|
+
const tryStatements = body.filter((s) => s.type === "TryStatement");
|
|
302
|
+
if (tryStatements.length !== 1) return { kind: "invalid" };
|
|
303
|
+
if (body.length === 1 && body[0].type === "TryStatement") {
|
|
304
|
+
return { kind: "A", tryStmt: body[0] };
|
|
305
|
+
}
|
|
306
|
+
if (statementsEndWithTerminal(body)) {
|
|
307
|
+
return { kind: "B", tryStmt: tryStatements[0] };
|
|
308
|
+
}
|
|
309
|
+
return { kind: "invalid" };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function checkTryBlock(context, tryBlock, funcName, inputArgNames, options) {
|
|
196
313
|
if (tryBlock.body.length === 0) {
|
|
197
314
|
context.report({ node: tryBlock, messageId: "tryEmpty", data: { funcName } });
|
|
198
315
|
return;
|
|
@@ -262,7 +379,7 @@ function checkTryBlock(context, tryBlock, funcName, inputArgNames) {
|
|
|
262
379
|
}
|
|
263
380
|
}
|
|
264
381
|
|
|
265
|
-
if (!successFound) {
|
|
382
|
+
if (!successFound && options?.requireSuccessReturn !== false) {
|
|
266
383
|
context.report({
|
|
267
384
|
node: tryBlock,
|
|
268
385
|
messageId: "trySuccessReturnMissing",
|
|
@@ -407,7 +524,7 @@ export const entryTemplateRule = {
|
|
|
407
524
|
},
|
|
408
525
|
messages: {
|
|
409
526
|
bodyNotTryCatch:
|
|
410
|
-
"entry '{{ funcName }}' must
|
|
527
|
+
"entry '{{ funcName }}' body must be either a single try/catch (Pattern A) or a try/catch followed by a terminal navigation call such as `redirect(...)` / `notFound(...)` (Pattern B).",
|
|
411
528
|
tryEmpty: "entry '{{ funcName }}' try block is empty.",
|
|
412
529
|
tryMissingStartLog:
|
|
413
530
|
"entry '{{ funcName }}' try block must start with `logger.info(<obj>, \"Start {{ funcName }}\")`.",
|
|
@@ -451,7 +568,8 @@ export const entryTemplateRule = {
|
|
|
451
568
|
const inputArgNames = getInputArgNames(decl.params);
|
|
452
569
|
|
|
453
570
|
const body = decl.body.body;
|
|
454
|
-
|
|
571
|
+
const classification = classifyBody(body);
|
|
572
|
+
if (classification.kind === "invalid") {
|
|
455
573
|
context.report({
|
|
456
574
|
node: decl.id,
|
|
457
575
|
messageId: "bodyNotTryCatch",
|
|
@@ -459,8 +577,10 @@ export const entryTemplateRule = {
|
|
|
459
577
|
});
|
|
460
578
|
return;
|
|
461
579
|
}
|
|
462
|
-
const tryStmt =
|
|
463
|
-
checkTryBlock(context, tryStmt.block, funcName, inputArgNames
|
|
580
|
+
const tryStmt = classification.tryStmt;
|
|
581
|
+
checkTryBlock(context, tryStmt.block, funcName, inputArgNames, {
|
|
582
|
+
requireSuccessReturn: classification.kind === "A",
|
|
583
|
+
});
|
|
464
584
|
if (tryStmt.handler) {
|
|
465
585
|
checkCatchClause(context, tryStmt.handler, funcName, inputArgNames);
|
|
466
586
|
}
|