@murky-web/typebuddy 1.0.0 → 1.1.0

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.
@@ -1,247 +1,309 @@
1
1
  type AstNode = {
2
- type: string;
3
- parent?: AstNode;
4
- [key: string]: unknown;
2
+ type: string;
3
+ parent?: AstNode;
4
+ [key: string]: unknown;
5
5
  };
6
6
 
7
7
  type RuleContext = {
8
- getSourceCode(): { getText(node: unknown): string };
9
- report(descriptor: {
10
- node: unknown;
11
- messageId: string;
12
- fix(
13
- fixer: {
14
- replaceText(node: unknown, text: string): unknown;
15
- insertTextBeforeRange(range: [number, number], text: string): unknown;
16
- },
17
- ): unknown;
18
- }): void;
8
+ getSourceCode(): { getText(node: unknown): string };
9
+ report(descriptor: {
10
+ node: unknown;
11
+ messageId: string;
12
+ fix(fixer: {
13
+ replaceText(node: unknown, text: string): unknown;
14
+ insertTextBeforeRange(
15
+ range: [number, number],
16
+ text: string,
17
+ ): unknown;
18
+ }): unknown;
19
+ }): void;
19
20
  };
20
21
 
21
22
  function isNode(value: unknown): value is AstNode {
22
- return typeof value === "object" && value !== null && "type" in value;
23
+ return typeof value === "object" && value !== null && "type" in value;
23
24
  }
24
25
 
25
26
  function isAsyncFunction(node: AstNode): boolean {
26
- return node.async === true;
27
+ return node.async === true;
27
28
  }
28
29
 
29
30
  function hasTypeParameters(value: unknown): value is { params: unknown[] } {
30
- return isNode(value) && Array.isArray(value.params);
31
+ return isNode(value) && Array.isArray(value.params);
31
32
  }
32
33
 
