resultar 1.4.5 → 2.0.0-beta.2
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 +618 -1398
- package/dist/index.d.ts +77 -1
- package/dist/index.js +126 -36
- package/dist/index.js.map +1 -1
- package/dist/no-discard.d.ts +18 -0
- package/dist/no-discard.js +124 -0
- package/dist/no-discard.js.map +1 -0
- package/dist/result-async-CdlVdXgX.js +698 -0
- package/dist/result-async-CdlVdXgX.js.map +1 -0
- package/dist/result-async-CxGBicja.d.ts +439 -0
- package/package.json +76 -78
- package/dist/chunk-C3IJPSX2.js +0 -718
- package/dist/chunk-C3IJPSX2.js.map +0 -1
- package/dist/chunk-VBI3T5VS.js +0 -19
- package/dist/chunk-VBI3T5VS.js.map +0 -1
- package/dist/error.cjs +0 -43
- package/dist/error.cjs.map +0 -1
- package/dist/error.d.cts +0 -1
- package/dist/error.d.ts +0 -1
- package/dist/error.js +0 -7
- package/dist/error.js.map +0 -1
- package/dist/index.cjs +0 -764
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -1
- package/dist/result-async.cjs +0 -745
- package/dist/result-async.cjs.map +0 -1
- package/dist/result-async.d.cts +0 -1
- package/dist/result-async.d.ts +0 -1
- package/dist/result-async.js +0 -26
- package/dist/result-async.js.map +0 -1
- package/dist/result.cjs +0 -737
- package/dist/result.cjs.map +0 -1
- package/dist/result.d.cts +0 -1
- package/dist/result.d.ts +0 -1
- package/dist/result.js +0 -22
- package/dist/result.js.map +0 -1
- package/dist/utils.cjs +0 -722
- package/dist/utils.cjs.map +0 -1
- package/dist/utils.d.cts +0 -612
- package/dist/utils.d.ts +0 -612
- package/dist/utils.js +0 -14
- package/dist/utils.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1 +1,77 @@
|
|
|
1
|
-
|
|
1
|
+
import { S as unit, _ as err, a as fromPromise, b as safeTry, c as okAsync, d as DisposableResult, f as ErrResult, g as StrictResult, h as ResultOperations, i as errAsync, l as tryCatchAsync, m as Result, n as ResultAsync, o as fromSafePromise, p as OkResult, r as StrictResultAsync, s as fromThrowableAsync, t as DisposableResultAsync, u as unitAsync, v as fromThrowable, x as tryCatch, y as ok } from "./result-async-CxGBicja.js";
|
|
2
|
+
|
|
3
|
+
//#region src/tagged-error.d.ts
|
|
4
|
+
type Alpha = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" | "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "_";
|
|
5
|
+
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
|
|
6
|
+
type AlphaNumeric = Alpha | Digit;
|
|
7
|
+
type ReservedTemplateVariable = "_tag" | "cause" | "fingerprint" | "message" | "messageTemplate" | "name" | "stack";
|
|
8
|
+
type ConsumeTemplateVariable<Template extends string, Accumulator extends string = ""> = Template extends `${infer Character}${infer Rest}` ? Character extends AlphaNumeric ? ConsumeTemplateVariable<Rest, `${Accumulator}${Character}`> : {
|
|
9
|
+
readonly rest: Template;
|
|
10
|
+
readonly variable: Accumulator;
|
|
11
|
+
} : {
|
|
12
|
+
readonly rest: "";
|
|
13
|
+
readonly variable: Accumulator;
|
|
14
|
+
};
|
|
15
|
+
type ExtractTemplateVariables<Template extends string> = Template extends `${string}$${infer AfterDollar}` ? AfterDollar extends `${Alpha}${string}` ? ConsumeTemplateVariable<AfterDollar> extends {
|
|
16
|
+
readonly rest: infer Rest extends string;
|
|
17
|
+
readonly variable: infer Variable extends string;
|
|
18
|
+
} ? Variable | ExtractTemplateVariables<Rest> : never : ExtractTemplateVariables<AfterDollar> : never;
|
|
19
|
+
type TemplateProps<Template extends string> = [ExtractTemplateVariables<Template>] extends [never] ? Record<never, never> : Readonly<Record<ExtractTemplateVariables<Template>, string | number>>;
|
|
20
|
+
type ReservedVariablesIn<Template extends string> = Extract<ExtractTemplateVariables<Template>, ReservedTemplateVariable>;
|
|
21
|
+
type RejectReservedTemplateVariables<Template extends string> = [ReservedVariablesIn<Template>] extends [never] ? Template : never;
|
|
22
|
+
type ErrorBaseConstructor<Base extends Error = Error> = new (message?: string, options?: ErrorOptions) => Base;
|
|
23
|
+
type ErrorClass<Base extends Error = Error> = new (...args: readonly any[]) => Base;
|
|
24
|
+
type ErrorClassInstance<This> = This extends (new (...args: readonly any[]) => infer Instance) ? Instance : never;
|
|
25
|
+
type ConstructorProps<Template extends string> = TemplateProps<Template> & {
|
|
26
|
+
readonly cause?: unknown;
|
|
27
|
+
};
|
|
28
|
+
type TaggedErrorConstructorArgs<Template extends string> = [ExtractTemplateVariables<Template>] extends [never] ? [props?: {
|
|
29
|
+
readonly cause?: unknown;
|
|
30
|
+
}] : [props: ConstructorProps<Template>];
|
|
31
|
+
interface TaggedErrorOptions<Tag extends string, MessageTemplate extends string, Base extends Error = Error> {
|
|
32
|
+
readonly extends?: ErrorBaseConstructor<Base>;
|
|
33
|
+
readonly message?: MessageTemplate;
|
|
34
|
+
readonly name: Tag;
|
|
35
|
+
}
|
|
36
|
+
type TaggedErrorInstance<Tag extends string, MessageTemplate extends string, Base extends Error = Error> = Base & Readonly<TemplateProps<MessageTemplate>> & {
|
|
37
|
+
readonly _tag: Tag;
|
|
38
|
+
readonly fingerprint: readonly [Tag, MessageTemplate];
|
|
39
|
+
readonly messageTemplate: MessageTemplate;
|
|
40
|
+
findCause<T extends Error>(ErrorClass: ErrorClass<T>): T | undefined;
|
|
41
|
+
toJSON(): object;
|
|
42
|
+
};
|
|
43
|
+
type TaggedErrorClass<Tag extends string, MessageTemplate extends string, Base extends Error = Error> = {
|
|
44
|
+
new (...args: TaggedErrorConstructorArgs<MessageTemplate>): TaggedErrorInstance<Tag, MessageTemplate, Base>;
|
|
45
|
+
readonly tag: Tag;
|
|
46
|
+
err<This extends new (...args: TaggedErrorConstructorArgs<MessageTemplate>) => Error>(this: This, ...args: TaggedErrorConstructorArgs<MessageTemplate>): ErrResult<never, ErrorClassInstance<This>>;
|
|
47
|
+
is(value: unknown): value is TaggedErrorInstance<Tag, MessageTemplate, Base>;
|
|
48
|
+
};
|
|
49
|
+
type TaggedEnum<Members extends Record<string, Record<PropertyKey, unknown>>> = { readonly [Tag in keyof Members]: Readonly<Members[Tag]> & {
|
|
50
|
+
readonly _tag: Tag;
|
|
51
|
+
} }[keyof Members];
|
|
52
|
+
type TaggedErrorLike = Error & {
|
|
53
|
+
readonly _tag: string;
|
|
54
|
+
};
|
|
55
|
+
type TaggedErrorTags<ErrorType extends Error> = Extract<ErrorType, TaggedErrorLike>["_tag"];
|
|
56
|
+
type ErrorForTag<ErrorType extends Error, Tag extends string> = Extract<ErrorType, {
|
|
57
|
+
readonly _tag: Tag;
|
|
58
|
+
}>;
|
|
59
|
+
type UntaggedError<ErrorType extends Error> = Exclude<ErrorType, TaggedErrorLike>;
|
|
60
|
+
type MatchHandlers<ErrorType extends Error, ReturnType> = { readonly [Tag in TaggedErrorTags<ErrorType>]: (error: ErrorForTag<ErrorType, Tag>) => ReturnType } & ([UntaggedError<ErrorType>] extends [never] ? {
|
|
61
|
+
readonly Error?: (error: Error) => ReturnType;
|
|
62
|
+
} : {
|
|
63
|
+
readonly Error: (error: UntaggedError<ErrorType>) => ReturnType;
|
|
64
|
+
});
|
|
65
|
+
type PartialMatchHandlers<ErrorType extends Error, ReturnType> = Partial<MatchHandlers<ErrorType, ReturnType>>;
|
|
66
|
+
declare const defaultMessageTemplate = "$message";
|
|
67
|
+
declare function isError(value: unknown): value is Error;
|
|
68
|
+
declare function isError<Value>(value: Value): value is Extract<Value, Error>;
|
|
69
|
+
declare const findCause: <T extends Error>(error: Error, ErrorClass: ErrorClass<T>) => T | undefined;
|
|
70
|
+
declare function createTaggedError<const Tag extends string, const MessageTemplate extends string = typeof defaultMessageTemplate, Base extends Error = Error>(options: Omit<TaggedErrorOptions<Tag, MessageTemplate, Base>, "message"> & {
|
|
71
|
+
readonly message?: RejectReservedTemplateVariables<MessageTemplate>;
|
|
72
|
+
}): TaggedErrorClass<Tag, MessageTemplate, Base>;
|
|
73
|
+
declare const matchError: <ErrorType extends Error, ReturnType>(error: ErrorType, handlers: MatchHandlers<ErrorType, ReturnType>) => ReturnType;
|
|
74
|
+
declare const matchErrorPartial: <ErrorType extends Error, ReturnType>(error: ErrorType, handlers: PartialMatchHandlers<ErrorType, ReturnType>, fallback: (error: ErrorType) => ReturnType) => ReturnType;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { DisposableResult, DisposableResultAsync, type ErrResult, type OkResult, Result, ResultAsync, type ResultOperations, type StrictResult, type StrictResultAsync, type TaggedEnum, type TaggedErrorClass, type TaggedErrorInstance, type TaggedErrorOptions, createTaggedError, err, errAsync, findCause, fromPromise, fromSafePromise, fromThrowable, fromThrowableAsync, isError, matchError, matchErrorPartial, ok, okAsync, safeTry, tryCatch, tryCatchAsync, unit, unitAsync };
|
|
77
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,128 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fromSafePromise,
|
|
27
|
-
fromThrowable,
|
|
28
|
-
fromThrowableAsync,
|
|
29
|
-
ok,
|
|
30
|
-
okAsync,
|
|
31
|
-
safeTry,
|
|
32
|
-
safeTryAsync,
|
|
33
|
-
tryCatch,
|
|
34
|
-
tryCatchAsync,
|
|
35
|
-
unit,
|
|
36
|
-
unitAsync
|
|
1
|
+
import { _ as unit, a as fromSafePromise, c as tryCatchAsync, d as Result, f as err, g as tryCatch, h as safeTry, i as fromPromise, l as unitAsync, m as ok, n as ResultAsync, o as fromThrowableAsync, p as fromThrowable, r as errAsync, s as okAsync, t as DisposableResultAsync, u as DisposableResult } from "./result-async-CdlVdXgX.js";
|
|
2
|
+
//#region src/tagged-error.ts
|
|
3
|
+
const defaultBaseError = Error;
|
|
4
|
+
const defaultMessageTemplate = "$message";
|
|
5
|
+
const nativeError = Error;
|
|
6
|
+
const templateVariableMatcher = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
7
|
+
const reservedTemplateVariables = new Set([
|
|
8
|
+
"_tag",
|
|
9
|
+
"cause",
|
|
10
|
+
"fingerprint",
|
|
11
|
+
"message",
|
|
12
|
+
"messageTemplate",
|
|
13
|
+
"name",
|
|
14
|
+
"stack"
|
|
15
|
+
]);
|
|
16
|
+
function isError(value) {
|
|
17
|
+
return nativeError.isError?.(value) ?? value instanceof Error;
|
|
18
|
+
}
|
|
19
|
+
const stringifyTemplateValue = (value) => {
|
|
20
|
+
if (typeof value === "string") return value;
|
|
21
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value.toString();
|
|
22
|
+
if (typeof value === "symbol") return value.description ?? value.toString();
|
|
23
|
+
if (typeof value === "function") return value.name;
|
|
24
|
+
if (typeof value === "object") return value === null ? "null" : JSON.stringify(value) ?? Object.prototype.toString.call(value);
|
|
25
|
+
return "";
|
|
37
26
|
};
|
|
27
|
+
const compileMessageInterpolator = (template) => (values) => template.replaceAll(templateVariableMatcher, (variable, key) => {
|
|
28
|
+
const value = values?.[key];
|
|
29
|
+
return value === void 0 ? variable : stringifyTemplateValue(value);
|
|
30
|
+
});
|
|
31
|
+
const getTemplateVariableNames = (template) => {
|
|
32
|
+
templateVariableMatcher.lastIndex = 0;
|
|
33
|
+
return [...template.matchAll(templateVariableMatcher)].map((match) => String(match[1]));
|
|
34
|
+
};
|
|
35
|
+
const assertNoReservedTemplateVariables = (tag, template) => {
|
|
36
|
+
if (template === defaultMessageTemplate) return;
|
|
37
|
+
const reservedVariables = getTemplateVariableNames(template).filter((variable) => reservedTemplateVariables.has(variable));
|
|
38
|
+
if (reservedVariables.length > 0) throw new Error(`createTaggedError(${tag}): reserved template variable $${reservedVariables[0]} is not allowed`);
|
|
39
|
+
};
|
|
40
|
+
const getErrorOptions = (cause) => cause === void 0 ? {} : { cause };
|
|
41
|
+
const assignReadonly = (target, key, value) => {
|
|
42
|
+
Object.defineProperty(target, key, {
|
|
43
|
+
configurable: true,
|
|
44
|
+
enumerable: true,
|
|
45
|
+
value,
|
|
46
|
+
writable: false
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
const serializeCause = (cause) => {
|
|
50
|
+
if (!isError(cause)) return cause;
|
|
51
|
+
return {
|
|
52
|
+
message: cause.message,
|
|
53
|
+
name: cause.name,
|
|
54
|
+
stack: cause.stack
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
const findCause = (error, ErrorClass) => {
|
|
58
|
+
const seen = /* @__PURE__ */ new Set();
|
|
59
|
+
let current = error;
|
|
60
|
+
while (isError(current)) {
|
|
61
|
+
if (seen.has(current)) break;
|
|
62
|
+
seen.add(current);
|
|
63
|
+
if (current instanceof ErrorClass) return current;
|
|
64
|
+
current = current.cause;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function createTaggedError(options) {
|
|
68
|
+
const BaseError = options.extends ?? defaultBaseError;
|
|
69
|
+
const messageTemplate = options.message ?? defaultMessageTemplate;
|
|
70
|
+
assertNoReservedTemplateVariables(options.name, messageTemplate);
|
|
71
|
+
const interpolate = compileMessageInterpolator(messageTemplate);
|
|
72
|
+
class GeneratedTaggedError extends BaseError {
|
|
73
|
+
static tag = options.name;
|
|
74
|
+
_tag = options.name;
|
|
75
|
+
fingerprint = [options.name, messageTemplate];
|
|
76
|
+
messageTemplate = messageTemplate;
|
|
77
|
+
constructor(props) {
|
|
78
|
+
const message = interpolate(props);
|
|
79
|
+
super(message, getErrorOptions(props?.cause));
|
|
80
|
+
this.name = options.name;
|
|
81
|
+
if (props) {
|
|
82
|
+
for (const [key, value] of Object.entries(props)) if (!reservedTemplateVariables.has(key)) assignReadonly(this, key, value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
static is(value) {
|
|
86
|
+
return value instanceof this;
|
|
87
|
+
}
|
|
88
|
+
static err(...args) {
|
|
89
|
+
return err(new this(...args));
|
|
90
|
+
}
|
|
91
|
+
findCause(ErrorClass) {
|
|
92
|
+
return findCause(this, ErrorClass);
|
|
93
|
+
}
|
|
94
|
+
toJSON() {
|
|
95
|
+
const json = {
|
|
96
|
+
_tag: this._tag,
|
|
97
|
+
fingerprint: this.fingerprint,
|
|
98
|
+
message: this.message,
|
|
99
|
+
messageTemplate: this.messageTemplate,
|
|
100
|
+
name: this.name
|
|
101
|
+
};
|
|
102
|
+
if (this.cause !== void 0) json["cause"] = serializeCause(this.cause);
|
|
103
|
+
for (const [key, value] of Object.entries(this)) if (!(key in json) && key !== "cause" && key !== "stack") json[key] = value;
|
|
104
|
+
return json;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return GeneratedTaggedError;
|
|
108
|
+
}
|
|
109
|
+
const matchError = (error, handlers) => {
|
|
110
|
+
if ("_tag" in error && typeof error._tag === "string" && error._tag in handlers) {
|
|
111
|
+
const handler = handlers[error._tag];
|
|
112
|
+
return handler(error);
|
|
113
|
+
}
|
|
114
|
+
if (handlers.Error) return handlers.Error(error);
|
|
115
|
+
throw error;
|
|
116
|
+
};
|
|
117
|
+
const matchErrorPartial = (error, handlers, fallback) => {
|
|
118
|
+
if ("_tag" in error && typeof error._tag === "string" && error._tag in handlers) {
|
|
119
|
+
const handler = handlers[error._tag];
|
|
120
|
+
if (handler) return handler(error);
|
|
121
|
+
}
|
|
122
|
+
if (handlers.Error) return handlers.Error(error);
|
|
123
|
+
return fallback(error);
|
|
124
|
+
};
|
|
125
|
+
//#endregion
|
|
126
|
+
export { DisposableResult, DisposableResultAsync, Result, ResultAsync, createTaggedError, err, errAsync, findCause, fromPromise, fromSafePromise, fromThrowable, fromThrowableAsync, isError, matchError, matchErrorPartial, ok, okAsync, safeTry, tryCatch, tryCatchAsync, unit, unitAsync };
|
|
127
|
+
|
|
38
128
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"file":"index.js","names":["resultErr"],"sources":["../src/tagged-error.ts"],"sourcesContent":["import type { ErrResult } from './result.js'\n\nimport { err as resultErr } from './result.js'\n\ntype Alpha =\n | 'a'\n | 'b'\n | 'c'\n | 'd'\n | 'e'\n | 'f'\n | 'g'\n | 'h'\n | 'i'\n | 'j'\n | 'k'\n | 'l'\n | 'm'\n | 'n'\n | 'o'\n | 'p'\n | 'q'\n | 'r'\n | 's'\n | 't'\n | 'u'\n | 'v'\n | 'w'\n | 'x'\n | 'y'\n | 'z'\n | 'A'\n | 'B'\n | 'C'\n | 'D'\n | 'E'\n | 'F'\n | 'G'\n | 'H'\n | 'I'\n | 'J'\n | 'K'\n | 'L'\n | 'M'\n | 'N'\n | 'O'\n | 'P'\n | 'Q'\n | 'R'\n | 'S'\n | 'T'\n | 'U'\n | 'V'\n | 'W'\n | 'X'\n | 'Y'\n | 'Z'\n | '_'\ntype Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'\ntype AlphaNumeric = Alpha | Digit\ntype ReservedTemplateVariable =\n | '_tag'\n | 'cause'\n | 'fingerprint'\n | 'message'\n | 'messageTemplate'\n | 'name'\n | 'stack'\n\ntype ConsumeTemplateVariable<\n Template extends string,\n Accumulator extends string = '',\n> = Template extends `${infer Character}${infer Rest}`\n ? Character extends AlphaNumeric\n ? ConsumeTemplateVariable<Rest, `${Accumulator}${Character}`>\n : { readonly rest: Template; readonly variable: Accumulator }\n : { readonly rest: ''; readonly variable: Accumulator }\n\ntype ExtractTemplateVariables<Template extends string> =\n Template extends `${string}$${infer AfterDollar}`\n ? AfterDollar extends `${Alpha}${string}`\n ? ConsumeTemplateVariable<AfterDollar> extends {\n readonly rest: infer Rest extends string\n readonly variable: infer Variable extends string\n }\n ? Variable | ExtractTemplateVariables<Rest>\n : never\n : ExtractTemplateVariables<AfterDollar>\n : never\n\ntype TemplateProps<Template extends string> = [ExtractTemplateVariables<Template>] extends [never]\n ? Record<never, never>\n : Readonly<Record<ExtractTemplateVariables<Template>, string | number>>\ntype ReservedVariablesIn<Template extends string> = Extract<\n ExtractTemplateVariables<Template>,\n ReservedTemplateVariable\n>\ntype RejectReservedTemplateVariables<Template extends string> = [\n ReservedVariablesIn<Template>,\n] extends [never]\n ? Template\n : never\n\ntype ErrorBaseConstructor<Base extends Error = Error> = new (\n message?: string,\n options?: ErrorOptions,\n) => Base\ntype ErrorClass<Base extends Error = Error> = new (...args: readonly any[]) => Base\ntype ErrorClassInstance<This> = This extends new (...args: readonly any[]) => infer Instance\n ? Instance\n : never\ntype ConstructorProps<Template extends string> = TemplateProps<Template> & {\n readonly cause?: unknown\n}\ntype TaggedErrorConstructorArgs<Template extends string> = [\n ExtractTemplateVariables<Template>,\n] extends [never]\n ? [props?: { readonly cause?: unknown }]\n : [props: ConstructorProps<Template>]\n\nexport interface TaggedErrorOptions<\n Tag extends string,\n MessageTemplate extends string,\n Base extends Error = Error,\n> {\n readonly extends?: ErrorBaseConstructor<Base>\n readonly message?: MessageTemplate\n readonly name: Tag\n}\n\nexport type TaggedErrorInstance<\n Tag extends string,\n MessageTemplate extends string,\n Base extends Error = Error,\n> = Base &\n Readonly<TemplateProps<MessageTemplate>> & {\n readonly _tag: Tag\n readonly fingerprint: readonly [Tag, MessageTemplate]\n readonly messageTemplate: MessageTemplate\n findCause<T extends Error>(ErrorClass: ErrorClass<T>): T | undefined\n toJSON(): object\n }\n\nexport type TaggedErrorClass<\n Tag extends string,\n MessageTemplate extends string,\n Base extends Error = Error,\n> = {\n new (\n ...args: TaggedErrorConstructorArgs<MessageTemplate>\n ): TaggedErrorInstance<Tag, MessageTemplate, Base>\n readonly tag: Tag\n err<This extends new (...args: TaggedErrorConstructorArgs<MessageTemplate>) => Error>(\n this: This,\n ...args: TaggedErrorConstructorArgs<MessageTemplate>\n ): ErrResult<never, ErrorClassInstance<This>>\n is(value: unknown): value is TaggedErrorInstance<Tag, MessageTemplate, Base>\n}\n\nexport type TaggedEnum<Members extends Record<string, Record<PropertyKey, unknown>>> = {\n readonly [Tag in keyof Members]: Readonly<Members[Tag]> & { readonly _tag: Tag }\n}[keyof Members]\n\ntype TaggedErrorLike = Error & { readonly _tag: string }\ntype TaggedErrorTags<ErrorType extends Error> = Extract<ErrorType, TaggedErrorLike>['_tag']\ntype ErrorForTag<ErrorType extends Error, Tag extends string> = Extract<\n ErrorType,\n { readonly _tag: Tag }\n>\ntype UntaggedError<ErrorType extends Error> = Exclude<ErrorType, TaggedErrorLike>\ntype MatchHandlers<ErrorType extends Error, ReturnType> = {\n readonly [Tag in TaggedErrorTags<ErrorType>]: (error: ErrorForTag<ErrorType, Tag>) => ReturnType\n} & ([UntaggedError<ErrorType>] extends [never]\n ? { readonly Error?: (error: Error) => ReturnType }\n : { readonly Error: (error: UntaggedError<ErrorType>) => ReturnType })\ntype PartialMatchHandlers<ErrorType extends Error, ReturnType> = Partial<\n MatchHandlers<ErrorType, ReturnType>\n>\n\nconst defaultBaseError: ErrorBaseConstructor = Error\nconst defaultMessageTemplate = '$message'\n\nconst nativeError = Error as typeof Error & { isError?: (value: unknown) => boolean }\nconst templateVariableMatcher = /\\$([a-zA-Z_][a-zA-Z0-9_]*)/g\nconst reservedTemplateVariables = new Set<ReservedTemplateVariable>([\n '_tag',\n 'cause',\n 'fingerprint',\n 'message',\n 'messageTemplate',\n 'name',\n 'stack',\n])\n\nexport function isError(value: unknown): value is Error\nexport function isError<Value>(value: Value): value is Extract<Value, Error>\nexport function isError(value: unknown): value is Error {\n return nativeError.isError?.(value) ?? value instanceof Error\n}\n\nconst stringifyTemplateValue = (value: unknown): string => {\n if (typeof value === 'string') {\n return value\n }\n\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return value.toString()\n }\n\n if (typeof value === 'symbol') {\n return value.description ?? value.toString()\n }\n\n if (typeof value === 'function') {\n return value.name\n }\n\n if (typeof value === 'object') {\n return value === null\n ? 'null'\n : (JSON.stringify(value) ?? Object.prototype.toString.call(value))\n }\n\n return ''\n}\n\nconst compileMessageInterpolator =\n (template: string): ((values?: Record<string, unknown>) => string) =>\n (values) =>\n template.replaceAll(templateVariableMatcher, (variable, key: string) => {\n const value = values?.[key]\n return value === undefined ? variable : stringifyTemplateValue(value)\n })\n\nconst getTemplateVariableNames = (template: string): readonly string[] => {\n templateVariableMatcher.lastIndex = 0\n return [...template.matchAll(templateVariableMatcher)].map((match) => String(match[1]))\n}\n\nconst assertNoReservedTemplateVariables = (tag: string, template: string): void => {\n if (template === defaultMessageTemplate) {\n return\n }\n\n const reservedVariables = getTemplateVariableNames(template).filter(\n (variable): variable is ReservedTemplateVariable =>\n reservedTemplateVariables.has(variable as ReservedTemplateVariable),\n )\n\n if (reservedVariables.length > 0) {\n throw new Error(\n `createTaggedError(${tag}): reserved template variable $${reservedVariables[0]} is not allowed`,\n )\n }\n}\n\nconst getErrorOptions = (cause: unknown): { cause?: unknown } =>\n cause === undefined ? {} : { cause }\n\nconst assignReadonly = (target: object, key: PropertyKey, value: unknown): void => {\n Object.defineProperty(target, key, {\n configurable: true,\n enumerable: true,\n value,\n writable: false,\n })\n}\n\nconst serializeCause = (cause: unknown): unknown => {\n if (!isError(cause)) {\n return cause\n }\n\n return { message: cause.message, name: cause.name, stack: cause.stack }\n}\n\nexport const findCause = <T extends Error>(\n error: Error,\n ErrorClass: ErrorClass<T>,\n): T | undefined => {\n const seen = new Set<Error>()\n let current: unknown = error\n\n while (isError(current)) {\n if (seen.has(current)) {\n break\n }\n\n seen.add(current)\n\n if (current instanceof ErrorClass) {\n return current\n }\n\n current = current.cause\n }\n\n return undefined\n}\n\nexport function createTaggedError<\n const Tag extends string,\n const MessageTemplate extends string = typeof defaultMessageTemplate,\n Base extends Error = Error,\n>(\n options: Omit<TaggedErrorOptions<Tag, MessageTemplate, Base>, 'message'> & {\n readonly message?: RejectReservedTemplateVariables<MessageTemplate>\n },\n): TaggedErrorClass<Tag, MessageTemplate, Base> {\n const BaseError = options.extends ?? defaultBaseError\n const messageTemplate = options.message ?? (defaultMessageTemplate as MessageTemplate)\n assertNoReservedTemplateVariables(options.name, messageTemplate)\n const interpolate = compileMessageInterpolator(messageTemplate)\n\n class GeneratedTaggedError extends BaseError {\n static readonly tag = options.name\n readonly _tag = options.name\n readonly fingerprint = [options.name, messageTemplate] as const\n readonly messageTemplate = messageTemplate\n\n constructor(props?: ConstructorProps<MessageTemplate>) {\n const message = interpolate(props)\n super(message, getErrorOptions(props?.cause))\n // eslint-disable-next-line nicorn/custom-error-definition\n this.name = options.name\n\n if (props) {\n for (const [key, value] of Object.entries(props)) {\n if (!reservedTemplateVariables.has(key as ReservedTemplateVariable)) {\n assignReadonly(this, key, value)\n }\n }\n }\n }\n\n static is(value: unknown): value is TaggedErrorInstance<Tag, MessageTemplate, Base> {\n return value instanceof this\n }\n\n static err<This extends new (...args: TaggedErrorConstructorArgs<MessageTemplate>) => Error>(\n this: This,\n ...args: TaggedErrorConstructorArgs<MessageTemplate>\n ): ErrResult<never, InstanceType<This>> {\n const error = new this(...args) as InstanceType<This>\n return resultErr<never, InstanceType<This>>(error)\n }\n\n findCause<T extends Error>(ErrorClass: ErrorClass<T>): T | undefined {\n return findCause(this, ErrorClass)\n }\n\n toJSON(): object {\n const json: Record<string, unknown> = {\n _tag: this._tag,\n fingerprint: this.fingerprint,\n message: this.message,\n messageTemplate: this.messageTemplate,\n name: this.name,\n }\n\n if (this.cause !== undefined) {\n json['cause'] = serializeCause(this.cause)\n }\n\n for (const [key, value] of Object.entries(this)) {\n if (!(key in json) && key !== 'cause' && key !== 'stack') {\n json[key] = value\n }\n }\n\n return json\n }\n }\n\n return GeneratedTaggedError as TaggedErrorClass<Tag, MessageTemplate, Base>\n}\n\nexport const matchError = <ErrorType extends Error, ReturnType>(\n error: ErrorType,\n handlers: MatchHandlers<ErrorType, ReturnType>,\n): ReturnType => {\n if ('_tag' in error && typeof error._tag === 'string' && error._tag in handlers) {\n const handler = handlers[error._tag as keyof typeof handlers] as (\n error: ErrorType,\n ) => ReturnType\n return handler(error)\n }\n\n if (handlers.Error) {\n return (handlers.Error as (error: ErrorType) => ReturnType)(error)\n }\n\n throw error\n}\n\nexport const matchErrorPartial = <ErrorType extends Error, ReturnType>(\n error: ErrorType,\n handlers: PartialMatchHandlers<ErrorType, ReturnType>,\n fallback: (error: ErrorType) => ReturnType,\n): ReturnType => {\n if ('_tag' in error && typeof error._tag === 'string' && error._tag in handlers) {\n const handler = handlers[error._tag as keyof typeof handlers]\n if (handler) {\n return (handler as (error: ErrorType) => ReturnType)(error)\n }\n }\n\n if (handlers.Error) {\n return (handlers.Error as (error: ErrorType) => ReturnType)(error)\n }\n\n return fallback(error)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { m as Result } from "./result-async-CxGBicja.js";
|
|
2
|
+
|
|
3
|
+
//#region src/no-discard.d.ts
|
|
4
|
+
interface NoDiscardFinding {
|
|
5
|
+
readonly column: number;
|
|
6
|
+
readonly file: string;
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly type: string;
|
|
9
|
+
}
|
|
10
|
+
interface NoDiscardOptions {
|
|
11
|
+
readonly project?: string;
|
|
12
|
+
readonly rootDir?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const findDiscardedResults: (options?: NoDiscardOptions) => Result<readonly NoDiscardFinding[], Error>;
|
|
15
|
+
declare const runNoDiscardCli: (args?: readonly string[]) => number;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { NoDiscardFinding, NoDiscardOptions, findDiscardedResults, runNoDiscardCli };
|
|
18
|
+
//# sourceMappingURL=no-discard.d.ts.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { f as err, m as ok } from "./result-async-CdlVdXgX.js";
|
|
3
|
+
import { relative, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import ts from "typescript";
|
|
6
|
+
//#region src/no-discard.ts
|
|
7
|
+
const resultTypeMatcher = /\b(?:DisposableResult|DisposableResultAsync|ErrResult|OkResult|Result|ResultAsync|StrictResult|StrictResultAsync)\b/;
|
|
8
|
+
const formatHost = {
|
|
9
|
+
getCanonicalFileName: (fileName) => fileName,
|
|
10
|
+
getCurrentDirectory: () => process.cwd(),
|
|
11
|
+
getNewLine: () => "\n"
|
|
12
|
+
};
|
|
13
|
+
const usage = `Usage: resultar-no-discard [--project tsconfig.json]
|
|
14
|
+
|
|
15
|
+
Flags:
|
|
16
|
+
-p, --project <path> TypeScript project file to inspect. Defaults to tsconfig.json.
|
|
17
|
+
-h, --help Show this help message.
|
|
18
|
+
`;
|
|
19
|
+
const cliError = (message) => err(new Error(message));
|
|
20
|
+
const parseArgs = (args) => {
|
|
21
|
+
let project = void 0;
|
|
22
|
+
let help = false;
|
|
23
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
24
|
+
const arg = args[index];
|
|
25
|
+
if (arg === "--help" || arg === "-h") help = true;
|
|
26
|
+
else if (arg === "--project" || arg === "-p") {
|
|
27
|
+
const nextArg = args[index + 1];
|
|
28
|
+
if (nextArg === void 0 || nextArg === "") return cliError(`${arg} requires a path`);
|
|
29
|
+
project = nextArg;
|
|
30
|
+
index += 1;
|
|
31
|
+
} else if (arg !== void 0 && arg.startsWith("--project=")) project = arg.slice(10);
|
|
32
|
+
else if (arg !== void 0 && arg !== "") return cliError(`Unknown argument: ${arg}`);
|
|
33
|
+
}
|
|
34
|
+
if (project === "") return cliError("--project requires a path");
|
|
35
|
+
return project === void 0 ? ok({ help }) : ok({
|
|
36
|
+
help,
|
|
37
|
+
project
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const readProject = (projectPath) => {
|
|
41
|
+
const config = ts.readConfigFile(projectPath, (fileName) => ts.sys.readFile(fileName));
|
|
42
|
+
if (config.error) return err(new Error(ts.formatDiagnosticsWithColorAndContext([config.error], formatHost)));
|
|
43
|
+
const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, resolve(projectPath, ".."), void 0, projectPath);
|
|
44
|
+
if (parsed.errors.length > 0) return err(new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, formatHost)));
|
|
45
|
+
return ok(parsed);
|
|
46
|
+
};
|
|
47
|
+
const isResultLikeType = (checker, node, type) => {
|
|
48
|
+
if (type.isUnionOrIntersection()) return type.types.some((innerType) => isResultLikeType(checker, node, innerType));
|
|
49
|
+
const typeName = checker.typeToString(type, node, ts.TypeFormatFlags.NoTruncation + ts.TypeFormatFlags.UseFullyQualifiedType);
|
|
50
|
+
return resultTypeMatcher.test(typeName);
|
|
51
|
+
};
|
|
52
|
+
const unwrapExpression = (expression) => {
|
|
53
|
+
let current = expression;
|
|
54
|
+
while (ts.isParenthesizedExpression(current)) current = current.expression;
|
|
55
|
+
return current;
|
|
56
|
+
};
|
|
57
|
+
const isExplicitDiscard = (expression) => ts.isVoidExpression(unwrapExpression(expression));
|
|
58
|
+
const isCallLikeDiscard = (expression) => {
|
|
59
|
+
const unwrapped = unwrapExpression(expression);
|
|
60
|
+
if (ts.isAwaitExpression(unwrapped)) return isCallLikeDiscard(unwrapped.expression);
|
|
61
|
+
if (ts.isCallExpression(unwrapped)) return true;
|
|
62
|
+
if (ts.isConditionalExpression(unwrapped)) return isCallLikeDiscard(unwrapped.whenTrue) || isCallLikeDiscard(unwrapped.whenFalse);
|
|
63
|
+
if (ts.isBinaryExpression(unwrapped) && [
|
|
64
|
+
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
65
|
+
ts.SyntaxKind.BarBarToken,
|
|
66
|
+
ts.SyntaxKind.QuestionQuestionToken
|
|
67
|
+
].includes(unwrapped.operatorToken.kind)) return isCallLikeDiscard(unwrapped.right);
|
|
68
|
+
return false;
|
|
69
|
+
};
|
|
70
|
+
const inspectSourceFile = (checker, sourceFile) => {
|
|
71
|
+
const findings = [];
|
|
72
|
+
const visit = (node) => {
|
|
73
|
+
if (ts.isExpressionStatement(node) && !isExplicitDiscard(node.expression) && isCallLikeDiscard(node.expression)) {
|
|
74
|
+
const type = checker.getTypeAtLocation(node.expression);
|
|
75
|
+
if (isResultLikeType(checker, node.expression, type)) {
|
|
76
|
+
const position = sourceFile.getLineAndCharacterOfPosition(node.expression.getStart());
|
|
77
|
+
findings.push({
|
|
78
|
+
column: position.character + 1,
|
|
79
|
+
file: sourceFile.fileName,
|
|
80
|
+
line: position.line + 1,
|
|
81
|
+
type: checker.typeToString(type, node.expression, ts.TypeFormatFlags.NoTruncation)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
ts.forEachChild(node, visit);
|
|
86
|
+
};
|
|
87
|
+
visit(sourceFile);
|
|
88
|
+
return findings;
|
|
89
|
+
};
|
|
90
|
+
const findDiscardedResults = (options = {}) => {
|
|
91
|
+
return readProject(resolve(resolve(options.rootDir ?? process.cwd()), options.project ?? "tsconfig.json")).map((parsed) => {
|
|
92
|
+
const program = ts.createProgram(parsed.fileNames, parsed.options);
|
|
93
|
+
const checker = program.getTypeChecker();
|
|
94
|
+
const findings = [];
|
|
95
|
+
for (const sourceFile of program.getSourceFiles()) if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes("/node_modules/")) findings.push(...inspectSourceFile(checker, sourceFile));
|
|
96
|
+
return findings;
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const formatFinding = (finding, rootDir) => {
|
|
100
|
+
return [`${relative(rootDir, finding.file)}:${finding.line}:${finding.column} no-discard-result`, `Ignored ${finding.type} value. Handle it or explicitly discard it with \`void\`.`].join(" - ");
|
|
101
|
+
};
|
|
102
|
+
const runNoDiscardCli = (args = process.argv.slice(2)) => {
|
|
103
|
+
const rootDir = process.cwd();
|
|
104
|
+
return parseArgs(args).andThen((options) => {
|
|
105
|
+
if (!options.help) return options.project === void 0 ? findDiscardedResults({ rootDir }) : findDiscardedResults({
|
|
106
|
+
project: options.project,
|
|
107
|
+
rootDir
|
|
108
|
+
});
|
|
109
|
+
process.stdout.write(usage);
|
|
110
|
+
return ok([]);
|
|
111
|
+
}).match((findings) => {
|
|
112
|
+
if (findings.length === 0) return 0;
|
|
113
|
+
process.stderr.write(`${findings.map((finding) => formatFinding(finding, rootDir)).join("\n")}\n`);
|
|
114
|
+
return 1;
|
|
115
|
+
}, (error) => {
|
|
116
|
+
process.stderr.write(`${error.message}\n`);
|
|
117
|
+
return 1;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) process.exitCode = runNoDiscardCli();
|
|
121
|
+
//#endregion
|
|
122
|
+
export { findDiscardedResults, runNoDiscardCli };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=no-discard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-discard.js","names":[],"sources":["../src/no-discard.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { relative, resolve } from 'node:path'\nimport { pathToFileURL } from 'node:url'\n\nimport ts from 'typescript'\n\nimport type { Result } from './result.js'\n\nimport { err, ok } from './result.js'\n\nconst resultTypeMatcher =\n /\\b(?:DisposableResult|DisposableResultAsync|ErrResult|OkResult|Result|ResultAsync|StrictResult|StrictResultAsync)\\b/\n\nexport interface NoDiscardFinding {\n readonly column: number\n readonly file: string\n readonly line: number\n readonly type: string\n}\n\nexport interface NoDiscardOptions {\n readonly project?: string\n readonly rootDir?: string\n}\n\ninterface CliOptions extends NoDiscardOptions {\n readonly help: boolean\n}\n\nconst formatHost: ts.FormatDiagnosticsHost = {\n getCanonicalFileName: (fileName) => fileName,\n getCurrentDirectory: () => process.cwd(),\n getNewLine: () => '\\n',\n}\n\nconst usage = `Usage: resultar-no-discard [--project tsconfig.json]\n\nFlags:\n -p, --project <path> TypeScript project file to inspect. Defaults to tsconfig.json.\n -h, --help Show this help message.\n`\n\nconst cliError = (message: string): Result<never, Error> => err(new Error(message))\n\nconst parseArgs = (args: readonly string[]): Result<CliOptions, Error> => {\n let project: string | undefined = undefined\n let help = false\n\n for (let index = 0; index < args.length; index += 1) {\n const arg = args[index]\n\n if (arg === '--help' || arg === '-h') {\n help = true\n } else if (arg === '--project' || arg === '-p') {\n const nextArg = args[index + 1]\n\n if (nextArg === undefined || nextArg === '') {\n return cliError(`${arg} requires a path`)\n }\n\n project = nextArg\n index += 1\n } else if (arg !== undefined && arg.startsWith('--project=')) {\n project = arg.slice('--project='.length)\n } else if (arg !== undefined && arg !== '') {\n return cliError(`Unknown argument: ${arg}`)\n }\n }\n\n if (project === '') {\n return cliError('--project requires a path')\n }\n\n return project === undefined ? ok({ help }) : ok({ help, project })\n}\n\nconst readProject = (projectPath: string): Result<ts.ParsedCommandLine, Error> => {\n const config = ts.readConfigFile(projectPath, (fileName) => ts.sys.readFile(fileName))\n\n if (config.error) {\n return err(new Error(ts.formatDiagnosticsWithColorAndContext([config.error], formatHost)))\n }\n\n const parsed = ts.parseJsonConfigFileContent(\n config.config,\n ts.sys,\n resolve(projectPath, '..'),\n undefined,\n projectPath,\n )\n\n if (parsed.errors.length > 0) {\n return err(new Error(ts.formatDiagnosticsWithColorAndContext(parsed.errors, formatHost)))\n }\n\n return ok(parsed)\n}\n\nconst isResultLikeType = (checker: ts.TypeChecker, node: ts.Node, type: ts.Type): boolean => {\n if (type.isUnionOrIntersection()) {\n return type.types.some((innerType) => isResultLikeType(checker, node, innerType))\n }\n\n const typeName = checker.typeToString(\n type,\n node,\n ts.TypeFormatFlags.NoTruncation + ts.TypeFormatFlags.UseFullyQualifiedType,\n )\n\n return resultTypeMatcher.test(typeName)\n}\n\nconst unwrapExpression = (expression: ts.Expression): ts.Expression => {\n let current = expression\n\n while (ts.isParenthesizedExpression(current)) {\n current = current.expression\n }\n\n return current\n}\n\nconst isExplicitDiscard = (expression: ts.Expression): boolean =>\n ts.isVoidExpression(unwrapExpression(expression))\n\nconst isCallLikeDiscard = (expression: ts.Expression): boolean => {\n const unwrapped = unwrapExpression(expression)\n\n if (ts.isAwaitExpression(unwrapped)) {\n return isCallLikeDiscard(unwrapped.expression)\n }\n\n if (ts.isCallExpression(unwrapped)) {\n return true\n }\n\n if (ts.isConditionalExpression(unwrapped)) {\n return isCallLikeDiscard(unwrapped.whenTrue) || isCallLikeDiscard(unwrapped.whenFalse)\n }\n\n if (\n ts.isBinaryExpression(unwrapped) &&\n [\n ts.SyntaxKind.AmpersandAmpersandToken,\n ts.SyntaxKind.BarBarToken,\n ts.SyntaxKind.QuestionQuestionToken,\n ].includes(unwrapped.operatorToken.kind)\n ) {\n return isCallLikeDiscard(unwrapped.right)\n }\n\n return false\n}\n\nconst inspectSourceFile = (\n checker: ts.TypeChecker,\n sourceFile: ts.SourceFile,\n): readonly NoDiscardFinding[] => {\n const findings: NoDiscardFinding[] = []\n\n const visit = (node: ts.Node): void => {\n if (\n ts.isExpressionStatement(node) &&\n !isExplicitDiscard(node.expression) &&\n isCallLikeDiscard(node.expression)\n ) {\n const type = checker.getTypeAtLocation(node.expression)\n\n if (isResultLikeType(checker, node.expression, type)) {\n const position = sourceFile.getLineAndCharacterOfPosition(node.expression.getStart())\n findings.push({\n column: position.character + 1,\n file: sourceFile.fileName,\n line: position.line + 1,\n type: checker.typeToString(type, node.expression, ts.TypeFormatFlags.NoTruncation),\n })\n }\n }\n\n ts.forEachChild(node, visit)\n }\n\n visit(sourceFile)\n\n return findings\n}\n\nexport const findDiscardedResults = (\n options: NoDiscardOptions = {},\n): Result<readonly NoDiscardFinding[], Error> => {\n const rootDir = resolve(options.rootDir ?? process.cwd())\n const projectPath = resolve(rootDir, options.project ?? 'tsconfig.json')\n\n return readProject(projectPath).map((parsed) => {\n const program = ts.createProgram(parsed.fileNames, parsed.options)\n const checker = program.getTypeChecker()\n const findings: NoDiscardFinding[] = []\n\n for (const sourceFile of program.getSourceFiles()) {\n if (!sourceFile.isDeclarationFile && !sourceFile.fileName.includes('/node_modules/')) {\n findings.push(...inspectSourceFile(checker, sourceFile))\n }\n }\n\n return findings\n })\n}\n\nconst formatFinding = (finding: NoDiscardFinding, rootDir: string): string => {\n const file = relative(rootDir, finding.file)\n\n return [\n `${file}:${finding.line}:${finding.column} no-discard-result`,\n `Ignored ${finding.type} value. Handle it or explicitly discard it with \\`void\\`.`,\n ].join(' - ')\n}\n\nexport const runNoDiscardCli = (args: readonly string[] = process.argv.slice(2)): number => {\n const rootDir = process.cwd()\n\n return parseArgs(args)\n .andThen((options) => {\n if (!options.help) {\n return options.project === undefined\n ? findDiscardedResults({ rootDir })\n : findDiscardedResults({ project: options.project, rootDir })\n }\n\n process.stdout.write(usage)\n return ok([] as const)\n })\n .match(\n (findings) => {\n if (findings.length === 0) {\n return 0\n }\n\n process.stderr.write(\n `${findings.map((finding) => formatFinding(finding, rootDir)).join('\\n')}\\n`,\n )\n\n return 1\n },\n (error) => {\n process.stderr.write(`${error.message}\\n`)\n return 1\n },\n )\n}\n\nif (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {\n process.exitCode = runNoDiscardCli()\n}\n"],"mappings":"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"}
|