@yasainet/eslint 0.0.57 → 0.0.58

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.57",
3
+ "version": "0.0.58",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,50 +1,47 @@
1
1
  /**
2
- * Enforce `[...] as const satisfies readonly (keyof Tables<"...">)[]` for
3
- * `*_COLUMNS` constant declarations.
2
+ * Enforce `<string literal> as const` for `*_COLUMNS` constant declarations.
4
3
  *
5
- * Apply to `**\/queries/*.query.ts`. Without `as const satisfies`, typos
6
- * in column names slip past the lint phase — Supabase only complains at
7
- * runtime when the query executes. The `satisfies` annotation forces
8
- * TypeScript to validate every entry against the schema before the code
9
- * ever runs. The actual `keyof Tables<"...">` content is left to the type
10
- * checker; the lint rule only verifies the syntactic shape.
4
+ * Apply to `**\/queries/*.query.ts` and `**\/constants/*.constant.ts`.
5
+ *
6
+ * `*_COLUMNS` 定数は Supabase `.select()` に直接渡される。`as const`
7
+ * 外すと TypeScript `string` widen し、Supabase `.select<Query>()`
8
+ * literal parse できなくなって row 型推論が壊れる(戻り値が
9
+ * `GenericStringError` になる)。
10
+ *
11
+ * Allowed:
12
+ * const POST_DETAIL_COLUMNS = "id,url,platform" as const;
13
+ *
14
+ * Banned:
15
+ * const POST_DETAIL_COLUMNS = "id,url,platform"; // string に widen
16
+ * const POST_DETAIL_COLUMNS = ["id", "url"] as const; // 配列
17
+ * const POST_DETAIL_COLUMNS = [...] as const satisfies ...; // 配列 + satisfies
18
+ * const POST_DETAIL_COLUMNS = `id,${col}`; // template literal
19
+ *
20
+ * Why: シンプルな string literal を `as const` で保つだけで、Supabase の
21
+ * 型推論(row 型 / column 名タイポ検出)はすべて自動で効く。runtime helper
22
+ * (`joinColumns` 等)は不要。
11
23
  */
12
24
 
13
25
  const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
14
26
 