33
- const rule = {
34
- create(context: RuleContext) {
35
- const sourceCode = context.getSourceCode();
36
-
37
- function isTypeReference(node: unknown): node is AstNode {
38
- return isNode(node) && node.type === "TSTypeReference";
39
- }
40
-
41
- function getTypeName(node: AstNode): string | null {
42
- const typeName = node.typeName;
43
- if (!isNode(typeName) || typeName.type !== "Identifier") return null;
44
- return typeof typeName.name === "string" ? typeName.name : null;
34
+ function isCallArgumentCallback(node: AstNode): boolean {
35
+ const parent = node.parent;
36
+ if (!parent) {
37
+ return false;
45
38
  }
46
39
 
47
- function getTypeArgument(node: AstNode): AstNode | null {
48
- const typeArguments = node.typeArguments;
49
- if (!hasTypeParameters(typeArguments)) return null;
50
- const [firstParam] = typeArguments.params;
51
- if (!isNode(firstParam)) return null;
52
- return firstParam;
40
+ if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
41
+ return false;
53
42
  }
54
43
 
55
- function getPromiseTypeArgument(node: AstNode): AstNode | null {
56
- if (getTypeName(node) !== "Promise") return null;
57
- return getTypeArgument(node);
58
- }
44
+ return Array.isArray(parent.arguments) && parent.arguments.includes(node);
45
+ }
59
46
 
60
- function checkReturnType(node: AstNode) {
61
- if (!isAsyncFunction(node)) return;
62
- if (!isNode(node.returnType)) return;
47
+ function isIdentifierNamed(node: unknown, name: string): boolean {
48
+ return isNode(node) && node.type === "Identifier" && node.name === name;
49
+ }
63
50
 
64
- const typeAnnotation = node.returnType.typeAnnotation;
65
- if (!isTypeReference(typeAnnotation)) return;
66
- if (getTypeName(typeAnnotation) !== "Promise") return;
51
+ function isResultHelperCall(node: unknown, name: "ok" | "err"): boolean {
52
+ return (
53
+ isNode(node) &&
54
+ node.type === "CallExpression" &&
55
+ isIdentifierNamed(node.callee, name)
56
+ );
57
+ }
67
58
 
68
- const typeName = typeAnnotation.typeName;
69
- context.report({
70
- node: typeName,
71
- messageId: "replaceWithMaybePromise",
72
- fix(fixer) {
73
- return fixer.replaceText(typeName, "MaybePromise");
74
- },
75
- });
59
+ function hasIsErrorFlag(node: unknown, expected: boolean): boolean {
60
+ if (
61
+ !isNode(node) ||
62
+ node.type !== "ObjectExpression" ||
63
+ !Array.isArray(node.properties)
64
+ ) {
65
+ return false;
76
66
  }
77
67
 
78
- function wrapReturnValue(
79
- node: AstNode,
80
- isAsync: boolean,
81
- returnType: AstNode | null,
82
- ) {
83
- if (!isAsync) return;
84
-
85
- const argument = node.argument;
86
- if (!isNode(argument)) {
87
- if (returnType?.type === "TSVoidKeyword") {
88
- context.report({
89
- node,
90
- messageId: "returnVoidPromise",
91
- fix(fixer) {
92
- return fixer.replaceText(node, "return VOID_PROMISE");
93
- },
94
- });
95
- }
96
- return;
97
- }
98
-
99
- if (
100
- argument.type === "Identifier" &&
101
- (argument.name === "VOID_PROMISE" || argument.name === "FAILED_PROMISE")
102
- ) {
103
- return;
104
- }
105
-
106
- const hasIsErrorProperty =
107
- argument.type === "ObjectExpression" &&
108
- Array.isArray(argument.properties) &&
109
- argument.properties.some(
110
- (property) =>
68
+ return node.properties.some((property) => {
69
+ return (
111
70
  isNode(property) &&
112
71
  property.type === "Property" &&
113
- isNode(property.key) &&
114
- property.key.type === "Identifier" &&
115
- property.key.name === "isError",
72
+ isIdentifierNamed(property.key, "isError") &&
73
+ isNode(property.value) &&
74
+ property.value.type === "Literal" &&
75
+ property.value.value === expected
116
76
  );
77
+ });
78
+ }
117
79
 
118
- if (!hasIsErrorProperty) {
119
- context.report({
120
- node: argument,
121
- messageId: "wrapReturn",
122
- fix(fixer) {
123
- const argumentText = sourceCode.getText(argument);
124
- return fixer.replaceText(
125
- argument,
126
- `{ value: ${argumentText}, isError: false }`,
127
- );
128
- },
129
- });
130
- }
131
- }
80
+ const rule = {
81
+ create(context: RuleContext) {
82
+ const sourceCode = context.getSourceCode();
132
83
 
133
- function processTryCatch(node: AstNode) {
134
- let parent = node.parent;
135
- let isAsync = false;
136
- let parentFunction: AstNode | null = null;
84
+ function isTypeReference(node: unknown): node is AstNode {
85
+ return isNode(node) && node.type === "TSTypeReference";
86
+ }
137
87
 
138
- while (parent) {
139
- if (
140
- parent.type === "FunctionDeclaration" ||
141
- parent.type === "FunctionExpression" ||
142
- parent.type === "ArrowFunctionExpression"
143
- ) {
144
- isAsync = parent.async === true;
145
- parentFunction = parent;
146
- break;
88
+ function getTypeName(node: AstNode): string | null {
89
+ const typeName = node.typeName;
90
+ if (!isNode(typeName) || typeName.type !== "Identifier")
91
+ return null;
92
+ return typeof typeName.name === "string" ? typeName.name : null;
147
93
  }
148
- parent = parent.parent;
149
- }
150
94
 
151
- if (!isAsync || !parentFunction) return;
95
+ function getTypeArgument(node: AstNode): AstNode | null {
96
+ const typeArguments = node.typeArguments;
97
+ if (!hasTypeParameters(typeArguments)) return null;
98
+ const [firstParam] = typeArguments.params;
99
+ if (!isNode(firstParam)) return null;
100
+ return firstParam;
101
+ }
152
102
 
153
- let returnType: AstNode | null = null;
154
- if (isNode(parentFunction.returnType)) {
155
- const typeAnnotation = parentFunction.returnType.typeAnnotation;
156
- if (isTypeReference(typeAnnotation)) {
157
- returnType = getPromiseTypeArgument(typeAnnotation);
103
+ function getPromiseTypeArgument(node: AstNode): AstNode | null {
104
+ if (getTypeName(node) !== "Promise") return null;
105
+ return getTypeArgument(node);
158
106
  }
159
- }
160
-
161
- const blockBody = isNode(node.block) && Array.isArray(node.block.body)
162
- ? node.block.body
163
- : [];
164
- for (const statement of blockBody) {
165
- if (isNode(statement) && statement.type === "ReturnStatement") {
166
- wrapReturnValue(statement, isAsync, returnType);
107
+
108
+ function checkReturnType(node: AstNode) {
109
+ if (!isAsyncFunction(node)) return;
110
+ if (isCallArgumentCallback(node)) return;
111
+ if (!isNode(node.returnType)) return;
112
+
113
+ const typeAnnotation = node.returnType.typeAnnotation;
114
+ if (!isTypeReference(typeAnnotation)) return;
115
+ if (getTypeName(typeAnnotation) !== "Promise") return;
116
+
117
+ const typeName = typeAnnotation.typeName;
118
+ context.report({
119
+ node: typeName,
120
+ messageId: "replaceWithMaybePromise",
121
+ fix(fixer) {
122
+ return fixer.replaceText(typeName, "MaybePromise");
123
+ },
124
+ });
167
125
  }
168
- }
169
-
170
- if (!isNode(node.handler) || !isNode(node.handler.body)) return;
171
- const catchBody = Array.isArray(node.handler.body.body)
172
- ? node.handler.body.body.filter(isNode)
173
- : [];
174
-
175
- const hasCorrectReturn = catchBody.some(
176
- (statement) =>
177
- statement.type === "ReturnStatement" &&
178
- isNode(statement.argument) &&
179
- statement.argument.type === "Identifier" &&
180
- statement.argument.name === "FAILED_PROMISE",
181
- );
182
-
183
- if (!hasCorrectReturn) {
184
- context.report({
185
- node: node.handler,
186
- messageId: "returnFailedPromise",
187
- fix(fixer) {
188
- const lastStatement = catchBody.at(-1);
126
+
127
+ function wrapReturnValue(
128
+ node: AstNode,
129
+ isAsync: boolean,
130
+ returnType: AstNode | null,
131
+ ) {
132
+ if (!isAsync) return;
133
+
134
+ const argument = node.argument;
135
+ if (!isNode(argument)) {
136
+ if (returnType?.type === "TSVoidKeyword") {
137
+ context.report({
138
+ node,
139
+ messageId: "returnVoidPromise",
140
+ fix(fixer) {
141
+ return fixer.replaceText(
142
+ node,
143
+ "return { isError: false, value: undefined }",
144
+ );
145
+ },
146
+ });
147
+ }
148
+ return;
149
+ }
150
+
189
151
  if (
190
- lastStatement?.type === "ReturnStatement" &&
191
- isNode(lastStatement.argument) &&
192
- lastStatement.argument.type === "ObjectExpression"
152
+ argument.type === "Identifier" &&
153
+ (argument.name === "VOID_PROMISE" ||
154
+ argument.name === "FAILED_PROMISE")
193
155
  ) {
194
- return fixer.replaceText(lastStatement, "return FAILED_PROMISE");
156
+ return;
195
157
  }
196
158
 
197
- const handler = node.handler;
198
- if (!isNode(handler)) {
199
- return null;
159
+ if (
160
+ isResultHelperCall(argument, "ok") ||
161
+ isResultHelperCall(argument, "err")
162
+ ) {
163
+ return;
200
164
  }
201
165
 
202
- const bodyRange = handler.body;
203
- if (!isNode(bodyRange) || !Array.isArray(bodyRange.range)) {
204
- return null;
166
+ if (
167
+ !hasIsErrorFlag(argument, true) &&
168
+ !hasIsErrorFlag(argument, false)
169
+ ) {
170
+ context.report({
171
+ node: argument,
172
+ messageId: "wrapReturn",
173
+ fix(fixer) {
174
+ const argumentText = sourceCode.getText(argument);
175
+ return fixer.replaceText(
176
+ argument,
177
+ `{ value: ${argumentText}, isError: false }`,
178
+ );
179
+ },
180
+ });
181
+ }
182
+ }
183
+
184
+ function processTryCatch(node: AstNode) {
185
+ let parent = node.parent;
186
+ let isAsync = false;
187
+ let parentFunction: AstNode | null = null;
188
+
189
+ while (parent) {
190
+ if (
191
+ parent.type === "FunctionDeclaration" ||
192
+ parent.type === "FunctionExpression" ||
193
+ parent.type === "ArrowFunctionExpression"
194
+ ) {
195
+ isAsync = parent.async === true;
196
+ parentFunction = parent;
197
+ break;
198
+ }
199
+ parent = parent.parent;
200
+ }
201
+
202
+ if (!isAsync || !parentFunction) return;
203
+ if (isCallArgumentCallback(parentFunction)) return;
204
+
205
+ let returnType: AstNode | null = null;
206
+ if (isNode(parentFunction.returnType)) {
207
+ const typeAnnotation = parentFunction.returnType.typeAnnotation;
208
+ if (isTypeReference(typeAnnotation)) {
209
+ returnType = getPromiseTypeArgument(typeAnnotation);
210
+ }
205
211
  }
206
212
 
207
- return fixer.insertTextBeforeRange(
208
- [bodyRange.range[1] - 1, bodyRange.range[1] - 1],
209
- "return FAILED_PROMISE; ",
213
+ const blockBody =
214
+ isNode(node.block) && Array.isArray(node.block.body)
215
+ ? node.block.body
216
+ : [];
217
+ for (const statement of blockBody) {
218
+ if (isNode(statement) && statement.type === "ReturnStatement") {
219
+ wrapReturnValue(statement, isAsync, returnType);
220
+ }
221
+ }
222
+
223
+ if (!isNode(node.handler) || !isNode(node.handler.body)) return;
224
+ const catchBody = Array.isArray(node.handler.body.body)
225
+ ? node.handler.body.body.filter(isNode)
226
+ : [];
227
+
228
+ const hasCorrectReturn = catchBody.some(
229
+ (statement) =>
230
+ statement.type === "ReturnStatement" &&
231
+ isNode(statement.argument) &&
232
+ ((statement.argument.type === "Identifier" &&
233
+ statement.argument.name === "FAILED_PROMISE") ||
234
+ isResultHelperCall(statement.argument, "err") ||
235
+ hasIsErrorFlag(statement.argument, true)),
210
236
  );
211
- },
212
- });
213
- }
214
- }
215
237
 
216
- return {
217
- FunctionDeclaration: checkReturnType,
218
- FunctionExpression: checkReturnType,
219
- ArrowFunctionExpression: checkReturnType,
220
- TSDeclareFunction: checkReturnType,
221
- TSFunctionType: checkReturnType,
222
- TSMethodSignature: checkReturnType,
223
- TryStatement: processTryCatch,
224
- };
225
- },
226
- defaultOptions: [],
227
- meta: {
228
- type: "suggestion",
229
- docs: {
230
- description:
231
- "Ensure async functions' return values follow the MaybePromise pattern.",
238
+ if (!hasCorrectReturn) {
239
+ context.report({
240
+ node: node.handler,
241
+ messageId: "returnFailedPromise",
242
+ fix(fixer) {
243
+ const lastStatement = catchBody.at(-1);
244
+ if (
245
+ lastStatement?.type === "ReturnStatement" &&
246
+ isNode(lastStatement.argument) &&
247
+ lastStatement.argument.type === "ObjectExpression"
248
+ ) {
249
+ return fixer.replaceText(
250
+ lastStatement,
251
+ "return { isError: true, value: null }",
252
+ );
253
+ }
254
+
255
+ const handler = node.handler;
256
+ if (!isNode(handler)) {
257
+ return null;
258
+ }
259
+
260
+ const bodyRange = handler.body;
261
+ if (
262
+ !isNode(bodyRange) ||
263
+ !Array.isArray(bodyRange.range)
264
+ ) {
265
+ return null;
266
+ }
267
+
268
+ return fixer.insertTextBeforeRange(
269
+ [bodyRange.range[1] - 1, bodyRange.range[1] - 1],
270
+ "return { isError: true, value: null }; ",
271
+ );
272
+ },
273
+ });
274
+ }
275
+ }
276
+
277
+ return {
278
+ FunctionDeclaration: checkReturnType,
279
+ FunctionExpression: checkReturnType,
280
+ ArrowFunctionExpression: checkReturnType,
281
+ TSDeclareFunction: checkReturnType,
282
+ TSFunctionType: checkReturnType,
283
+ TSMethodSignature: checkReturnType,
284
+ TryStatement: processTryCatch,
285
+ };
232
286
  },
233
- fixable: "code",
234
- schema: [],
235
- messages: {
236
- replaceWithMaybePromise:
237
- "Use 'MaybePromise' instead of 'Promise' as the return type in async functions.",
238
- wrapReturn: "Wrap return value with { value: value, isError: false }.",
239
- returnVoidPromise:
240
- "Return VOID_PROMISE for async functions with Promise<void> return type.",
241
- returnFailedPromise:
242
- "Return FAILED_PROMISE in catch block of async functions.",
287
+ defaultOptions: [],
288
+ meta: {
289
+ type: "suggestion",
290
+ docs: {
291
+ description:
292
+ "Ensure async functions' return values follow the MaybePromise pattern.",
293
+ },
294
+ fixable: "code",
295
+ schema: [],
296
+ messages: {
297
+ replaceWithMaybePromise:
298
+ "Use 'MaybePromise' instead of 'Promise' as the return type in async functions.",
299
+ wrapReturn:
300
+ "Wrap return value with { value: value, isError: false }.",
301
+ returnVoidPromise:
302
+ "Return a success result object for async functions with Promise<void> return type.",
303
+ returnFailedPromise:
304
+ "Return an error result object in the catch block of async functions.",
305
+ },
243
306
  },
244
- },
245
307
  };
246
308
 
247
309
  export { rule as errorSafeAsyncRule };