@yasainet/eslint 0.0.73 → 0.0.75

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.
Files changed (76) hide show
  1. package/README.md +9 -152
  2. package/package.json +7 -1
  3. package/src/cli/test-audit.mjs +97 -0
  4. package/src/common/CLAUDE.md +17 -0
  5. package/src/common/{constants.mjs → _internal/constants.mjs} +1 -19
  6. package/src/common/_internal/import-patterns.mjs +16 -0
  7. package/src/common/{plugins.mjs → _internal/plugins.mjs} +0 -1
  8. package/src/common/_internal/selectors.mjs +12 -0
  9. package/src/common/{rules.mjs → base/typescript.mjs} +15 -36
  10. package/src/common/{entry-points.mjs → boundaries/entry-point.mjs} +1 -2
  11. package/src/common/cross-cutting/ban-alias.mjs +22 -0
  12. package/src/common/cross-cutting/feature-default-imports.mjs +26 -0
  13. package/src/common/cross-cutting/feature-name.mjs +15 -0
  14. package/src/common/cross-cutting/features-ts-only.mjs +20 -0
  15. package/src/common/cross-cutting/form-state.mjs +16 -0
  16. package/src/common/{jsdoc.mjs → cross-cutting/jsdoc.mjs} +2 -3
  17. package/src/common/cross-cutting/logger.mjs +21 -0
  18. package/src/common/cross-cutting/namespace-import.mjs +23 -0
  19. package/src/common/cross-cutting/no-any-return.mjs +18 -0
  20. package/src/common/cross-cutting/no-colocated-test.mjs +18 -0
  21. package/src/common/cross-cutting/supabase-columns-satisfies.mjs +18 -0
  22. package/src/common/index.mjs +44 -24
  23. package/src/common/layers/constants.mjs +36 -0
  24. package/src/common/layers/entries.mjs +174 -0
  25. package/src/common/layers/lib.mjs +18 -0
  26. package/src/common/layers/queries.mjs +187 -0
  27. package/src/common/layers/schemas.mjs +50 -0
  28. package/src/common/layers/services.mjs +121 -0
  29. package/src/common/layers/top-level-utils.mjs +18 -0
  30. package/src/common/layers/types.mjs +44 -0
  31. package/src/common/layers/utils.mjs +54 -0
  32. package/src/common/local-plugins/entry-single-service-call.mjs +3 -31
  33. package/src/common/local-plugins/entry-template.mjs +51 -88
  34. package/src/common/local-plugins/feature-name.mjs +5 -24
  35. package/src/common/local-plugins/form-state-naming.mjs +1 -11
  36. package/src/common/local-plugins/form-state-shape.mjs +8 -42
  37. package/src/common/local-plugins/import-path-style.mjs +2 -9
  38. package/src/common/local-plugins/index.mjs +2 -1
  39. package/src/common/local-plugins/layout-main-structural-only.mjs +1 -22
  40. package/src/common/local-plugins/namespace-import-name.mjs +1 -27
  41. package/src/common/local-plugins/no-any-return.mjs +1 -9
  42. package/src/common/local-plugins/no-colocated-test.mjs +26 -0
  43. package/src/common/local-plugins/queries-export.mjs +1 -9
  44. package/src/common/local-plugins/queries-namespace-import.mjs +1 -11
  45. package/src/common/local-plugins/schema-naming.mjs +2 -8
  46. package/src/common/local-plugins/supabase-columns-satisfies.mjs +1 -25
  47. package/src/common/local-plugins/supabase-select-typed-columns.mjs +5 -37
  48. package/src/deno/CLAUDE.md +10 -0
  49. package/src/deno/boundaries/entry-point.mjs +44 -0
  50. package/src/deno/boundaries/lib.mjs +28 -0
  51. package/src/deno/boundaries/utils.mjs +25 -0
  52. package/src/deno/index.mjs +9 -13
  53. package/src/deno/local-plugins/flat-entry-point.mjs +1 -6
  54. package/src/deno/local-plugins/index.mjs +0 -1
  55. package/src/next/CLAUDE.md +14 -0
  56. package/src/next/boundaries/components.mjs +36 -0
  57. package/src/next/boundaries/hooks.mjs +36 -0
  58. package/src/next/boundaries/lib.mjs +23 -0
  59. package/src/next/boundaries/page.mjs +36 -0
  60. package/src/next/boundaries/route.mjs +36 -0
  61. package/src/next/boundaries/sitemap.mjs +36 -0
  62. package/src/next/directives.mjs +4 -5
  63. package/src/next/imports.mjs +0 -1
  64. package/src/next/index.mjs +12 -15
  65. package/src/next/layers/components.mjs +30 -0
  66. package/src/next/layers/hooks.mjs +31 -0
  67. package/src/next/layers/layouts.mjs +12 -0
  68. package/src/next/tailwindcss.mjs +2 -23
  69. package/src/node/CLAUDE.md +7 -0
  70. package/src/node/index.mjs +1 -2
  71. package/src/common/imports.mjs +0 -457
  72. package/src/common/layers.mjs +0 -158
  73. package/src/common/naming.mjs +0 -347
  74. package/src/deno/imports.mjs +0 -90
  75. package/src/next/layouts.mjs +0 -18
  76. package/src/next/naming.mjs +0 -60
