@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasainet/eslint",
3
- "version": "0.0.64",
3
+ "version": "0.0.66",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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 === "Identifier") {
26
- names.push(p.name);
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 checkTryBlock(context, tryBlock, funcName, inputArgNames) {
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 have a single try/catch as its body.",
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
- if (body.length !== 1 || body[0].type !== "TryStatement") {
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 = body[0];
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
  }