@murky-web/typebuddy 0.1.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.
- package/README.md +22 -0
- package/dist/index.js +2 -2
- package/dist/rules/async_rule.js +10 -3
- package/dist/rules/maybe_promise_rule.js +28 -7
- package/dist/{type_helper.js → src/type_helper.js} +16 -3
- package/jsr.json +1 -1
- package/oxlint/index.ts +13 -13
- package/package.json +13 -4
- package/rules/async_rule.ts +93 -71
- package/rules/maybe_promise_rule.ts +259 -197
- package/src/type_helper.ts +279 -253
|
@@ -1,247 +1,309 @@
|
|
|
1
1
|
type AstNode = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
type: string;
|
|
3
|
+
parent?: AstNode;
|
|
4
|
+
[key: string]: unknown;
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
type RuleContext = {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
23
|
+
return typeof value === "object" && value !== null && "type" in value;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function isAsyncFunction(node: AstNode): boolean {
|
|
26
|
-
|
|
27
|
+
return node.async === true;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function hasTypeParameters(value: unknown): value is { params: unknown[] } {
|
|
30
|
-
|
|
31
|
+
return isNode(value) && Array.isArray(value.params);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
return getTypeArgument(node);
|
|
58
|
-
}
|
|
44
|
+
return Array.isArray(parent.arguments) && parent.arguments.includes(node);
|
|
45
|
+
}
|
|
59
46
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
47
|
+
function isIdentifierNamed(node: unknown, name: string): boolean {
|
|
48
|
+
return isNode(node) && node.type === "Identifier" && node.name === name;
|
|
49
|
+
}
|
|
63
50
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
node
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
114
|
-
property.
|
|
115
|
-
property.
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
152
|
+
argument.type === "Identifier" &&
|
|
153
|
+
(argument.name === "VOID_PROMISE" ||
|
|
154
|
+
argument.name === "FAILED_PROMISE")
|
|
193
155
|
) {
|
|
194
|
-
|
|
156
|
+
return;
|
|
195
157
|
}
|
|
196
158
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
159
|
+
if (
|
|
160
|
+
isResultHelperCall(argument, "ok") ||
|
|
161
|
+
isResultHelperCall(argument, "err")
|
|
162
|
+
) {
|
|
163
|
+
return;
|
|
200
164
|
}
|
|
201
165
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 };
|