@@ -0,0 +1,121 @@
1
+ import { featuresGlob } from "../_internal/constants.mjs";
2
+ import { LIB_BOUNDARY_PATTERNS } from "../_internal/import-patterns.mjs";
3
+ import { checkFile } from "../_internal/plugins.mjs";
4
+ import {
5
+ aliasDynamicImportMessage,
6
+ aliasDynamicImportSelector,
7
+ loggerMessage,
8
+ loggerSelector,
9
+ } from "../_internal/selectors.mjs";
10
+
11
+ const LAYER_PATTERNS = [
12
+ {
13
+ group: ["**/entries/*", "**/entries"],
14
+ message: "services は entries を import 不可。依存は単方向に保つ。",
15
+ },
16
+ {
17
+ group: ["**/hooks/*", "**/hooks"],
18
+ message: "services は hooks を import 不可。依存は単方向に保つ。",
19
+ },
20
+ ];
21
+
22
+ const LATERAL_PATTERNS = [
23
+ {
24
+ group: ["@/features/*/services/*", "@/features/*/services"],
25
+ message:
26
+ "他 feature の services は import 不可。feature を跨ぐ依存は禁止。",
27
+ },
28
+ ];
29
+
30
+ export function createServicesConfigs({ featureRoot, prefixLibMapping }) {
31
+ const prefixes = Object.keys(prefixLibMapping);
32
+ const hasPrefixes = prefixes.length > 0;
33
+ const prefixPattern = hasPrefixes ? `@(${prefixes.join("|")})` : "*";
34
+ const sharedPrefixPattern = hasPrefixes
35
+ ? `@(shared|${prefixes.join("|")})`
36
+ : "shared";
37
+
38
+ return [
39
+ {
40
+ name: "naming/services",
41
+ files: featuresGlob(featureRoot, "**/services/*.ts"),
42
+ ignores: [
43
+ ...featuresGlob(featureRoot, "shared/services/*.ts"),
44
+ "**/*.test.ts",
45
+ ],
46
+ plugins: { "check-file": checkFile },
47
+ rules: {
48
+ "check-file/filename-naming-convention": [
49
+ "error",
50
+ { "**/*.ts": prefixPattern },
51
+ ],
52
+ },
53
+ },
54
+ {
55
+ name: "naming/services-shared",
56
+ files: featuresGlob(featureRoot, "shared/services/*.ts"),
57
+ ignores: ["**/*.test.ts"],
58
+ plugins: { "check-file": checkFile },
59
+ rules: {
60
+ "check-file/filename-naming-convention": [
61
+ "error",
62
+ { "**/*.ts": sharedPrefixPattern },
63
+ ],
64
+ },
65
+ },
66
+ {
67
+ name: "layers/services",
68
+ files: [`${featureRoot}/**/services/*.ts`],
69
+ rules: {
70
+ "no-restricted-syntax": [
71
+ "error",
72
+ {
73
+ selector: "TryStatement",
74
+ message:
75
+ "services で try-catch は禁止。エラー処理は entries に集約する。",
76
+ },
77
+ {
78
+ selector: "ThrowStatement",
79
+ message:
80
+ "services で throw は禁止。失敗は値で返す:\n" +
81
+ "- `T | null` / `{ data, error }` / 空デフォルトのいずれか\n" +
82
+ "- lib の native 例外は entry の catch に自動伝播する",
83
+ },
84
+ { selector: loggerSelector, message: loggerMessage },
85
+ {
86
+ selector:
87
+ "LogicalExpression[operator='??'][left.type='ChainExpression'][left.expression.property.name='message'][right.type='Literal']",
88
+ message:
89
+ "error message の dead fallback。この分岐に来た時点で error は既知 — error をそのまま返す。",
90
+ },
91
+ {
92
+ selector:
93
+ "LogicalExpression[operator='??'][left.type='MemberExpression'][left.property.name='error'][right.type='ObjectExpression']",
94
+ message:
95
+ "nullable error の dead fallback。`if (error)` で判定し error をそのまま返す。",
96
+ },
97
+ {
98
+ selector: aliasDynamicImportSelector,
99
+ message: aliasDynamicImportMessage,
100
+ },
101
+ ],
102
+ },
103
+ },
104
+ {
105
+ name: "imports/services",
106
+ files: [`${featureRoot}/**/services/*.ts`],
107
+ rules: {
108
+ "no-restricted-imports": [
109
+ "error",
110
+ {
111
+ patterns: [
112
+ ...LAYER_PATTERNS,
113
+ ...LATERAL_PATTERNS,
114
+ ...LIB_BOUNDARY_PATTERNS,
115
+ ],
116
+ },
117
+ ],
118
+ },
119
+ },
120
+ ];
121
+ }
@@ -0,0 +1,18 @@
1
+ import { checkFile } from "../_internal/plugins.mjs";
2
+
3
+ export function createTopLevelUtilsConfigs({ featureRoot }) {
4
+ const utilsRoot = featureRoot.replace(/features$/, "utils");
5
+ return [
6
+ {
7
+ name: "naming/top-level-utils",
8
+ files: [`${utilsRoot}/**/*.ts`],
9
+ plugins: { "check-file": checkFile },
10
+ rules: {
11
+ "check-file/filename-naming-convention": [
12
+ "error",
13
+ { "**/*.ts": "*" },
14
+ ],
15
+ },
16
+ },
17
+ ];
18
+ }
@@ -0,0 +1,44 @@
1
+ import { featuresGlob } from "../_internal/constants.mjs";
2
+ import { MAPPING_PATTERNS } from "../_internal/import-patterns.mjs";
3
+ import { checkFile } from "../_internal/plugins.mjs";
4
+
5
+ export function createTypesConfigs({ featureRoot, prefixLibMapping }) {
6
+ const prefixes = Object.keys(prefixLibMapping);
7
+ const hasPrefixes = prefixes.length > 0;
8
+ const sharedPrefixPattern = hasPrefixes
9
+ ? `@(shared|${prefixes.join("|")})`
10
+ : "shared";
11
+
12
+ return [
13
+ {
14
+ name: "naming/types",
15
+ files: featuresGlob(featureRoot, "*/types/*.ts"),
16
+ ignores: featuresGlob(featureRoot, "shared/types/*.ts"),
17
+ plugins: { "check-file": checkFile },
18
+ rules: {
19
+ "check-file/filename-naming-convention": [
20
+ "error",
21
+ { "**/*/types/*.ts": "<1>" },
22
+ ],
23
+ },
24
+ },
25
+ {
26
+ name: "naming/types-shared",
27
+ files: featuresGlob(featureRoot, "shared/types/*.ts"),
28
+ plugins: { "check-file": checkFile },
29
+ rules: {
30
+ "check-file/filename-naming-convention": [
31
+ "error",
32
+ { "**/*.ts": sharedPrefixPattern },
33
+ ],
34
+ },
35
+ },
36
+ {
37
+ name: "imports/feature-types",
38
+ files: [`${featureRoot}/**/types/*.ts`],
39
+ rules: {
40
+ "no-restricted-imports": ["error", { patterns: MAPPING_PATTERNS }],
41
+ },
42
+ },
43
+ ];
44
+ }
@@ -0,0 +1,54 @@
1
+ import { featuresGlob } from "../_internal/constants.mjs";
2
+ import {
3
+ LIB_BOUNDARY_PATTERNS,
4
+ MAPPING_PATTERNS,
5
+ } from "../_internal/import-patterns.mjs";
6
+ import { checkFile } from "../_internal/plugins.mjs";
7
+
8
+ export function createUtilsConfigs({ featureRoot, prefixLibMapping }) {
9
+ const prefixes = Object.keys(prefixLibMapping);
10
+ const hasPrefixes = prefixes.length > 0;
11
+ const sharedPrefixPattern = hasPrefixes
12
+ ? `@(shared|${prefixes.join("|")})`
13
+ : "shared";
14
+
15
+ return [
16
+ {
17
+ name: "naming/utils",
18
+ files: featuresGlob(featureRoot, "*/utils/*.ts"),
19
+ ignores: [
20
+ ...featuresGlob(featureRoot, "shared/utils/*.ts"),
21
+ "**/*.test.ts",
22
+ ],
23
+ plugins: { "check-file": checkFile },
24
+ rules: {
25
+ "check-file/filename-naming-convention": [
26
+ "error",
27
+ { "**/*/utils/*.ts": "<1>" },
28
+ ],
29
+ },
30
+ },
31
+ {
32
+ name: "naming/utils-shared",
33
+ files: featuresGlob(featureRoot, "shared/utils/*.ts"),
34
+ ignores: ["**/*.test.ts"],
35
+ plugins: { "check-file": checkFile },
36
+ rules: {
37
+ "check-file/filename-naming-convention": [
38
+ "error",
39
+ { "**/*.ts": sharedPrefixPattern },
40
+ ],
41
+ },
42
+ },
43
+ {
44
+ name: "imports/utils",
45
+ files: [`${featureRoot}/**/utils/*.ts`],
46
+ rules: {
47
+ "no-restricted-imports": [
48
+ "error",
49
+ { patterns: [...LIB_BOUNDARY_PATTERNS, ...MAPPING_PATTERNS] },
50
+ ],
51
+ },
52
+ },
53
+ ];
54
+ }
@@ -1,33 +1,3 @@
1
- /**
2
- * Enforce 1:1 entry-to-service mapping for `**\/entries/*.ts` exports.
3
- *
4
- * Why: services are the orchestration layer (they may combine multiple queries
5
- * and other features' queries). entries should be a thin wrapper that calls a
6
- * single service function and normalizes the return shape into
7
- * `{ data, error }`. If an entry calls more than one service, orchestration is
8
- * leaking up into the entry layer.
9
- *
10
- * Detection rule:
11
- *
12
- * - For every exported async `FunctionDeclaration` in an entries file, count
13
- * `CallExpression`s whose callee is a `MemberExpression` of the form
14
- * `<binding>.<method>(...)` where `<binding>` matches the namespace import
15
- * naming convention `*Service` (e.g. `articlesServerService`,
16
- * `usersClientService`).
17
- * - More than one such call inside the same exported function is an error.
18
- *
19
- * Exception (C-3):
20
- *
21
- * - Bindings starting with `shared` (e.g. `sharedDiscordService`,
22
- * `sharedResendService`) are EXCLUDED from the count. These represent
23
- * cross-cutting side-effect abstractions (Discord / Resend / Slack
24
- * notifications) that don't fit the entry-service 1:1 model and are allowed
25
- * to be invoked from entries directly.
26
- *
27
- * The rule reports the 2nd and later violations (the 1st call is permitted),
28
- * so the fix surface is the redundant calls.
29
- */
30
-
31
1
  const SERVICE_BINDING_REGEX = /Service$/;