15
- /** Unwrap nested type assertions and find the underlying expression. */
16
- function unwrapTypeAssertions(node) {
17
- let current = node;
18
- while (
19
- current &&
20
- (current.type === "TSAsExpression" ||
21
- current.type === "TSSatisfiesExpression")
22
- ) {
23
- current = current.expression;
24
- }
25
- return current;
26
- }
27
-
28
- function isAsConstSatisfies(initNode) {
27
+ function isStringAsConst(initNode) {
29
28
  if (!initNode) return false;
30
- if (initNode.type !== "TSSatisfiesExpression") return false;
31
- const inner = initNode.expression;
32
- if (inner.type !== "TSAsExpression") return false;
33
- const ann = inner.typeAnnotation;
34
- if (!ann) return false;
35
- if (ann.type !== "TSTypeReference") return false;
29
+ if (initNode.type !== "TSAsExpression") return false;
30
+ const ann = initNode.typeAnnotation;
31
+ if (!ann || ann.type !== "TSTypeReference") return false;
36
32
  if (ann.typeName.type !== "Identifier") return false;
37
33
  if (ann.typeName.name !== "const") return false;
38
- if (inner.expression.type !== "ArrayExpression") return false;
39
- return true;
34
+ const inner = initNode.expression;
35
+ if (inner.type !== "Literal") return false;
36
+ return typeof inner.value === "string";
40
37
  }
41
38
 
42
39
  export const supabaseColumnsSatisfiesRule = {
43
40
  meta: {
44
41
  type: "problem",
45
42
  messages: {
46
- missing:
47
- 'Column constant `{{ name }}` must use `[...] as const satisfies readonly (keyof Tables<"table">)[]` so column names are validated against the schema at compile time.',
43
+ shape:
44
+ 'Column constant `{{ name }}` must be `"<comma-separated columns>" as const`. `as const` を外すと Supabase `.select()` 型推論が壊れる。配列 / template literal も不可。',
48
45
  },
49
46
  schema: [],
50
47
  },
@@ -53,15 +50,11 @@ export const supabaseColumnsSatisfiesRule = {
53
50
  VariableDeclarator(node) {
54
51
  if (node.id.type !== "Identifier") return;
55
52
  if (!COLUMNS_NAME.test(node.id.name)) return;
56
- // Only enforce on array initializers. String literals like
57
- // POST_UPSERT_CONFLICT_COLUMNS are PostgREST conflict-target specs,
58
- // not column lists, and are out of scope.
59
- const inner = unwrapTypeAssertions(node.init);
60
- if (!inner || inner.type !== "ArrayExpression") return;
61
- if (isAsConstSatisfies(node.init)) return;
53
+ if (!node.init) return;
54
+ if (isStringAsConst(node.init)) return;
62
55
  context.report({
63
56
  node: node.id,
64
- messageId: "missing",
57
+ messageId: "shape",
65
58
  data: { name: node.id.name },
66
59
  });
67
60
  },
@@ -1,59 +1,51 @@
1
1
  /**
2
- * Enforce typed column constants for Supabase `.select()` calls.
2
+ * Enforce explicit column lists for Supabase `.select()` calls.
3
3
  *
4
- * Apply to `**\/queries/*.query.ts`. Forces `.select()` to take the form
5
- * `joinColumns(<X_COLUMNS>)` where `X_COLUMNS` is an UPPER_SNAKE identifier
6
- * ending with `_COLUMNS`. The identifier is expected to be declared with
7
- * `as const satisfies readonly (keyof Tables<"table">)[]` so the column
8
- * names are validated against the schema at compile time. The `satisfies`
9
- * shape itself is enforced by the companion `supabase-columns-satisfies`
10
- * rule.
4
+ * Apply to `**\/queries/*.query.ts`. `.select()` の引数は次のいずれかでなければならない:
11
5
  *
12
- * `joinColumns()` is a project-supplied helper that comma-joins a const
13
- * string tuple while preserving the literal string type so Supabase's
14
- * `.select()` type parser can infer the row shape (a plain `.join(",")`
15
- * widens to `string` and breaks inference).
6
+ * - inline string literal(例: `.select("id,url,platform")`)
7
+ * - `*_COLUMNS` という UPPER_SNAKE 命名の identifier(例: `.select(POST_DETAIL_COLUMNS)`)
8
+ *
9
+ * `*_COLUMNS` 定数は companion rule `supabase-columns-satisfies`
10
+ * `<string literal> as const` の形が強制される。これにより:
11
+ *
12
+ * - Supabase の `.select()` は literal string を parse して row 型を推論できる
13
+ * - 存在しない column 名は Supabase の型推論が `SelectQueryError` として弾く(compile time)
14
+ * - runtime helper(`joinColumns`)は不要
16
15
  *
17
16
  * Banned:
18
- * .select() implicit "all columns"
19
- * .select("*") silent exposure of new schema columns
20
- * .select("id, name") inline literal, invisible to grep
21
- * .select(`${x}, y`) dynamic concatenation
22
- * .select(POST_LIST_COLUMNS.join(",")) plain .join widens to `string`, breaks inference
23
- * .select(someVar) non-conforming variable
17
+ * .select() implicit "all columns"
18
+ * .select("*") silent exposure of new schema columns
19
+ * .select(`${x},y`) dynamic concatenation
20
+ * .select(cols.join(",")) runtime expression
21
+ * .select(someVar) non-conforming variable
24
22
  *
25
23
  * Allowed:
26
- * .select(joinColumns(POST_LIST_COLUMNS)) typed constant via project helper
24
+ * .select("id,url,platform") inline literal
25
+ * .select(POST_DETAIL_COLUMNS) *_COLUMNS named constant
27
26
  *
28
- * Why: column lists must be (1) named for grep / review, (2) checked
29
- * against the schema, (3) never silently grow on schema additions.
30
- * For column-level access control, use Postgres views (`from("posts_public")`).
27
+ * Why: column lists must be (1) statically analyzable for grep / review,
28
+ * (2) literal so Supabase can infer the row shape, (3) never silently grow
29
+ * on schema additions. For column-level access control, use Postgres views
30
+ * (`from("posts_public")`).
31
31
  */
32
32
 
33
33
  const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
34
34
 
35
- function asJoinColumnsCall(arg) {
36
- if (!arg) return null;
37
- if (arg.type !== "CallExpression") return null;
38
- if (arg.callee.type !== "Identifier") return null;
39
- if (arg.callee.name !== "joinColumns") return null;
40
- if (arg.arguments.length !== 1) return null;
41
- if (arg.arguments[0].type !== "Identifier") return null;
42
- return arg.arguments[0];
43
- }
44
-
45
35
  export const supabaseSelectTypedColumnsRule = {
46
36
  meta: {
47
37
  type: "problem",
48
38
  messages: {
49
39
  noArgs:
50
- "Empty `.select()` returns all columns implicitly. Pass `joinColumns(<X_COLUMNS>)` where X_COLUMNS is a typed constant.",
51
- literalArg:
52
- 'Inline `.select()` argument is forbidden. Define `const X_COLUMNS = [...] as const satisfies readonly (keyof Tables<"table">)[];` and call `.select(joinColumns(X_COLUMNS))`. Use Postgres views for column-level access control.',
40
+ "Empty `.select()` returns all columns implicitly. Pass a string literal or a `*_COLUMNS` constant.",
41
+ wildcard:
42
+ '`.select("*")` exposes new schema columns silently. Enumerate columns explicitly.',
43
+ template:
44
+ "Template literal in `.select()` defeats type inference. Use a string literal or a `*_COLUMNS` constant.",
53
45
  shapeArg:
54
- '`.select()` argument must be `joinColumns(<X_COLUMNS>)`. Other expressions defeat type inference and column-level review.',
46
+ "`.select()` argument must be a string literal or a `*_COLUMNS` identifier.",
55
47
  naming:
56
- "Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_LIST_COLUMNS, POST_DETAIL_COLUMNS).",
48
+ "Column constant `{{ name }}` must be UPPER_SNAKE_CASE ending with `_COLUMNS` (e.g. POST_DETAIL_COLUMNS).",
57
49
  },
58
50
  schema: [],
59
51
  },
@@ -71,24 +63,34 @@ export const supabaseSelectTypedColumnsRule = {
71
63
 
72
64
  const arg = node.arguments[0];
73
65
 
74
- if (arg.type === "Literal" || arg.type === "TemplateLiteral") {
75
- context.report({ node: arg, messageId: "literalArg" });
66
+ if (arg.type === "Literal") {
67
+ if (typeof arg.value !== "string") {
68
+ context.report({ node: arg, messageId: "shapeArg" });
69
+ return;
70
+ }
71
+ if (arg.value.trim() === "*") {
72
+ context.report({ node: arg, messageId: "wildcard" });
73
+ }
76
74
  return;
77
75
  }
78
76
 
79
- const id = asJoinColumnsCall(arg);
80
- if (!id) {
81
- context.report({ node: arg, messageId: "shapeArg" });
77
+ if (arg.type === "TemplateLiteral") {
78
+ context.report({ node: arg, messageId: "template" });
82
79
  return;
83
80
  }
84
81
 
85
- if (!COLUMNS_NAME.test(id.name)) {
86
- context.report({
87
- node: id,
88
- messageId: "naming",
89
- data: { name: id.name },
90
- });
82
+ if (arg.type === "Identifier") {
83
+ if (!COLUMNS_NAME.test(arg.name)) {
84
+ context.report({
85
+ node: arg,
86
+ messageId: "naming",
87
+ data: { name: arg.name },
88
+ });
89
+ }
90
+ return;
91
91
  }
92
+
93
+ context.report({ node: arg, messageId: "shapeArg" });
92
94
  },
93
95
  };
94
96
  },