@yasainet/eslint 0.0.82 → 0.0.83

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.82",
3
+ "version": "0.0.83",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,15 +1,58 @@
1
1
  const COLUMNS_NAME = /^[A-Z][A-Z0-9_]*_COLUMNS$/;
2
2
 
3
- function isStringAsConst(initNode) {
4
- if (!initNode) return false;
5
- if (initNode.type !== "TSAsExpression") return false;
3
+ function isAsConst(initNode) {
4
+ if (!initNode || initNode.type !== "TSAsExpression") return false;
6
5
  const ann = initNode.typeAnnotation;
7
6
  if (!ann || ann.type !== "TSTypeReference") return false;
8
7
  if (ann.typeName.type !== "Identifier") return false;
9
- if (ann.typeName.name !== "const") return false;
8
+ return ann.typeName.name === "const";
9
+ }
10
+
11
+ function isAsConstStringLiteral(initNode) {
12
+ if (!isAsConst(initNode)) return false;
13
+ const inner = initNode.expression;
14
+ return inner.type === "Literal" && typeof inner.value === "string";
15
+ }
16
+
17
+ function isLocalAsConstStringRef(identifier, sourceCode) {
18
+ let scope = sourceCode.getScope(identifier);
19
+ while (scope) {
20
+ const variable = scope.set.get(identifier.name);
21
+ if (variable) {
22
+ const def = variable.defs[0];
23
+ if (!def || def.type !== "Variable") return false;
24
+ if (def.parent?.kind !== "const") return false;
25
+ return isAsConstStringLiteral(def.node.init);
26
+ }
27
+ scope = scope.upper;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ /**
33
+ * `*_COLUMNS` 定数の shape を検証し、Supabase `.select()` の型推論を保つ。
34
+ *
35
+ * 許容するのは以下のいずれか:
36
+ * - `"..." as const` (string literal)
37
+ * - `` `${IDENT}, ...` as const `` (template literal で interpolation が
38
+ * 全て **同一 file scope の as const string literal Identifier** であるもの)
39
+ *
40
+ * Why-not type-aware (cross-module 追跡): rule を dumb で機械的に保つため
41
+ * scope manager の参照解決のみで検査する。派生定数を作りたい場合は base 定数と
42
+ * 同じ file に集約する運用とする (詳細: yasainet/eslint#3)。
43
+ */
44
+ function isValidShape(initNode, sourceCode) {
45
+ if (!isAsConst(initNode)) return false;
10
46
  const inner = initNode.expression;
11
- if (inner.type !== "Literal") return false;
12
- return typeof inner.value === "string";
47
+ if (inner.type === "Literal") return typeof inner.value === "string";
48
+ if (inner.type === "TemplateLiteral") {
49
+ return inner.expressions.every(
50
+ (expr) =>
51
+ expr.type === "Identifier" &&
52
+ isLocalAsConstStringRef(expr, sourceCode),
53
+ );
54
+ }
55
+ return false;
13
56
  }
14
57
 
15
58
  export const supabaseColumnsSatisfiesRule = {
@@ -17,17 +60,18 @@ export const supabaseColumnsSatisfiesRule = {
17
60
  type: "problem",
18
61
  messages: {
19
62
  shape:
20
- 'column 定数 `{{ name }}` は `"<comma-separated columns>" as const` にする。`as const` を外すと Supabase `.select()` 型推論が壊れる。配列 / template literal も不可。',
63
+ 'column 定数 `{{ name }}` は `"..." as const` か、`` `${BASE}, ...` as const `` で同一 file 内の as const literal を合成する。配列 / 識別子追跡不可能な template literal / 文字列結合は不可。`as const` を外すと Supabase `.select()` の型推論が壊れる。',
21
64
  },
22
65
  schema: [],
23
66
  },
24
67
  create(context) {
68
+ const sourceCode = context.sourceCode;
25
69
  return {
26
70
  VariableDeclarator(node) {
27
71
  if (node.id.type !== "Identifier") return;
28
72
  if (!COLUMNS_NAME.test(node.id.name)) return;
29
73
  if (!node.init) return;
30
- if (isStringAsConst(node.init)) return;
74
+ if (isValidShape(node.init, sourceCode)) return;
31
75
  context.report({
32
76
  node: node.id,
33
77
  messageId: "shape",