32
2
 
33
3
  function isServiceCall(node) {
@@ -69,7 +39,9 @@ export const entrySingleServiceCallRule = {
69
39
  },
70
40
  messages: {
71
41
  multipleServiceCalls:
72
- "entry '{{ funcName }}' calls more than one feature service ({{ count }} total). entries must be a thin wrapper that calls a single service. Move orchestration into the service layer. `shared/services/*` (e.g. `sharedDiscordService`) is exempt.",
42
+ "entry '{{ funcName }}' が複数の feature service を呼んでいる ({{ count }} ):\n" +
43
+ "- entry は単一 service を呼ぶ薄いラッパー、orchestration は service 層へ移す\n" +
44
+ "- `shared/services/*` (例 `sharedDiscordService`) は例外",
73
45
  },
74
46
  schema: [],
75
47
  },
@@ -1,58 +1,12 @@
1
- /**
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:
15
- *
16
- * - try first statement: `logger.info(<obj>, "Start <funcName>")`
17
- * - try success return preceded by: `logger.info(<obj>, "Success <funcName>")`
18
- * (Pattern A only — Pattern B's success path terminates via redirect)
19
- * - try failed branch (when present): `logger.error(<obj>, "Failed <funcName>")`
20
- * followed by a return with the proper error shape
21
- * - catch param: `error: unknown`
22
- * - catch first statement: `logger.error(<obj>, "Unexpected error in <funcName>")`
23
- * - catch return error.message must be the literal "An unexpected error occurred"
24
- * - every log object must include the `err` key first (when applicable) and
25
- * propagate all function input parameters as values
26
- *
27
- * Why one rule with many messageIds: each invariant is a small rule
28
- * conceptually, but they share the same structural traversal and access to
29
- * funcName / inputArgs. Splitting would duplicate the AST walk.
30
- */
31
-
32
1
  const CATCH_RETURN_MESSAGE = "An unexpected error occurred";
