@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.
- 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/src/type_helper.js +15 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/rules/async_rule.ts +93 -71
- package/rules/maybe_promise_rule.ts +259 -197
- package/src/type_helper.ts +279 -253
package/README.md
CHANGED
|
@@ -150,12 +150,34 @@ That makes these names available globally:
|
|
|
150
150
|
|
|
151
151
|
### Utility Functions
|
|
152
152
|
|
|
153
|
+
- `ok(value)` and `err()` helpers for `MaybePromise` result objects
|
|
153
154
|
- `getKeys<T extends Record<string, unknown>>(object: T): Array<keyof T>`
|
|
154
155
|
- `arrayContainsCommonValue<T>(array1: T[], array2: T[]): boolean`
|
|
155
156
|
- `isEmptyString(value: unknown): boolean`
|
|
156
157
|
- `isEmptyLike(value: unknown): boolean`
|
|
157
158
|
- `hasEmptyValues(value: unknown): boolean`
|
|
158
159
|
|
|
160
|
+
### Result Helpers
|
|
161
|
+
|
|
162
|
+
Use these when you want small Rust-like `Ok` and `Err` ergonomics without
|
|
163
|
+
hand-writing result objects:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { err, ok } from "@murky-web/typebuddy";
|
|
167
|
+
|
|
168
|
+
export async function loadName(): MaybePromise<string> {
|
|
169
|
+
try {
|
|
170
|
+
return ok("murky");
|
|
171
|
+
} catch {
|
|
172
|
+
return err();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function flush(): MaybePromise<void> {
|
|
177
|
+
return ok();
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
159
181
|
### Types
|
|
160
182
|
|
|
161
183
|
- `Optional<T>`
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString } from "./src/type_helper.js";
|
|
2
|
-
export { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
|
|
1
|
+
import { arrayContainsCommonValue, err, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, ok, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString } from "./src/type_helper.js";
|
|
2
|
+
export { arrayContainsCommonValue, err, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, ok, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
|
package/dist/rules/async_rule.js
CHANGED
|
@@ -4,6 +4,12 @@ function hasTryCatch(nodes) {
|
|
|
4
4
|
return node.type === "TryStatement";
|
|
5
5
|
});
|
|
6
6
|
}
|
|
7
|
+
function isCallArgumentCallback(node) {
|
|
8
|
+
const parent = node.parent;
|
|
9
|
+
if (!parent) return false;
|
|
10
|
+
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") return false;
|
|
11
|
+
return Array.isArray(parent.arguments) && parent.arguments.includes(node);
|
|
12
|
+
}
|
|
7
13
|
const rule = {
|
|
8
14
|
create(context) {
|
|
9
15
|
const sourceCode = context.getSourceCode();
|
|
@@ -22,12 +28,13 @@ ${body.map((statement) => {
|
|
|
22
28
|
return `${innerIndent}${sourceCode.getText(statement).replaceAll(/^\s*/gm, "")}`;
|
|
23
29
|
}).join("\n")}
|
|
24
30
|
${indent}} catch (err) {
|
|
25
|
-
${innerIndent}return
|
|
31
|
+
${innerIndent}return { isError: true, value: null };
|
|
26
32
|
${indent}}
|
|
27
33
|
}`;
|
|
28
34
|
}
|
|
29
35
|
function checkFunction(node) {
|
|
30
36
|
if (node.async !== true) return;
|
|
37
|
+
if (isCallArgumentCallback(node)) return;
|
|
31
38
|
const bodyNode = node.body;
|
|
32
39
|
if (!bodyNode || Array.isArray(bodyNode) || bodyNode.type !== "BlockStatement") return;
|
|
33
40
|
if (hasTryCatch(Array.isArray(bodyNode.body) ? bodyNode.body : [])) return;
|
|
@@ -48,10 +55,10 @@ ${indent}}
|
|
|
48
55
|
defaultOptions: [],
|
|
49
56
|
meta: {
|
|
50
57
|
type: "suggestion",
|
|
51
|
-
docs: { description: "Async functions should have a try-catch block returning
|
|
58
|
+
docs: { description: "Async functions should have a try-catch block returning an error result." },
|
|
52
59
|
fixable: "code",
|
|
53
60
|
schema: [],
|
|
54
|
-
messages: { missingTryCatch: "Async functions must have a try-catch block returning
|
|
61
|
+
messages: { missingTryCatch: "Async functions must have a try-catch block returning an error result." }
|
|
55
62
|
}
|
|
56
63
|
};
|
|
57
64
|
//#endregion
|
|
@@ -8,6 +8,24 @@ function isAsyncFunction(node) {
|
|
|
8
8
|
function hasTypeParameters(value) {
|
|
9
9
|
return isNode(value) && Array.isArray(value.params);
|
|
10
10
|
}
|
|
11
|
+
function isCallArgumentCallback(node) {
|
|
12
|
+
const parent = node.parent;
|
|
13
|
+
if (!parent) return false;
|
|
14
|
+
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") return false;
|
|
15
|
+
return Array.isArray(parent.arguments) && parent.arguments.includes(node);
|
|
16
|
+
}
|
|
17
|
+
function isIdentifierNamed(node, name) {
|
|
18
|
+
return isNode(node) && node.type === "Identifier" && node.name === name;
|
|
19
|
+
}
|
|
20
|
+
function isResultHelperCall(node, name) {
|
|
21
|
+
return isNode(node) && node.type === "CallExpression" && isIdentifierNamed(node.callee, name);
|
|
22
|
+
}
|
|
23
|
+
function hasIsErrorFlag(node, expected) {
|
|
24
|
+
if (!isNode(node) || node.type !== "ObjectExpression" || !Array.isArray(node.properties)) return false;
|
|
25
|
+
return node.properties.some((property) => {
|
|
26
|
+
return isNode(property) && property.type === "Property" && isIdentifierNamed(property.key, "isError") && isNode(property.value) && property.value.type === "Literal" && property.value.value === expected;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
11
29
|
const rule = {
|
|
12
30
|
create(context) {
|
|
13
31
|
const sourceCode = context.getSourceCode();
|
|
@@ -32,6 +50,7 @@ const rule = {
|
|
|
32
50
|
}
|
|
33
51
|
function checkReturnType(node) {
|
|
34
52
|
if (!isAsyncFunction(node)) return;
|
|
53
|
+
if (isCallArgumentCallback(node)) return;
|
|
35
54
|
if (!isNode(node.returnType)) return;
|
|
36
55
|
const typeAnnotation = node.returnType.typeAnnotation;
|
|
37
56
|
if (!isTypeReference(typeAnnotation)) return;
|
|
@@ -53,13 +72,14 @@ const rule = {
|
|
|
53
72
|
node,
|
|
54
73
|
messageId: "returnVoidPromise",
|
|
55
74
|
fix(fixer) {
|
|
56
|
-
return fixer.replaceText(node, "return
|
|
75
|
+
return fixer.replaceText(node, "return { isError: false, value: undefined }");
|
|
57
76
|
}
|
|
58
77
|
});
|
|
59
78
|
return;
|
|
60
79
|
}
|
|
61
80
|
if (argument.type === "Identifier" && (argument.name === "VOID_PROMISE" || argument.name === "FAILED_PROMISE")) return;
|
|
62
|
-
if (
|
|
81
|
+
if (isResultHelperCall(argument, "ok") || isResultHelperCall(argument, "err")) return;
|
|
82
|
+
if (!hasIsErrorFlag(argument, true) && !hasIsErrorFlag(argument, false)) context.report({
|
|
63
83
|
node: argument,
|
|
64
84
|
messageId: "wrapReturn",
|
|
65
85
|
fix(fixer) {
|
|
@@ -81,6 +101,7 @@ const rule = {
|
|
|
81
101
|
parent = parent.parent;
|
|
82
102
|
}
|
|
83
103
|
if (!isAsync || !parentFunction) return;
|
|
104
|
+
if (isCallArgumentCallback(parentFunction)) return;
|
|
84
105
|
let returnType = null;
|
|
85
106
|
if (isNode(parentFunction.returnType)) {
|
|
86
107
|
const typeAnnotation = parentFunction.returnType.typeAnnotation;
|
|
@@ -90,17 +111,17 @@ const rule = {
|
|
|
90
111
|
for (const statement of blockBody) if (isNode(statement) && statement.type === "ReturnStatement") wrapReturnValue(statement, isAsync, returnType);
|
|
91
112
|
if (!isNode(node.handler) || !isNode(node.handler.body)) return;
|
|
92
113
|
const catchBody = Array.isArray(node.handler.body.body) ? node.handler.body.body.filter(isNode) : [];
|
|
93
|
-
if (!catchBody.some((statement) => statement.type === "ReturnStatement" && isNode(statement.argument) && statement.argument.type === "Identifier" && statement.argument.name === "FAILED_PROMISE")) context.report({
|
|
114
|
+
if (!catchBody.some((statement) => statement.type === "ReturnStatement" && isNode(statement.argument) && (statement.argument.type === "Identifier" && statement.argument.name === "FAILED_PROMISE" || isResultHelperCall(statement.argument, "err") || hasIsErrorFlag(statement.argument, true)))) context.report({
|
|
94
115
|
node: node.handler,
|
|
95
116
|
messageId: "returnFailedPromise",
|
|
96
117
|
fix(fixer) {
|
|
97
118
|
const lastStatement = catchBody.at(-1);
|
|
98
|
-
if (lastStatement?.type === "ReturnStatement" && isNode(lastStatement.argument) && lastStatement.argument.type === "ObjectExpression") return fixer.replaceText(lastStatement, "return
|
|
119
|
+
if (lastStatement?.type === "ReturnStatement" && isNode(lastStatement.argument) && lastStatement.argument.type === "ObjectExpression") return fixer.replaceText(lastStatement, "return { isError: true, value: null }");
|
|
99
120
|
const handler = node.handler;
|
|
100
121
|
if (!isNode(handler)) return null;
|
|
101
122
|
const bodyRange = handler.body;
|
|
102
123
|
if (!isNode(bodyRange) || !Array.isArray(bodyRange.range)) return null;
|
|
103
|
-
return fixer.insertTextBeforeRange([bodyRange.range[1] - 1, bodyRange.range[1] - 1], "return
|
|
124
|
+
return fixer.insertTextBeforeRange([bodyRange.range[1] - 1, bodyRange.range[1] - 1], "return { isError: true, value: null }; ");
|
|
104
125
|
}
|
|
105
126
|
});
|
|
106
127
|
}
|
|
@@ -123,8 +144,8 @@ const rule = {
|
|
|
123
144
|
messages: {
|
|
124
145
|
replaceWithMaybePromise: "Use 'MaybePromise' instead of 'Promise' as the return type in async functions.",
|
|
125
146
|
wrapReturn: "Wrap return value with { value: value, isError: false }.",
|
|
126
|
-
returnVoidPromise: "Return
|
|
127
|
-
returnFailedPromise: "Return
|
|
147
|
+
returnVoidPromise: "Return a success result object for async functions with Promise<void> return type.",
|
|
148
|
+
returnFailedPromise: "Return an error result object in the catch block of async functions."
|
|
128
149
|
}
|
|
129
150
|
}
|
|
130
151
|
};
|
package/dist/src/type_helper.js
CHANGED
|
@@ -26,6 +26,20 @@ function cloneDefaultArray(defaultValue) {
|
|
|
26
26
|
function isSuccess(result) {
|
|
27
27
|
return !result.isError;
|
|
28
28
|
}
|
|
29
|
+
function ok(...args) {
|
|
30
|
+
const [value] = args;
|
|
31
|
+
return {
|
|
32
|
+
isError: false,
|
|
33
|
+
value
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function err(...args) {
|
|
37
|
+
const [value] = args;
|
|
38
|
+
return {
|
|
39
|
+
isError: true,
|
|
40
|
+
value: value ?? null
|
|
41
|
+
};
|
|
42
|
+
}
|
|
29
43
|
function isString(value) {
|
|
30
44
|
return typeof value === "string";
|
|
31
45
|
}
|
|
@@ -292,4 +306,4 @@ function parseDomainName(url, defaultValue) {
|
|
|
292
306
|
return domainName;
|
|
293
307
|
}
|
|
294
308
|
//#endregion
|
|
295
|
-
export { arrayContainsCommonValue, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
|
|
309
|
+
export { arrayContainsCommonValue, err, fastIsArray, getKeys, hasEmptyValues, isArray, isBoolean, isDate, isEmptyArray, isEmptyLike, isEmptyObject, isEmptyString, isError, isFloat, isFunction, isInstanceOf, isInteger, isMaybe, isNull, isNullable, isNumber, isObject, isOptional, isPromise, isRegExp, isString, isSuccess, isSymbol, isUlidString, isUndefined, isUuidString, ok, parseArray, parseDomainName, parseFloat, parseInteger, parseNumber, parseString };
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/rules/async_rule.ts
CHANGED
|
@@ -1,98 +1,120 @@
|
|
|
1
1
|
type AstNode = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
type: string;
|
|
3
|
+
body?: AstNode | AstNode[];
|
|
4
|
+
async?: boolean;
|
|
5
|
+
parent?: AstNode;
|
|
6
|
+
range?: [number, number];
|
|
7
|
+
[key: string]: unknown;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
type RuleContext = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
): unknown;
|
|
19
|
-
}): void;
|
|
11
|
+
getSourceCode(): { getText(node: unknown): string };
|
|
12
|
+
report(descriptor: {
|
|
13
|
+
node: unknown;
|
|
14
|
+
messageId: string;
|
|
15
|
+
fix(fixer: {
|
|
16
|
+
replaceText(node: unknown, text: string): unknown;
|
|
17
|
+
}): unknown;
|
|
18
|
+
}): void;
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
function hasTryCatch(nodes: AstNode[]): boolean {
|
|
23
|
-
|
|
22
|
+
return nodes.some((node) => {
|
|
23
|
+
return node.type === "TryStatement";
|
|
24
|
+
});
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
function isCallArgumentCallback(node: AstNode): boolean {
|
|
28
|
+
const parent = node.parent;
|
|
29
|
+
if (!parent) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const firstLine = lines[0];
|
|
33
|
-
const match = /^\s*/.exec(firstLine);
|
|
34
|
-
return match ? match[0] : "";
|
|
33
|
+
if (parent.type !== "CallExpression" && parent.type !== "NewExpression") {
|
|
34
|
+
return false;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
return Array.isArray(parent.arguments) && parent.arguments.includes(node);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rule = {
|
|
41
|
+
create(context: RuleContext) {
|
|
42
|
+
const sourceCode = context.getSourceCode();
|
|
43
|
+
|
|
44
|
+
function getIndentation(node: AstNode): string {
|
|
45
|
+
const lines = sourceCode.getText(node).split("\n");
|
|
46
|
+
const firstLine = lines[0];
|
|
47
|
+
const match = /^\s*/.exec(firstLine);
|
|
48
|
+
return match ? match[0] : "";
|
|
49
|
+
}
|
|
47
50
|
|
|
48
|
-
|
|
51
|
+
function wrapInTryCatch(node: AstNode): string {
|
|
52
|
+
const body = Array.isArray(node.body) ? node.body : [];
|
|
53
|
+
const indent = getIndentation(node);
|
|
54
|
+
const innerIndent = `${indent} `;
|
|
55
|
+
const bodyText = body
|
|
56
|
+
.map((statement) => {
|
|
57
|
+
const text = sourceCode.getText(statement);
|
|
58
|
+
return `${innerIndent}${text.replaceAll(/^\s*/gm, "")}`;
|
|
59
|
+
})
|
|
60
|
+
.join("\n");
|
|
61
|
+
|
|
62
|
+
return `{
|
|
49
63
|
${indent}try {
|
|
50
64
|
${bodyText}
|
|
51
65
|
${indent}} catch (err) {
|
|
52
|
-
${innerIndent}return
|
|
66
|
+
${innerIndent}return { isError: true, value: null };
|
|
53
67
|
${indent}}
|
|
54
68
|
}`;
|
|
55
|
-
|
|
69
|
+
}
|
|
56
70
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
function checkFunction(node: AstNode) {
|
|
72
|
+
if (node.async !== true) return;
|
|
73
|
+
if (isCallArgumentCallback(node)) return;
|
|
74
|
+
const bodyNode = node.body;
|
|
75
|
+
if (
|
|
76
|
+
!bodyNode ||
|
|
77
|
+
Array.isArray(bodyNode) ||
|
|
78
|
+
bodyNode.type !== "BlockStatement"
|
|
79
|
+
) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
63
82
|
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
const body = Array.isArray(bodyNode.body) ? bodyNode.body : [];
|
|
84
|
+
if (hasTryCatch(body)) return;
|
|
66
85
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
context.report({
|
|
87
|
+
node,
|
|
88
|
+
messageId: "missingTryCatch",
|
|
89
|
+
fix(fixer) {
|
|
90
|
+
return fixer.replaceText(
|
|
91
|
+
bodyNode,
|
|
92
|
+
wrapInTryCatch(bodyNode),
|
|
93
|
+
);
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
defaultOptions: [],
|
|
83
|
-
meta: {
|
|
84
|
-
type: "suggestion",
|
|
85
|
-
docs: {
|
|
86
|
-
description:
|
|
87
|
-
"Async functions should have a try-catch block returning FAILED_PROMISE.",
|
|
98
|
+
return {
|
|
99
|
+
FunctionDeclaration: checkFunction,
|
|
100
|
+
FunctionExpression: checkFunction,
|
|
101
|
+
ArrowFunctionExpression: checkFunction,
|
|
102
|
+
};
|
|
88
103
|
},
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
defaultOptions: [],
|
|
105
|
+
meta: {
|
|
106
|
+
type: "suggestion",
|
|
107
|
+
docs: {
|
|
108
|
+
description:
|
|
109
|
+
"Async functions should have a try-catch block returning an error result.",
|
|
110
|
+
},
|
|
111
|
+
fixable: "code",
|
|
112
|
+
schema: [],
|
|
113
|
+
messages: {
|
|
114
|
+
missingTryCatch:
|
|
115
|
+
"Async functions must have a try-catch block returning an error result.",
|
|
116
|
+
},
|
|
94
117
|
},
|
|
95
|
-
},
|
|
96
118
|
};
|
|
97
119
|
|
|
98
120
|
export { rule as requireTryCatchAsyncRule };
|