@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.
- package/README.md +9 -152
- package/package.json +7 -1
- package/src/cli/test-audit.mjs +97 -0
- package/src/common/CLAUDE.md +17 -0
- package/src/common/{constants.mjs → _internal/constants.mjs} +1 -19
- package/src/common/_internal/import-patterns.mjs +16 -0
- package/src/common/{plugins.mjs → _internal/plugins.mjs} +0 -1
- package/src/common/_internal/selectors.mjs +12 -0
- package/src/common/{rules.mjs → base/typescript.mjs} +15 -36
- package/src/common/{entry-points.mjs → boundaries/entry-point.mjs} +1 -2
- package/src/common/cross-cutting/ban-alias.mjs +22 -0
- package/src/common/cross-cutting/feature-default-imports.mjs +26 -0
- package/src/common/cross-cutting/feature-name.mjs +15 -0
- package/src/common/cross-cutting/features-ts-only.mjs +20 -0
- package/src/common/cross-cutting/form-state.mjs +16 -0
- package/src/common/{jsdoc.mjs → cross-cutting/jsdoc.mjs} +2 -3
- package/src/common/cross-cutting/logger.mjs +21 -0
- package/src/common/cross-cutting/namespace-import.mjs +23 -0
- package/src/common/cross-cutting/no-any-return.mjs +18 -0
- package/src/common/cross-cutting/no-colocated-test.mjs +18 -0
- package/src/common/cross-cutting/supabase-columns-satisfies.mjs +18 -0
- package/src/common/index.mjs +44 -24
- package/src/common/layers/constants.mjs +36 -0
- package/src/common/layers/entries.mjs +174 -0
- package/src/common/layers/lib.mjs +18 -0
- package/src/common/layers/queries.mjs +187 -0
- package/src/common/layers/schemas.mjs +50 -0
- package/src/common/layers/services.mjs +121 -0
- package/src/common/layers/top-level-utils.mjs +18 -0
- package/src/common/layers/types.mjs +44 -0
- package/src/common/layers/utils.mjs +54 -0
- package/src/common/local-plugins/entry-single-service-call.mjs +3 -31
- package/src/common/local-plugins/entry-template.mjs +51 -88
- package/src/common/local-plugins/feature-name.mjs +5 -24
- package/src/common/local-plugins/form-state-naming.mjs +1 -11
- package/src/common/local-plugins/form-state-shape.mjs +8 -42
- package/src/common/local-plugins/import-path-style.mjs +2 -9
- package/src/common/local-plugins/index.mjs +2 -1
- package/src/common/local-plugins/layout-main-structural-only.mjs +1 -22
- package/src/common/local-plugins/namespace-import-name.mjs +1 -27
- package/src/common/local-plugins/no-any-return.mjs +1 -9
- package/src/common/local-plugins/no-colocated-test.mjs +26 -0
- package/src/common/local-plugins/queries-export.mjs +1 -9
- package/src/common/local-plugins/queries-namespace-import.mjs +1 -11
- package/src/common/local-plugins/schema-naming.mjs +2 -8
- package/src/common/local-plugins/supabase-columns-satisfies.mjs +1 -25
- package/src/common/local-plugins/supabase-select-typed-columns.mjs +5 -37
- package/src/deno/CLAUDE.md +10 -0
- package/src/deno/boundaries/entry-point.mjs +44 -0
- package/src/deno/boundaries/lib.mjs +28 -0
- package/src/deno/boundaries/utils.mjs +25 -0
- package/src/deno/index.mjs +9 -13
- package/src/deno/local-plugins/flat-entry-point.mjs +1 -6
- package/src/deno/local-plugins/index.mjs +0 -1
- package/src/next/CLAUDE.md +14 -0
- package/src/next/boundaries/components.mjs +36 -0
- package/src/next/boundaries/hooks.mjs +36 -0
- package/src/next/boundaries/lib.mjs +23 -0
- package/src/next/boundaries/page.mjs +36 -0
- package/src/next/boundaries/route.mjs +36 -0
- package/src/next/boundaries/sitemap.mjs +36 -0
- package/src/next/directives.mjs +4 -5
- package/src/next/imports.mjs +0 -1
- package/src/next/index.mjs +12 -15
- package/src/next/layers/components.mjs +30 -0
- package/src/next/layers/hooks.mjs +31 -0
- package/src/next/layers/layouts.mjs +12 -0
- package/src/next/tailwindcss.mjs +2 -23
- package/src/node/CLAUDE.md +7 -0
- package/src/node/index.mjs +1 -2
- package/src/common/imports.mjs +0 -457
- package/src/common/layers.mjs +0 -158
- package/src/common/naming.mjs +0 -347
- package/src/deno/imports.mjs +0 -90
- package/src/next/layouts.mjs +0 -18
- 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 }}'
|
|
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")
|
|
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 (
|
|
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
|
|
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")
|
|
234
|
-
|
|
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({
|
|
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;
|
|
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>";
|
|
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({
|
|
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: {
|
|
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
|
|
528
|
-
|
|
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
|
|
493
|
+
"entry '{{ funcName }}' の try block は `logger.info(<obj>, \"Start {{ funcName }}\")` で始める。",
|
|
531
494
|
trySuccessLogMissing:
|
|
532
|
-
"entry '{{ funcName }}' success return
|
|
495
|
+
"entry '{{ funcName }}' の success return の直前に `logger.info(<obj>, \"Success {{ funcName }}\")` が必須。",
|
|
533
496
|
trySuccessReturnMissing:
|
|
534
|
-
"entry '{{ funcName }}'
|
|
497
|
+
"entry '{{ funcName }}' は success return `return { data, error: null }` が必須。",
|
|
535
498
|
failedLogMissing:
|
|
536
|
-
"entry '{{ funcName }}' Failed
|
|
499
|
+
"entry '{{ funcName }}' の Failed 分岐は return 前に `logger.error(<obj>, \"Failed {{ funcName }}\")` を呼ぶ。",
|
|
537
500
|
catchParamWrongType:
|
|
538
|
-
"entry '{{ funcName }}' catch param
|
|
539
|
-
catchEmpty: "entry '{{ funcName }}' catch block
|
|
501
|
+
"entry '{{ funcName }}' の catch param は `error: unknown`。",
|
|
502
|
+
catchEmpty: "entry '{{ funcName }}' の catch block が空。",
|
|
540
503
|
catchMissingErrorLog:
|
|
541
|
-
"entry '{{ funcName }}' catch block
|
|
504
|
+
"entry '{{ funcName }}' の catch block は `logger.error(<obj>, \"Unexpected error in {{ funcName }}\")` で始める。",
|
|
542
505
|
catchLastNotReturn:
|
|
543
|
-
"entry '{{ funcName }}' catch block
|
|
506
|
+
"entry '{{ funcName }}' の catch block は return 文で終える。",
|
|
544
507
|
catchWrongReturnMessage:
|
|
545
|
-
"entry '{{ funcName }}' catch return error.message
|
|
508
|
+
"entry '{{ funcName }}' の catch return の error.message はリテラル '{{ expected }}'。実際: '{{ actual }}'。",
|
|
546
509
|
logWrongCallShape:
|
|
547
|
-
"{{
|
|
510
|
+
"'{{ funcName }}' の {{ where }} log は `logger.{{ expectedLevel }}(<obj>, \"{{ expectedMessage }}\")`。",
|
|
548
511
|
logWrongMessage:
|
|
549
|
-
"{{ where }} log message
|
|
512
|
+
"{{ where }} log message は '{{ expectedMessage }}'。実際: '{{ actual }}'。",
|
|
550
513
|
logFirstArgNotObject:
|
|
551
|
-
"{{
|
|
514
|
+
"'{{ funcName }}' の {{ where }} log の第1引数は object literal にする。",
|
|
552
515
|
logErrKeyNotFirst:
|
|
553
|
-
"{{
|
|
516
|
+
"'{{ funcName }}' の {{ where }} log object は `err:` キーで始める。",
|
|
554
517
|
logMissingInputArg:
|
|
555
|
-
"{{
|
|
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
|
-
"
|
|
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(
|
|
117
|
-
|
|
118
|
-
|
|
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 =
|