33
- const TERMINAL_CALLEES = new Set([
34
- "redirect",
35
- "permanentRedirect",
36
- "notFound",
37
- ]);
2
+ const TERMINAL_CALLEES = new Set(["redirect", "permanentRedirect", "notFound"]);
38
3
 
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
4
  const REDACT_PARAM_NAMES = new Set([
45
5
  "password",
46
6
  "newPassword",
47
7
  "currentPassword",
48
8
  ]);
49
9
 
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
10
  const REDACT_PARAM_TYPES = new Set([
57
11
  "SignUpWithPasswordCredentials",
58
12
  "SignInWithPasswordCredentials",
@@ -91,7 +45,8 @@ function isLoggerCall(node, level) {
91
45
 
92
46
  function getStringLiteralArg(callExpr, index) {
93
47
  const arg = callExpr.arguments[index];
94
- if (arg?.type === "Literal" && typeof arg.value === "string") return arg.value;
48
+ if (arg?.type === "Literal" && typeof arg.value === "string")
49
+ return arg.value;
95
50
  return null;
96
51
  }
97
52
 
@@ -112,7 +67,10 @@ function objectContainsValue(objExpr, identifierName) {
112
67
  ) {
113
68
  return true;
114
69
  }
115
- if (prop.value.type === "Identifier" && prop.value.name === identifierName) {
70
+ if (
71
+ prop.value.type === "Identifier" &&
72
+ prop.value.name === identifierName
73
+ ) {
116
74
  return true;
117
75
  }
118
76
  }
@@ -124,10 +82,10 @@ function firstPropertyIsErrKey(objExpr, errorIdentifierName) {
124
82
  const first = objExpr.properties[0];
125
83
  if (first.type !== "Property") return false;
126
84
  if (first.key.type !== "Identifier" || first.key.name !== "err") return false;
127
- // value must be the catch error identifier (or any Identifier — we accept both
128
- // `err: error` and `err: result.error` since Failed Pattern C uses MemberExpression)
129
85
  if (first.value.type === "Identifier") {
130
- return errorIdentifierName ? first.value.name === errorIdentifierName : true;
86
+ return errorIdentifierName
87
+ ? first.value.name === errorIdentifierName
88
+ : true;
131
89
  }
132
90
  if (first.value.type === "MemberExpression") return true;
133
91
  return false;
@@ -201,7 +159,6 @@ function checkLogCall({
201
159
  }
202
160
 
203
161
  function isReturnDataErrorNull(ret) {
204
- // `return { data: ..., error: null }` (data shorthand or explicit)
205
162
  const arg = ret.argument;
206
163
  if (arg?.type !== "ObjectExpression") return false;
207
164
  let hasData = false;
@@ -230,8 +187,12 @@ function getReturnErrorMessageLiteral(ret) {
230
187
  if (prop.value.type !== "ObjectExpression") return null;
231
188
  for (const inner of prop.value.properties) {
232
189
  if (inner.type !== "Property") continue;
233
- if (inner.key.type !== "Identifier" || inner.key.name !== "message") continue;
234
- if (inner.value.type === "Literal" && typeof inner.value.value === "string") {
190
+ if (inner.key.type !== "Identifier" || inner.key.name !== "message")
191
+ continue;
192
+ if (
193
+ inner.value.type === "Literal" &&
194
+ typeof inner.value.value === "string"
195
+ ) {
235
196
  return inner.value.value;
236
197
  }
237
198
  return "<non-literal>";
@@ -283,7 +244,6 @@ function endsWithTerminal(node) {
283
244
  }
284
245
 
285
246
  function caseEndsWithTerminal(switchCase, allCases, idx) {
286
- // Empty consequent = fallthrough; inherit next case's terminator.
287
247
  if (switchCase.consequent.length === 0) {
288
248
  const next = allCases[idx + 1];
289
249
  if (!next) return false;
@@ -311,11 +271,14 @@ function classifyBody(body) {
311
271
 
312
272
  function checkTryBlock(context, tryBlock, funcName, inputArgNames, options) {
313
273
  if (tryBlock.body.length === 0) {
314
- context.report({ node: tryBlock, messageId: "tryEmpty", data: { funcName } });
274
+ context.report({
275
+ node: tryBlock,
276
+ messageId: "tryEmpty",
277
+ data: { funcName },
278
+ });
315
279
  return;
316
280
  }
317
281
 
318
- // Start log: first statement
319
282
  const first = tryBlock.body[0];
320
283
  if (!isExpressionStatementWithLoggerCall(first, "info")) {
321
284
  context.report({
@@ -337,16 +300,12 @@ function checkTryBlock(context, tryBlock, funcName, inputArgNames, options) {
337
300
  });
338
301
  }
339
302
 
340
- // Walk body to find Success returns and Failed branches
341
303
  let successFound = false;
342
304
  for (let i = 0; i < tryBlock.body.length; i++) {
343
305
  const stmt = tryBlock.body[i];
344
306
 
345
- // Success log + return
346
307
  if (stmt.type === "ReturnStatement" && isReturnDataErrorNull(stmt)) {
347
308
  successFound = true;
348
- // The previous non-ExpressionStatement non-IfStatement statement should be
349
- // the Success log. Walk back to find it.
350
309
  const prev = findPrecedingLoggerCall(tryBlock.body, i);
351
310
  if (
352
311
  !prev ||
@@ -373,7 +332,6 @@ function checkTryBlock(context, tryBlock, funcName, inputArgNames, options) {
373
332
  }
374
333
  }
375
334
 
376
- // Failed branches inside if statements
377
335
  if (stmt.type === "IfStatement") {
378
336
  checkFailedBranch(context, stmt, funcName, inputArgNames);
379
337
  }
@@ -396,7 +354,6 @@ function findPrecedingLoggerCall(body, returnIndex) {
396
354
  s.type === "ExpressionStatement" &&
397
355
  s.expression.type === "AwaitExpression"
398
356
  ) {
399
- // `await revalidatePath(...)` etc — keep walking
400
357
  continue;
401
358
  }
402
359
  if (s.type === "ExpressionStatement") {
@@ -409,16 +366,13 @@ function findPrecedingLoggerCall(body, returnIndex) {
409
366
  }
410
367
 
411
368
  function checkFailedBranch(context, ifStmt, funcName, inputArgNames) {
412
- // We only validate IFs that look like Failed branches: contain a return whose
413
- // error.message is a string literal (not the catch's "An unexpected ..." literal).
414
369
  const consequent = ifStmt.consequent;
415
370
  if (consequent.type !== "BlockStatement") return;
416
371
  const ret = consequent.body.find((s) => s.type === "ReturnStatement");
417
372
  if (!ret) return;
418
373
  const errMsg = getReturnErrorMessageLiteral(ret);
419
- if (errMsg === null) return; // not a Failed-shaped return; skip
374
+ if (errMsg === null) return;
420
375
 
421
- // Must have logger.error("Failed <funcName>") preceding the return
422
376
  const idx = consequent.body.indexOf(ret);
423
377
  let loggerCall = null;
424
378
  for (let j = 0; j < idx; j++) {
@@ -440,7 +394,7 @@ function checkFailedBranch(context, ifStmt, funcName, inputArgNames) {
440
394
  return;
441
395
  }
442
396
 
443
- const isPatternC = errMsg === "<non-literal>"; // .message access
397
+ const isPatternC = errMsg === "<non-literal>";
444
398
  checkLogCall({
445
399
  context,
446
400
  callExpr: loggerCall,
@@ -471,7 +425,11 @@ function checkCatchClause(context, handler, funcName, inputArgNames) {
471
425
  }
472
426
  const block = handler.body;
473
427
  if (block.body.length === 0) {
474
- context.report({ node: block, messageId: "catchEmpty", data: { funcName } });
428
+ context.report({
429
+ node: block,
430
+ messageId: "catchEmpty",
431
+ data: { funcName },
432
+ });
475
433
  return;
476
434
  }
477
435
  const first = block.body[0];
@@ -495,7 +453,6 @@ function checkCatchClause(context, handler, funcName, inputArgNames) {
495
453
  });
496
454
  }
497
455
 
498
- // Last statement must be a return whose error.message is the catch literal
499
456
  const last = block.body[block.body.length - 1];
500
457
  if (last?.type !== "ReturnStatement") {
501
458
  context.report({
@@ -510,7 +467,11 @@ function checkCatchClause(context, handler, funcName, inputArgNames) {
510
467
  context.report({
511
468
  node: last,
512
469
  messageId: "catchWrongReturnMessage",
513
- data: { funcName, expected: CATCH_RETURN_MESSAGE, actual: msg ?? "<missing>" },
470
+ data: {
471
+ funcName,
472
+ expected: CATCH_RETURN_MESSAGE,
473
+ actual: msg ?? "<missing>",
474
+ },
514
475
  });
515
476
  }
516
477
  }
@@ -524,35 +485,37 @@ export const entryTemplateRule = {
524
485
  },
525
486
  messages: {
526
487
  bodyNotTryCatch:
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).",
528
- tryEmpty: "entry '{{ funcName }}' try block is empty.",
488
+ "entry '{{ funcName }}' body は次のいずれか:\n" +
489
+ "- 単一の try/catch (Pattern A)\n" +
490
+ "- try/catch + 末尾の navigation 呼び出し `redirect(...)` / `notFound(...)` (Pattern B)",
491
+ tryEmpty: "entry '{{ funcName }}' の try block が空。",
529
492
  tryMissingStartLog:
530
- "entry '{{ funcName }}' try block must start with `logger.info(<obj>, \"Start {{ funcName }}\")`.",
493
+ "entry '{{ funcName }}' try block `logger.info(<obj>, \"Start {{ funcName }}\")` で始める。",
531
494
  trySuccessLogMissing:
532
- "entry '{{ funcName }}' success return must be preceded by `logger.info(<obj>, \"Success {{ funcName }}\")`.",
495
+ "entry '{{ funcName }}' success return の直前に `logger.info(<obj>, \"Success {{ funcName }}\")` が必須。",
533
496
  trySuccessReturnMissing:
534
- "entry '{{ funcName }}' must contain a success return `return { data, error: null }`.",
497
+ "entry '{{ funcName }}' success return `return { data, error: null }` が必須。",
535
498
  failedLogMissing:
536
- "entry '{{ funcName }}' Failed branch must call `logger.error(<obj>, \"Failed {{ funcName }}\")` before return.",
499
+ "entry '{{ funcName }}' Failed 分岐は return 前に `logger.error(<obj>, \"Failed {{ funcName }}\")` を呼ぶ。",
537
500
  catchParamWrongType:
538
- "entry '{{ funcName }}' catch param must be `error: unknown`.",
539
- catchEmpty: "entry '{{ funcName }}' catch block is empty.",
501
+ "entry '{{ funcName }}' catch param `error: unknown`。",
502
+ catchEmpty: "entry '{{ funcName }}' catch block が空。",
540
503
  catchMissingErrorLog:
541
- "entry '{{ funcName }}' catch block must start with `logger.error(<obj>, \"Unexpected error in {{ funcName }}\")`.",
504
+ "entry '{{ funcName }}' catch block `logger.error(<obj>, \"Unexpected error in {{ funcName }}\")` で始める。",
542
505
  catchLastNotReturn:
543
- "entry '{{ funcName }}' catch block must end with a return statement.",
506
+ "entry '{{ funcName }}' catch block return 文で終える。",
544
507
  catchWrongReturnMessage:
545
- "entry '{{ funcName }}' catch return error.message must be the literal '{{ expected }}'. Got: '{{ actual }}'.",
508
+ "entry '{{ funcName }}' catch return error.message はリテラル '{{ expected }}'。実際: '{{ actual }}'",
546
509
  logWrongCallShape:
547
- "{{ where }} log in '{{ funcName }}' must be `logger.{{ expectedLevel }}(<obj>, \"{{ expectedMessage }}\")`.",
510
+ "'{{ funcName }}' {{ where }} log `logger.{{ expectedLevel }}(<obj>, \"{{ expectedMessage }}\")`。",
548
511
  logWrongMessage:
549
- "{{ where }} log message must be '{{ expectedMessage }}'. Got: '{{ actual }}'.",
512
+ "{{ where }} log message '{{ expectedMessage }}'。実際: '{{ actual }}'",
550
513
  logFirstArgNotObject:
551
- "{{ where }} log in '{{ funcName }}' first argument must be an object literal.",
514
+ "'{{ funcName }}' {{ where }} log の第1引数は object literal にする。",
552
515
  logErrKeyNotFirst:
553
- "{{ where }} log in '{{ funcName }}' object must start with `err:` key.",
516
+ "'{{ funcName }}' {{ where }} log object `err:` キーで始める。",
554
517
  logMissingInputArg:
555
- "{{ where }} log in '{{ funcName }}' is missing input arg '{{ argName }}'. All function inputs must propagate to log objects.",
518
+ "'{{ funcName }}' {{ where }} log に入力引数 '{{ argName }}' が無い。全ての関数入力を log object に伝播する。",
556
519
  },
557
520
  schema: [],
558
521
  },
@@ -1,11 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
 
4
- /**
5
- * Extract table names from Supabase generated types file.
6
- * Looks for top-level keys under `Tables: {` inside the `public` schema.
7
- * Uses brace counting to handle deeply nested type definitions.
8
- */
9
4
  function extractTableNames(supabaseTypePath) {
10
5
  if (!fs.existsSync(supabaseTypePath)) {
11
6
  return [];
@@ -13,7 +8,6 @@ function extractTableNames(supabaseTypePath) {
13
8
 
14
9
  const content = fs.readFileSync(supabaseTypePath, "utf-8");
15
10
 
16
- // Find `public:` excluding `graphql_public:` via negative lookbehind
17
11
  const publicMatch = /(?<!\w)public:\s*\{/.exec(content);
18
12
  if (!publicMatch) {
19
13
  return [];
@@ -25,13 +19,11 @@ function extractTableNames(supabaseTypePath) {
25
19
  return [];
26
20
  }
27
21
 
28
- // Find the opening brace of `Tables: {`
29
22
  const braceStart = content.indexOf("{", tablesIdx + tablesLabel.length);
30
23
  if (braceStart === -1) {
31
24
  return [];
32
25
  }
33
26
 
34
- // Extract the Tables block using brace counting
35
27
  let depth = 0;
36
28
  let blockEnd = -1;
37
29
  for (let i = braceStart; i < content.length; i++) {
@@ -46,14 +38,12 @@ function extractTableNames(supabaseTypePath) {
46
38
  return [];
47
39
  }
48
40
 
49
- // Extract top-level keys (depth 0) inside the Tables block
50
41
  const tablesBlock = content.slice(braceStart + 1, blockEnd);
51
42
  const names = [];
52
43
  depth = 0;
53
44
  const keyRegex = /(\w+)\s*:/g;
54
45
  let match;
55
46
  while ((match = keyRegex.exec(tablesBlock)) !== null) {
56
- // Count braces before this match to determine depth
57
47
  const preceding = tablesBlock.slice(0, match.index);
58
48
  let d = 0;
59
49
  for (const ch of preceding) {
@@ -67,21 +57,16 @@ function extractTableNames(supabaseTypePath) {
67
57
  return names;
68
58
  }
69
59
 
70
- /** Convert snake_case to kebab-case. */
71
60
  function toKebab(name) {
72
61
  return name.replace(/_/g, "-");
73
62
  }
74
63
 
75
- /**
76
- * Enforce that feature directory names match allowed values:
77
- * "shared", "auth", plus Supabase table names converted to kebab-case.
78
- */
79
64
  export const featureNameRule = {
80
65
  meta: {
81
66
  type: "problem",
82
67
  messages: {
83
68
  invalidFeatureName:
84
- "Feature directory '{{ name }}' is not allowed. Allowed: {{ allowed }}.",
69
+ "feature directory '{{ name }}' は許可されない。許可: {{ allowed }}",
85
70
  },
86
71
  schema: [
87
72
  {
@@ -107,15 +92,11 @@ export const featureNameRule = {
107
92
  if (!featureName) return {};
108
93
 
109
94
  const projectRoot = filename.slice(0, rootIdx).replace(/\/src$/, "");
110
- // Prefer the Supabase types file adjacent to `featureRoot` (e.g. `src/lib/...`
111
- // for `src/features`). Fall back to `src/lib/...` at the project root so that
112
- // non-`src` feature roots (e.g. `scripts/features`) can reuse the same
113
- // generated types without duplicating the file. Both `types.ts` (plural) and
114
- // `type.ts` (singular) are accepted to match either naming convention.
115
95
  const candidateTypePaths = [
116
- path.join(projectRoot, featureRoot.replace(/features$/, "lib/supabase/types.ts")),
117
- path.join(projectRoot, featureRoot.replace(/features$/, "lib/supabase/type.ts")),
118
- path.join(projectRoot, "src/lib/supabase/types.ts"),
96
+ path.join(
97
+ projectRoot,
98
+ featureRoot.replace(/features$/, "lib/supabase/type.ts"),
99
+ ),
119
100
  path.join(projectRoot, "src/lib/supabase/type.ts"),
120
101
  ];
121
102
  const supabaseTypePath =