nalloc 0.4.0 → 0.5.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 +300 -180
- package/build/codemod-cli.cjs +153 -0
- package/build/codemod-cli.cjs.map +1 -0
- package/build/codemod-cli.d.ts +2 -0
- package/build/codemod-cli.js +103 -0
- package/build/codemod-cli.js.map +1 -0
- package/build/codemod.cjs +652 -0
- package/build/codemod.cjs.map +1 -0
- package/build/codemod.d.ts +29 -0
- package/build/codemod.js +634 -0
- package/build/codemod.js.map +1 -0
- package/build/eslint.cjs +221 -0
- package/build/eslint.cjs.map +1 -0
- package/build/eslint.d.ts +36 -0
- package/build/eslint.js +198 -0
- package/build/eslint.js.map +1 -0
- package/build/http.cjs +31 -0
- package/build/http.cjs.map +1 -0
- package/build/http.d.ts +31 -0
- package/build/http.js +13 -0
- package/build/http.js.map +1 -0
- package/build/result.cjs +0 -11
- package/build/result.cjs.map +1 -1
- package/build/result.d.ts +0 -32
- package/build/result.js +0 -8
- package/build/result.js.map +1 -1
- package/build/safe.cjs +4 -0
- package/build/safe.cjs.map +1 -1
- package/build/safe.d.ts +1 -0
- package/build/safe.js +1 -0
- package/build/safe.js.map +1 -1
- package/build/schema.cjs +32 -0
- package/build/schema.cjs.map +1 -0
- package/build/schema.d.ts +44 -0
- package/build/schema.js +14 -0
- package/build/schema.js.map +1 -0
- package/package.json +54 -5
- package/src/__tests__/codemod.ts +211 -0
- package/src/__tests__/eslint.ts +99 -0
- package/src/__tests__/fixtures/tsconfig.json +10 -0
- package/src/__tests__/http.ts +64 -0
- package/src/__tests__/result.ts +74 -125
- package/src/__tests__/schema.ts +58 -0
- package/src/codemod-cli.ts +108 -0
- package/src/codemod.ts +623 -0
- package/src/eslint.ts +145 -0
- package/src/http.ts +42 -0
- package/src/result.ts +0 -37
- package/src/safe.ts +1 -0
- package/src/schema.ts +52 -0
package/src/eslint.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { ESLintUtils, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import type * as ts from 'typescript';
|
|
3
|
+
|
|
4
|
+
interface RuleDocs {
|
|
5
|
+
description: string;
|
|
6
|
+
requiresTypeChecking?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const createRule = ESLintUtils.RuleCreator<RuleDocs>((name) => `https://github.com/3axap4eHko/nalloc#${name}`);
|
|
10
|
+
|
|
11
|
+
const UNWRAP_NAMES: ReadonlySet<string> = new Set(['unwrap', 'unwrapErr', 'expect', 'expectErr']);
|
|
12
|
+
const DEFAULT_MODULES: readonly string[] = ['nalloc', 'nalloc/safe', 'nalloc/unsafe', 'nalloc/result', 'nalloc/option'];
|
|
13
|
+
const DEFAULT_TYPE_NAMES: readonly string[] = ['Result', 'Ok', 'Err', 'Option', 'Some', 'None'];
|
|
14
|
+
|
|
15
|
+
function nallocTypeName(type: ts.Type, names: ReadonlySet<string>): string | undefined {
|
|
16
|
+
const direct = type.aliasSymbol?.getName();
|
|
17
|
+
if (direct !== undefined && names.has(direct)) {
|
|
18
|
+
return direct;
|
|
19
|
+
}
|
|
20
|
+
if (type.isUnion()) {
|
|
21
|
+
for (const member of type.types) {
|
|
22
|
+
const name = member.aliasSymbol?.getName();
|
|
23
|
+
if (name !== undefined && names.has(name)) {
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mustUse = createRule<[{ typeNames?: string[] }], 'mustUse'>({
|
|
32
|
+
name: 'must-use',
|
|
33
|
+
meta: {
|
|
34
|
+
type: 'problem',
|
|
35
|
+
docs: {
|
|
36
|
+
description: 'Require Result and Option values to be handled instead of silently discarded',
|
|
37
|
+
requiresTypeChecking: true,
|
|
38
|
+
},
|
|
39
|
+
messages: {
|
|
40
|
+
mustUse: 'This {{name}} value is discarded. Handle it with match/unwrapOr/isErr/isNone, return it, or assign it.',
|
|
41
|
+
},
|
|
42
|
+
schema: [
|
|
43
|
+
{
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: { typeNames: { type: 'array', items: { type: 'string' }, uniqueItems: true } },
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
defaultOptions: [{}],
|
|
50
|
+
},
|
|
51
|
+
create(context, [options]) {
|
|
52
|
+
const names = new Set(options.typeNames ?? DEFAULT_TYPE_NAMES);
|
|
53
|
+
const services = ESLintUtils.getParserServices(context);
|
|
54
|
+
const check = (node: TSESTree.Expression): void => {
|
|
55
|
+
const name = nallocTypeName(services.getTypeAtLocation(node), names);
|
|
56
|
+
if (name !== undefined) {
|
|
57
|
+
context.report({ node, messageId: 'mustUse', data: { name } });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
'ExpressionStatement > CallExpression'(node: TSESTree.CallExpression): void {
|
|
62
|
+
check(node);
|
|
63
|
+
},
|
|
64
|
+
'ExpressionStatement > AwaitExpression'(node: TSESTree.AwaitExpression): void {
|
|
65
|
+
check(node);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const noUnwrap = createRule<[{ modules?: string[] }], 'noUnwrap'>({
|
|
72
|
+
name: 'no-unwrap',
|
|
73
|
+
meta: {
|
|
74
|
+
type: 'suggestion',
|
|
75
|
+
docs: {
|
|
76
|
+
description: 'Disallow unwrap and expect on Result and Option, which throw on failure',
|
|
77
|
+
},
|
|
78
|
+
messages: {
|
|
79
|
+
noUnwrap: '{{name}} throws on failure. Handle the error with match/unwrapOr/isErr, or turn this rule off in test files.',
|
|
80
|
+
},
|
|
81
|
+
schema: [
|
|
82
|
+
{
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: { modules: { type: 'array', items: { type: 'string' }, uniqueItems: true } },
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
defaultOptions: [{}],
|
|
89
|
+
},
|
|
90
|
+
create(context, [options]) {
|
|
91
|
+
const modules = new Set(options.modules ?? DEFAULT_MODULES);
|
|
92
|
+
const namespaceLocals = new Set<string>();
|
|
93
|
+
const directLocals = new Map<string, string>();
|
|
94
|
+
return {
|
|
95
|
+
ImportDeclaration(node: TSESTree.ImportDeclaration): void {
|
|
96
|
+
if (typeof node.source.value !== 'string' || !modules.has(node.source.value)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const spec of node.specifiers) {
|
|
100
|
+
if (spec.type === 'ImportNamespaceSpecifier') {
|
|
101
|
+
namespaceLocals.add(spec.local.name);
|
|
102
|
+
} else if (spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier') {
|
|
103
|
+
const imported = spec.imported.name;
|
|
104
|
+
if (imported === 'Result' || imported === 'Option') {
|
|
105
|
+
namespaceLocals.add(spec.local.name);
|
|
106
|
+
} else if (UNWRAP_NAMES.has(imported)) {
|
|
107
|
+
directLocals.set(spec.local.name, imported);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
CallExpression(node: TSESTree.CallExpression): void {
|
|
113
|
+
const callee = node.callee;
|
|
114
|
+
if (callee.type === 'MemberExpression' && !callee.computed && callee.property.type === 'Identifier' && callee.object.type === 'Identifier') {
|
|
115
|
+
if (UNWRAP_NAMES.has(callee.property.name) && namespaceLocals.has(callee.object.name)) {
|
|
116
|
+
context.report({ node: callee, messageId: 'noUnwrap', data: { name: callee.property.name } });
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (callee.type === 'Identifier') {
|
|
121
|
+
const imported = directLocals.get(callee.name);
|
|
122
|
+
if (imported !== undefined) {
|
|
123
|
+
context.report({ node: callee, messageId: 'noUnwrap', data: { name: imported } });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export const rules = { 'must-use': mustUse, 'no-unwrap': noUnwrap };
|
|
132
|
+
|
|
133
|
+
const plugin: { meta: { name: string }; rules: typeof rules; configs: Record<string, unknown> } = {
|
|
134
|
+
meta: { name: 'nalloc' },
|
|
135
|
+
rules,
|
|
136
|
+
configs: {},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
plugin.configs.recommended = {
|
|
140
|
+
plugins: { nalloc: plugin },
|
|
141
|
+
rules: { 'nalloc/must-use': 'error', 'nalloc/no-unwrap': 'error' },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export { mustUse, noUnwrap };
|
|
145
|
+
export default plugin;
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { err as ERR } from './types.js';
|
|
2
|
+
import type { Ok, Result } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts a fetch Response into a Result, treating a non-ok status as an Err.
|
|
6
|
+
* Native fetch only rejects on transport errors, never on 4xx/5xx; this closes that gap.
|
|
7
|
+
* The failed Response itself is the error - read its status, headers, or body from it.
|
|
8
|
+
* @param response - The Response to inspect
|
|
9
|
+
* @returns Ok(response) when response.ok, Err(response) otherwise
|
|
10
|
+
* @example
|
|
11
|
+
* import { Result } from 'nalloc';
|
|
12
|
+
* import { fromResponse } from 'nalloc/http';
|
|
13
|
+
* const res = Result.flatMap(await Result.fromPromise(fetch(url)), fromResponse);
|
|
14
|
+
*/
|
|
15
|
+
export function fromResponse(response: Response): Result<Response, Response> {
|
|
16
|
+
return response.ok ? (response as Ok<Response>) : ERR(response);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs fetch and forces every failure mode into the error channel.
|
|
21
|
+
* Ok means the request connected AND returned a 2xx status; a non-2xx Response
|
|
22
|
+
* becomes Err(response), and a transport failure becomes Err with the thrown
|
|
23
|
+
* value (per spec: TypeError on network/CORS errors, DOMException on abort/timeout).
|
|
24
|
+
* The body is never read - it stays available to the caller.
|
|
25
|
+
* @param input - The fetch input (URL or Request)
|
|
26
|
+
* @param init - Optional fetch init
|
|
27
|
+
* @returns Promise of Ok(response) for 2xx, Err otherwise
|
|
28
|
+
* @example
|
|
29
|
+
* import { fromFetch } from 'nalloc/http';
|
|
30
|
+
* const res = await fromFetch(url);
|
|
31
|
+
* // Ok(Response) -> connected and 2xx
|
|
32
|
+
* // Err(Response) -> reached the server, non-2xx
|
|
33
|
+
* // Err(TypeError) -> network/CORS failure
|
|
34
|
+
* // Err(DOMException) -> aborted or timed out
|
|
35
|
+
*/
|
|
36
|
+
export async function fromFetch(input: string | URL | Request, init?: RequestInit): Promise<Result<Response, Response | TypeError | DOMException>> {
|
|
37
|
+
try {
|
|
38
|
+
return fromResponse(await fetch(input, init));
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return ERR(error as TypeError | DOMException);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/result.ts
CHANGED
|
@@ -106,43 +106,6 @@ export async function fromPromise<T, E = unknown>(promise: Promise<T>, onError?:
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** A validation issue from a Standard Schema validator. */
|
|
110
|
-
export interface SchemaIssue {
|
|
111
|
-
readonly message: string;
|
|
112
|
-
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Minimal Standard Schema v1 interface (duck-typed, no external dependency). */
|
|
116
|
-
export interface StandardSchema<O = unknown> {
|
|
117
|
-
readonly '~standard': {
|
|
118
|
-
readonly validate: (value: unknown) => StandardSchemaResult<O> | Promise<StandardSchemaResult<O>>;
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
type StandardSchemaResult<O> = { readonly value: O; readonly issues?: undefined } | { readonly issues: ReadonlyArray<SchemaIssue> };
|
|
123
|
-
|
|
124
|
-
function schemaResultToResult<O>(sr: StandardSchemaResult<O>): Result<O, readonly SchemaIssue[]> {
|
|
125
|
-
return sr.issues ? ERR(sr.issues) : (sr.value as Ok<O>);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Validates a value against a Standard Schema and returns a Result.
|
|
130
|
-
* Works with any Standard Schema v1 compliant library (Zod, Valibot, ArkType, etc.).
|
|
131
|
-
* Returns synchronously if the schema validates synchronously.
|
|
132
|
-
* @param schema - A Standard Schema v1 compliant schema
|
|
133
|
-
* @param value - The value to validate
|
|
134
|
-
* @returns Ok(parsed) if valid, Err(issues) if invalid
|
|
135
|
-
* @example
|
|
136
|
-
* import { z } from 'zod';
|
|
137
|
-
* const result = fromSchema(z.string().email(), input);
|
|
138
|
-
* // Result<string, readonly SchemaIssue[]>
|
|
139
|
-
*/
|
|
140
|
-
export function fromSchema<O>(schema: StandardSchema<O>, value: unknown): Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
141
|
-
const sr = schema['~standard'].validate(value);
|
|
142
|
-
if (isThenable(sr)) return sr.then(schemaResultToResult);
|
|
143
|
-
return schemaResultToResult(sr);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
109
|
/**
|
|
147
110
|
* Executes a function that may return sync or async, preserving sync execution when possible.
|
|
148
111
|
* @param fn - Function that may return T or Promise<T>
|
package/src/safe.ts
CHANGED
|
@@ -21,6 +21,7 @@ export * as Option from './option.js';
|
|
|
21
21
|
export * as Result from './result.js';
|
|
22
22
|
export * as Iter from './iter.js';
|
|
23
23
|
export * as NonEmpty from './nonempty.js';
|
|
24
|
+
export * as Schema from './schema.js';
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Threads a value through a sequence of unary functions, left to right.
|
package/src/schema.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { err as ERR, isThenable } from './types.js';
|
|
2
|
+
import type { Ok, Result } from './types.js';
|
|
3
|
+
|
|
4
|
+
/** A validation issue from a Standard Schema validator. */
|
|
5
|
+
export interface SchemaIssue {
|
|
6
|
+
readonly message: string;
|
|
7
|
+
readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Minimal Standard Schema v1 interface (duck-typed, no external dependency). */
|
|
11
|
+
export interface StandardSchema<O = unknown> {
|
|
12
|
+
readonly '~standard': {
|
|
13
|
+
readonly validate: (value: unknown) => StandardSchemaResult<O> | Promise<StandardSchemaResult<O>>;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type StandardSchemaResult<O> = { readonly value: O; readonly issues?: undefined } | { readonly issues: ReadonlyArray<SchemaIssue> };
|
|
18
|
+
|
|
19
|
+
function schemaResultToResult<O>(sr: StandardSchemaResult<O>): Result<O, readonly SchemaIssue[]> {
|
|
20
|
+
return sr.issues ? ERR(sr.issues) : (sr.value as Ok<O>);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a value against a Standard Schema and returns a Result.
|
|
25
|
+
* Works with any Standard Schema v1 compliant library (Zod, Valibot, ArkType, etc.).
|
|
26
|
+
* Returns synchronously if the schema validates synchronously.
|
|
27
|
+
* @param schema - A Standard Schema v1 compliant schema
|
|
28
|
+
* @param value - The value to validate
|
|
29
|
+
* @returns Ok(parsed) if valid, Err(issues) if invalid
|
|
30
|
+
* @example
|
|
31
|
+
* import { z } from 'zod';
|
|
32
|
+
* const result = fromSchema(z.string().email(), input);
|
|
33
|
+
* // Result<string, readonly SchemaIssue[]>
|
|
34
|
+
*/
|
|
35
|
+
export function fromSchema<O>(schema: StandardSchema<O>, value: unknown): Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
36
|
+
const sr = schema['~standard'].validate(value);
|
|
37
|
+
if (isThenable(sr)) return sr.then(schemaResultToResult);
|
|
38
|
+
return schemaResultToResult(sr);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Binds a Standard Schema once and returns a reusable validator function.
|
|
43
|
+
* The partial-application form of fromSchema - validate many values against one schema.
|
|
44
|
+
* @param schema - A Standard Schema v1 compliant schema
|
|
45
|
+
* @returns A function that validates a value and returns a Result
|
|
46
|
+
* @example
|
|
47
|
+
* const parseUser = wrapSchema(userSchema);
|
|
48
|
+
* const result = parseUser(input); // Result<User, readonly SchemaIssue[]>
|
|
49
|
+
*/
|
|
50
|
+
export function wrapSchema<O>(schema: StandardSchema<O>): (value: unknown) => Result<O, readonly SchemaIssue[]> | Promise<Result<O, readonly SchemaIssue[]>> {
|
|
51
|
+
return (value) => fromSchema(schema, value);
|
|
52
|
+
}
|