nalloc 0.4.0 → 0.5.1

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.
Files changed (51) hide show
  1. package/README.md +300 -180
  2. package/build/codemod-cli.cjs +153 -0
  3. package/build/codemod-cli.cjs.map +1 -0
  4. package/build/codemod-cli.d.ts +2 -0
  5. package/build/codemod-cli.js +103 -0
  6. package/build/codemod-cli.js.map +1 -0
  7. package/build/codemod.cjs +652 -0
  8. package/build/codemod.cjs.map +1 -0
  9. package/build/codemod.d.ts +29 -0
  10. package/build/codemod.js +634 -0
  11. package/build/codemod.js.map +1 -0
  12. package/build/eslint.cjs +221 -0
  13. package/build/eslint.cjs.map +1 -0
  14. package/build/eslint.d.ts +36 -0
  15. package/build/eslint.js +198 -0
  16. package/build/eslint.js.map +1 -0
  17. package/build/http.cjs +31 -0
  18. package/build/http.cjs.map +1 -0
  19. package/build/http.d.ts +31 -0
  20. package/build/http.js +13 -0
  21. package/build/http.js.map +1 -0
  22. package/build/result.cjs +0 -11
  23. package/build/result.cjs.map +1 -1
  24. package/build/result.d.ts +0 -32
  25. package/build/result.js +0 -8
  26. package/build/result.js.map +1 -1
  27. package/build/safe.cjs +4 -0
  28. package/build/safe.cjs.map +1 -1
  29. package/build/safe.d.ts +1 -0
  30. package/build/safe.js +1 -0
  31. package/build/safe.js.map +1 -1
  32. package/build/schema.cjs +32 -0
  33. package/build/schema.cjs.map +1 -0
  34. package/build/schema.d.ts +44 -0
  35. package/build/schema.js +14 -0
  36. package/build/schema.js.map +1 -0
  37. package/package.json +55 -6
  38. package/src/__tests__/codemod.ts +211 -0
  39. package/src/__tests__/eslint.ts +99 -0
  40. package/src/__tests__/fixtures/tsconfig.json +10 -0
  41. package/src/__tests__/http.ts +64 -0
  42. package/src/__tests__/result.ts +74 -125
  43. package/src/__tests__/result.types.ts +2 -0
  44. package/src/__tests__/schema.ts +58 -0
  45. package/src/codemod-cli.ts +108 -0
  46. package/src/codemod.ts +623 -0
  47. package/src/eslint.ts +145 -0
  48. package/src/http.ts +42 -0
  49. package/src/result.ts +0 -37
  50. package/src/safe.ts +1 -0
  51. 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
+ }