@yasainet/eslint 0.0.48 → 0.0.50

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.48",
3
+ "version": "0.0.50",
4
4
  "description": "ESLint",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,3 +1,5 @@
1
+ import { localPlugin } from "./local-plugins/index.mjs";
2
+
1
3
  /** Scope layer rules to the given feature root. */
2
4
  export function createLayersConfigs(featureRoot) {
3
5
  const loggerSelector = "CallExpression[callee.object.name='logger']";
@@ -69,6 +71,20 @@ export function createLayersConfigs(featureRoot) {
69
71
  ],
70
72
  },
71
73
  },
74
+ // Boundary type safety: repositories & services must not leak `any`
75
+ // into their public API. Uses type-aware inspection of the inferred
76
+ // return type so unannotated functions are still checked.
77
+ {
78
+ name: "layers/no-any-return",
79
+ files: [
80
+ `${featureRoot}/**/repositories/*.ts`,
81
+ `${featureRoot}/**/services/*.ts`,
82
+ ],
83
+ plugins: { local: localPlugin },
84
+ rules: {
85
+ "local/no-any-return": "error",
86
+ },
87
+ },
72
88
  // Services: try-catch + logger + dead error fallbacks
73
89
  {
74
90
  name: "layers/services",
@@ -2,6 +2,7 @@ import { actionHandleServiceRule } from "./action-handle-service.mjs";
2
2
  import { featureNameRule } from "./feature-name.mjs";
3
3
  import { importPathStyleRule } from "./import-path-style.mjs";
4
4
  import { namespaceImportNameRule } from "./namespace-import-name.mjs";
5
+ import { noAnyReturnRule } from "./no-any-return.mjs";
5
6
  import { schemaNamingRule } from "./schema-naming.mjs";
6
7
 
7
8
  /** Single plugin object to avoid ESLint "Cannot redefine plugin" errors. */
@@ -11,6 +12,7 @@ export const localPlugin = {
11
12
  "feature-name": featureNameRule,
12
13
  "import-path-style": importPathStyleRule,
13
14
  "namespace-import-name": namespaceImportNameRule,
15
+ "no-any-return": noAnyReturnRule,
14
16
  "schema-naming": schemaNamingRule,
15
17
  },
16
18
  };
@@ -0,0 +1,100 @@
1
+ import ts from "typescript";
2
+
3
+ /**
4
+ * Exported function の return type が any を含んでいる場合に error:
5
+ *
6
+ * - typescript-eslint の type checker を使って inferred type まで見る
7
+ * - repositories / services の API 境界を any 汚染から守ることで、domain shape が型で保証される
8
+ * - Promise<any>, Promise<{ data: any }>, Array<any> など nested も展開して検査する
9
+ */
10
+ export const noAnyReturnRule = {
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description:
15
+ "Disallow exported functions whose inferred return type contains `any`.",
16
+ },
17
+ messages: {
18
+ anyInReturn:
19
+ "Exported function's inferred return type contains `any`: {{ typeText }}. Annotate with a known type or narrow from the source — public layer APIs must have known shapes.",
20
+ },
21
+ schema: [],
22
+ },
23
+ create(context) {
24
+ const services = context.sourceCode.parserServices;
25
+ if (!services?.program || !services.esTreeNodeToTSNodeMap) return {};
26
+ const checker = services.program.getTypeChecker();
27
+
28
+ const isExported = (node) => {
29
+ const parent = node.parent;
30
+ if (!parent) return false;
31
+ if (parent.type === "ExportNamedDeclaration") return true;
32
+ if (parent.type === "ExportDefaultDeclaration") return true;
33
+ return false;
34
+ };
35
+
36
+ const getReturnType = (node) => {
37
+ const tsNode = services.esTreeNodeToTSNodeMap.get(node);
38
+ if (!tsNode) return null;
39
+ const signature = checker.getSignatureFromDeclaration(tsNode);
40
+ if (!signature) return null;
41
+ return checker.getReturnTypeOfSignature(signature);
42
+ };
43
+
44
+ // `any` を type tree 全体で検出する (Promise<any>, { a: any }, any[] 等を展開)
45
+ const containsAny = (type, seen = new Set()) => {
46
+ if (!type) return false;
47
+ if (seen.has(type)) return false;
48
+ seen.add(type);
49
+
50
+ if (type.flags & ts.TypeFlags.Any) return true;
51
+
52
+ if (type.isUnion?.() || type.isIntersection?.()) {
53
+ return type.types.some((t) => containsAny(t, seen));
54
+ }
55
+
56
+ const typeArgs =
57
+ checker.getTypeArguments?.(type) ??
58
+ type.typeArguments ??
59
+ type.aliasTypeArguments ??
60
+ [];
61
+ for (const arg of typeArgs) {
62
+ if (containsAny(arg, seen)) return true;
63
+ }
64
+
65
+ const props = type.getProperties?.() ?? [];
66
+ for (const prop of props) {
67
+ const decl = prop.valueDeclaration ?? prop.declarations?.[0];
68
+ if (!decl) continue;
69
+ const propType = checker.getTypeOfSymbolAtLocation(prop, decl);
70
+ if (containsAny(propType, seen)) return true;
71
+ }
72
+
73
+ return false;
74
+ };
75
+
76
+ const check = (node) => {
77
+ if (!isExported(node)) return;
78
+ const returnType = getReturnType(node);
79
+ if (!returnType) return;
80
+ if (!containsAny(returnType)) return;
81
+
82
+ const typeText = checker.typeToString(
83
+ returnType,
84
+ undefined,
85
+ ts.TypeFormatFlags.NoTruncation,
86
+ );
87
+ context.report({
88
+ node: node.id ?? node,
89
+ messageId: "anyInReturn",
90
+ data: { typeText },
91
+ });
92
+ };
93
+
94
+ return {
95
+ FunctionDeclaration: check,
96
+ FunctionExpression: check,
97
+ ArrowFunctionExpression: check,
98
+ };
99
+ },
100
+ };
@@ -91,6 +91,19 @@ export const rulesConfigs = [
91
91
  // on a non-null column). Kept at warn until existing violations are
92
92
  // cleaned up across consuming projects; promote to error afterwards.
93
93
  "@typescript-eslint/no-unnecessary-condition": "warn",
94
+ // Type-aware async safety: silent await omissions are a leading cause
95
+ // of race conditions in server actions and background tasks.
96
+ "@typescript-eslint/no-floating-promises": "error",
97
+ "@typescript-eslint/no-misused-promises": "error",
98
+ "@typescript-eslint/await-thenable": "error",
99
+ "@typescript-eslint/require-await": "error",
100
+ // Type-aware `any` propagation checks: any が境界を越えた瞬間に
101
+ // 残りのコードで型検査が無効化されるため、検出したら確実に止める。
102
+ "@typescript-eslint/no-unsafe-assignment": "error",
103
+ "@typescript-eslint/no-unsafe-call": "error",
104
+ "@typescript-eslint/no-unsafe-member-access": "error",
105
+ "@typescript-eslint/no-unsafe-argument": "error",
106
+ "@typescript-eslint/no-unsafe-return": "error",
94
107
  },
95
108
  },
96
109
  ];