@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 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 };
@@ -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 FAILED_PROMISE;
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 FAILED_PROMISE." },
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 FAILED_PROMISE." }
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 VOID_PROMISE");
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 (!(argument.type === "ObjectExpression" && Array.isArray(argument.properties) && argument.properties.some((property) => isNode(property) && property.type === "Property" && isNode(property.key) && property.key.type === "Identifier" && property.key.name === "isError"))) context.report({
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 FAILED_PROMISE");
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 FAILED_PROMISE; ");
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 VOID_PROMISE for async functions with Promise<void> return type.",
127
- returnFailedPromise: "Return FAILED_PROMISE in catch block of async functions."
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
  };
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
  "name": "@murky-web/typebuddy",
4
- "version": "1.0.0",
4
+ "version": "1.1.0",
5
5
  "license": "ISC",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murky-web/typebuddy",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Your new best friend for simple typescript guards every project needs.",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -1,98 +1,120 @@
1
1
  type AstNode = {
2
- type: string;
3
- body?: AstNode | AstNode[];
4
- async?: boolean;
5
- range?: [number, number];
6
- [key: string]: unknown;
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
- getSourceCode(): { getText(node: unknown): string };
11
- report(descriptor: {
12
- node: unknown;
13
- messageId: string;
14
- fix(
15
- fixer: {
16
- replaceText(node: unknown, text: string): unknown;
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
- return nodes.some((node) => {return node.type === "TryStatement"});
22
+ return nodes.some((node) => {
23
+ return node.type === "TryStatement";
24
+ });
24
25
  }
25
26
 
26
- const rule = {
27
- create(context: RuleContext) {
28
- const sourceCode = context.getSourceCode();
27
+ function isCallArgumentCallback(node: AstNode): boolean {
28
+ const parent = node.parent;
29
+ if (!parent) {
30
+ return false;
31
+ }
29
32
 
30
- function getIndentation(node: AstNode): string {
31
- const lines = sourceCode.getText(node).split("\n");
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
- function wrapInTryCatch(node: AstNode): string {
38
- const body = Array.isArray(node.body) ? node.body : [];
39
- const indent = getIndentation(node);
40
- const innerIndent = `${indent} `;
41
- const bodyText = body
42
- .map((statement) => {
43
- const text = sourceCode.getText(statement);
44
- return `${innerIndent}${text.replaceAll(/^\s*/gm, "")}`;
45
- })
46
- .join("\n");
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
- return `{
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 FAILED_PROMISE;
66
+ ${innerIndent}return { isError: true, value: null };
53
67
  ${indent}}
54
68
  }`;
55
- }
69
+ }
56
70
 
57
- function checkFunction(node: AstNode) {
58
- if (node.async !== true) return;
59
- const bodyNode = node.body;
60
- if (!bodyNode || Array.isArray(bodyNode) || bodyNode.type !== "BlockStatement") {
61
- return;
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
- const body = Array.isArray(bodyNode.body) ? bodyNode.body : [];
65
- if (hasTryCatch(body)) return;
83
+ const body = Array.isArray(bodyNode.body) ? bodyNode.body : [];
84
+ if (hasTryCatch(body)) return;
66
85
 
67
- context.report({
68
- node,
69
- messageId: "missingTryCatch",
70
- fix(fixer) {
71
- return fixer.replaceText(bodyNode, wrapInTryCatch(bodyNode));
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
- return {
77
- FunctionDeclaration: checkFunction,
78
- FunctionExpression: checkFunction,
79
- ArrowFunctionExpression: checkFunction,
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
- fixable: "code",
90
- schema: [],
91
- messages: {
92
- missingTryCatch:
93
- "Async functions must have a try-catch block returning FAILED_PROMISE.",
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 };