bupkis 0.7.2 → 0.8.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.
Files changed (149) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/commonjs/assertion/assertion.d.ts +0 -13
  3. package/dist/commonjs/assertion/assertion.d.ts.map +1 -1
  4. package/dist/commonjs/assertion/assertion.js +42 -27
  5. package/dist/commonjs/assertion/assertion.js.map +1 -1
  6. package/dist/commonjs/assertion/create.d.ts +2 -0
  7. package/dist/commonjs/assertion/create.d.ts.map +1 -1
  8. package/dist/commonjs/assertion/create.js +7 -4
  9. package/dist/commonjs/assertion/create.js.map +1 -1
  10. package/dist/commonjs/assertion/impl/assertion-util.d.ts +2 -0
  11. package/dist/commonjs/assertion/impl/assertion-util.d.ts.map +1 -1
  12. package/dist/commonjs/assertion/impl/assertion-util.js +13 -2
  13. package/dist/commonjs/assertion/impl/assertion-util.js.map +1 -1
  14. package/dist/commonjs/assertion/impl/sync-basic.d.ts.map +1 -1
  15. package/dist/commonjs/assertion/impl/sync-basic.js +2 -3
  16. package/dist/commonjs/assertion/impl/sync-basic.js.map +1 -1
  17. package/dist/commonjs/assertion/impl/sync-collection.d.ts.map +1 -1
  18. package/dist/commonjs/assertion/impl/sync-collection.js +24 -14
  19. package/dist/commonjs/assertion/impl/sync-collection.js.map +1 -1
  20. package/dist/commonjs/assertion/impl/sync-esoteric.d.ts +1 -5
  21. package/dist/commonjs/assertion/impl/sync-esoteric.d.ts.map +1 -1
  22. package/dist/commonjs/assertion/impl/sync-esoteric.js +11 -13
  23. package/dist/commonjs/assertion/impl/sync-esoteric.js.map +1 -1
  24. package/dist/commonjs/assertion/impl/sync-parametric.d.ts.map +1 -1
  25. package/dist/commonjs/assertion/impl/sync-parametric.js +4 -2
  26. package/dist/commonjs/assertion/impl/sync-parametric.js.map +1 -1
  27. package/dist/commonjs/assertion/impl/sync.d.ts +2 -10
  28. package/dist/commonjs/assertion/impl/sync.d.ts.map +1 -1
  29. package/dist/commonjs/assertion/slotify.d.ts +1 -0
  30. package/dist/commonjs/assertion/slotify.d.ts.map +1 -1
  31. package/dist/commonjs/assertion/slotify.js +1 -0
  32. package/dist/commonjs/assertion/slotify.js.map +1 -1
  33. package/dist/commonjs/bootstrap.d.ts +1 -5
  34. package/dist/commonjs/bootstrap.d.ts.map +1 -1
  35. package/dist/commonjs/bootstrap.js +1 -0
  36. package/dist/commonjs/bootstrap.js.map +1 -1
  37. package/dist/commonjs/diff.d.ts +51 -0
  38. package/dist/commonjs/diff.d.ts.map +1 -0
  39. package/dist/commonjs/diff.js +279 -0
  40. package/dist/commonjs/diff.js.map +1 -0
  41. package/dist/commonjs/error.d.ts +17 -0
  42. package/dist/commonjs/error.d.ts.map +1 -1
  43. package/dist/commonjs/error.js +17 -7
  44. package/dist/commonjs/error.js.map +1 -1
  45. package/dist/commonjs/expect.d.ts.map +1 -1
  46. package/dist/commonjs/expect.js +13 -0
  47. package/dist/commonjs/expect.js.map +1 -1
  48. package/dist/commonjs/guards.d.ts +12 -8
  49. package/dist/commonjs/guards.d.ts.map +1 -1
  50. package/dist/commonjs/guards.js +24 -14
  51. package/dist/commonjs/guards.js.map +1 -1
  52. package/dist/commonjs/index.d.ts +1 -5
  53. package/dist/commonjs/index.d.ts.map +1 -1
  54. package/dist/commonjs/schema.d.ts.map +1 -1
  55. package/dist/commonjs/schema.js +3 -2
  56. package/dist/commonjs/schema.js.map +1 -1
  57. package/dist/commonjs/use.js +22 -8
  58. package/dist/commonjs/use.js.map +1 -1
  59. package/dist/commonjs/util.d.ts +1 -0
  60. package/dist/commonjs/util.d.ts.map +1 -1
  61. package/dist/commonjs/util.js +14 -10
  62. package/dist/commonjs/util.js.map +1 -1
  63. package/dist/commonjs/value-to-schema.d.ts +1 -0
  64. package/dist/commonjs/value-to-schema.d.ts.map +1 -1
  65. package/dist/commonjs/value-to-schema.js +17 -10
  66. package/dist/commonjs/value-to-schema.js.map +1 -1
  67. package/dist/esm/assertion/assertion.d.ts +0 -13
  68. package/dist/esm/assertion/assertion.d.ts.map +1 -1
  69. package/dist/esm/assertion/assertion.js +42 -27
  70. package/dist/esm/assertion/assertion.js.map +1 -1
  71. package/dist/esm/assertion/create.d.ts +2 -0
  72. package/dist/esm/assertion/create.d.ts.map +1 -1
  73. package/dist/esm/assertion/create.js +7 -4
  74. package/dist/esm/assertion/create.js.map +1 -1
  75. package/dist/esm/assertion/impl/assertion-util.d.ts +2 -0
  76. package/dist/esm/assertion/impl/assertion-util.d.ts.map +1 -1
  77. package/dist/esm/assertion/impl/assertion-util.js +13 -2
  78. package/dist/esm/assertion/impl/assertion-util.js.map +1 -1
  79. package/dist/esm/assertion/impl/sync-basic.d.ts.map +1 -1
  80. package/dist/esm/assertion/impl/sync-basic.js +2 -3
  81. package/dist/esm/assertion/impl/sync-basic.js.map +1 -1
  82. package/dist/esm/assertion/impl/sync-collection.d.ts.map +1 -1
  83. package/dist/esm/assertion/impl/sync-collection.js +24 -14
  84. package/dist/esm/assertion/impl/sync-collection.js.map +1 -1
  85. package/dist/esm/assertion/impl/sync-esoteric.d.ts +1 -5
  86. package/dist/esm/assertion/impl/sync-esoteric.d.ts.map +1 -1
  87. package/dist/esm/assertion/impl/sync-esoteric.js +11 -13
  88. package/dist/esm/assertion/impl/sync-esoteric.js.map +1 -1
  89. package/dist/esm/assertion/impl/sync-parametric.d.ts.map +1 -1
  90. package/dist/esm/assertion/impl/sync-parametric.js +4 -2
  91. package/dist/esm/assertion/impl/sync-parametric.js.map +1 -1
  92. package/dist/esm/assertion/impl/sync.d.ts +2 -10
  93. package/dist/esm/assertion/impl/sync.d.ts.map +1 -1
  94. package/dist/esm/assertion/slotify.d.ts +1 -0
  95. package/dist/esm/assertion/slotify.d.ts.map +1 -1
  96. package/dist/esm/assertion/slotify.js +1 -0
  97. package/dist/esm/assertion/slotify.js.map +1 -1
  98. package/dist/esm/bootstrap.d.ts +1 -5
  99. package/dist/esm/bootstrap.d.ts.map +1 -1
  100. package/dist/esm/bootstrap.js +1 -0
  101. package/dist/esm/bootstrap.js.map +1 -1
  102. package/dist/esm/diff.d.ts +51 -0
  103. package/dist/esm/diff.d.ts.map +1 -0
  104. package/dist/esm/diff.js +273 -0
  105. package/dist/esm/diff.js.map +1 -0
  106. package/dist/esm/error.d.ts +17 -0
  107. package/dist/esm/error.d.ts.map +1 -1
  108. package/dist/esm/error.js +17 -7
  109. package/dist/esm/error.js.map +1 -1
  110. package/dist/esm/expect.d.ts.map +1 -1
  111. package/dist/esm/expect.js +13 -0
  112. package/dist/esm/expect.js.map +1 -1
  113. package/dist/esm/guards.d.ts +12 -8
  114. package/dist/esm/guards.d.ts.map +1 -1
  115. package/dist/esm/guards.js +23 -12
  116. package/dist/esm/guards.js.map +1 -1
  117. package/dist/esm/index.d.ts +1 -5
  118. package/dist/esm/index.d.ts.map +1 -1
  119. package/dist/esm/schema.d.ts.map +1 -1
  120. package/dist/esm/schema.js +3 -2
  121. package/dist/esm/schema.js.map +1 -1
  122. package/dist/esm/use.js +19 -6
  123. package/dist/esm/use.js.map +1 -1
  124. package/dist/esm/util.d.ts +1 -0
  125. package/dist/esm/util.d.ts.map +1 -1
  126. package/dist/esm/util.js +14 -10
  127. package/dist/esm/util.js.map +1 -1
  128. package/dist/esm/value-to-schema.d.ts +1 -0
  129. package/dist/esm/value-to-schema.d.ts.map +1 -1
  130. package/dist/esm/value-to-schema.js +17 -10
  131. package/dist/esm/value-to-schema.js.map +1 -1
  132. package/package.json +22 -6
  133. package/src/assertion/assertion.ts +47 -28
  134. package/src/assertion/create.ts +8 -8
  135. package/src/assertion/impl/assertion-util.ts +27 -16
  136. package/src/assertion/impl/sync-basic.ts +3 -3
  137. package/src/assertion/impl/sync-collection.ts +34 -14
  138. package/src/assertion/impl/sync-esoteric.ts +17 -13
  139. package/src/assertion/impl/sync-parametric.ts +5 -2
  140. package/src/assertion/slotify.ts +1 -0
  141. package/src/bootstrap.ts +1 -0
  142. package/src/diff.ts +343 -0
  143. package/src/error.ts +35 -7
  144. package/src/expect.ts +13 -0
  145. package/src/guards.ts +27 -15
  146. package/src/schema.ts +4 -2
  147. package/src/use.ts +21 -7
  148. package/src/util.ts +15 -12
  149. package/src/value-to-schema.ts +18 -10
package/src/diff.ts ADDED
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Diff utilities for generating rich assertion failure output.
3
+ *
4
+ * This module provides utilities for extracting meaningful actual/expected
5
+ * values from Zod validation errors and generating high-quality diff output
6
+ * using `jest-diff` for assertion failures.
7
+ *
8
+ * @packageDocumentation
9
+ * @see {@link https://npm.im/jest-diff | jest-diff}
10
+ */
11
+
12
+ import type { DiffOptions } from 'jest-diff';
13
+ import type { z } from 'zod/v4';
14
+
15
+ import { diff } from 'jest-diff';
16
+ export type { DiffOptions };
17
+
18
+ const { isArray } = Array;
19
+ const { max } = Math;
20
+ const { stringify } = JSON;
21
+ const { entries } = Object;
22
+
23
+ /**
24
+ * Result of extracting diff values from a `ZodError`
25
+ */
26
+ export interface DiffValues {
27
+ /** The actual value that was validated */
28
+ actual: unknown;
29
+ /** The expected value (corrected version) */
30
+ expected: unknown;
31
+ }
32
+
33
+ /**
34
+ * Extracts actual and expected values from a `ZodError` for meaningful diffs.
35
+ *
36
+ * @function
37
+ * @param zodError The `ZodError` to extract values from
38
+ * @param subject The original subject that failed validation
39
+ * @returns Object with actual and expected values for diff
40
+ */
41
+ export const extractDiffValues = (
42
+ zodError: z.ZodError,
43
+ subject: unknown,
44
+ ): DiffValues => {
45
+ try {
46
+ // Start with the original subject as the actual value
47
+ const actual = subject;
48
+
49
+ // Create a copy to modify as the expected value
50
+ let expected = subject != null ? deepClone(subject) : subject;
51
+
52
+ // Process each issue to build the expected value
53
+ for (const issue of zodError.issues) {
54
+ try {
55
+ // Filter path to only string/number keys (symbols are rare in practice)
56
+ const filteredPath = issue.path.filter(
57
+ (key): key is number | string =>
58
+ typeof key === 'string' || typeof key === 'number',
59
+ );
60
+
61
+ switch (issue.code) {
62
+ case 'invalid_type': {
63
+ const correctedValue = createCorrectValueForType(
64
+ issue.expected,
65
+ getValueAtPath(actual, filteredPath),
66
+ );
67
+ expected = setValueAtPath(expected, filteredPath, correctedValue);
68
+ break;
69
+ }
70
+
71
+ case 'invalid_value': {
72
+ // For literal/enum errors, use the first valid value
73
+ const correctedValue =
74
+ issue.values && issue.values.length > 0
75
+ ? issue.values[0]
76
+ : '<valid value>';
77
+ expected = setValueAtPath(expected, filteredPath, correctedValue);
78
+ break;
79
+ }
80
+
81
+ case 'too_big': {
82
+ const actualValue = getValueAtPath(actual, filteredPath);
83
+ let correctedValue;
84
+
85
+ if (issue.origin === 'string' && typeof actualValue === 'string') {
86
+ correctedValue = actualValue.slice(0, Number(issue.maximum));
87
+ } else if (issue.origin === 'array' && isArray(actualValue)) {
88
+ correctedValue = actualValue.slice(0, Number(issue.maximum));
89
+ } else {
90
+ correctedValue = issue.maximum;
91
+ }
92
+
93
+ expected = setValueAtPath(expected, filteredPath, correctedValue);
94
+ break;
95
+ }
96
+
97
+ case 'too_small': {
98
+ const actualValue = getValueAtPath(actual, filteredPath);
99
+ let correctedValue;
100
+
101
+ if (issue.origin === 'string' && typeof actualValue === 'string') {
102
+ const needed = Number(issue.minimum) - actualValue.length;
103
+ correctedValue = actualValue + 'x'.repeat(max(0, needed));
104
+ } else if (issue.origin === 'array' && isArray(actualValue)) {
105
+ const needed = Number(issue.minimum) - actualValue.length;
106
+ correctedValue = actualValue.concat(
107
+ new Array(max(0, needed)).fill(null),
108
+ );
109
+ } else {
110
+ correctedValue = issue.minimum;
111
+ }
112
+
113
+ expected = setValueAtPath(expected, filteredPath, correctedValue);
114
+ break;
115
+ }
116
+
117
+ case 'unrecognized_keys': {
118
+ // Remove unrecognized keys from expected
119
+ if (
120
+ filteredPath.length === 0 &&
121
+ typeof expected === 'object' &&
122
+ expected !== null
123
+ ) {
124
+ const expectedCopy = {
125
+ ...(expected as Record<string, unknown>),
126
+ };
127
+ for (const key of issue.keys) {
128
+ delete expectedCopy[key];
129
+ }
130
+ expected = expectedCopy;
131
+ }
132
+ break;
133
+ }
134
+
135
+ default: {
136
+ // For other error types, just mark that something should be different
137
+ // but don't try to guess what the correct value should be
138
+ break;
139
+ }
140
+ }
141
+ } catch {
142
+ // If we can't process an individual issue, continue with others
143
+ continue;
144
+ }
145
+ }
146
+
147
+ return { actual, expected };
148
+ } catch {
149
+ // If anything goes wrong, return undefined to fall back to pretty error
150
+ return { actual: undefined, expected: undefined };
151
+ }
152
+ };
153
+
154
+ /**
155
+ * Generates a rich diff string using jest-diff for assertion failures.
156
+ *
157
+ * @function
158
+ * @param expected The expected value
159
+ * @param actual The actual value
160
+ * @param options Configuration options for diff generation
161
+ * @returns A formatted diff string, or null if values are identical
162
+ */
163
+ export const generateDiff = (
164
+ expected: unknown,
165
+ actual: unknown,
166
+ options: DiffOptions = {},
167
+ ): null | string => {
168
+ if (!shouldGenerateDiff(actual, expected)) {
169
+ return null;
170
+ }
171
+
172
+ return diff(expected, actual, {
173
+ aAnnotation: 'expected',
174
+ bAnnotation: 'actual',
175
+ expand: false,
176
+ includeChangeCounts: true,
177
+ ...options,
178
+ });
179
+ };
180
+
181
+ /**
182
+ * Checks if two values should generate a meaningful diff.
183
+ *
184
+ * @function
185
+ * @param actual The actual value
186
+ * @param expected The expected value
187
+ * @returns True if a diff should be generated, false otherwise
188
+ */
189
+ export const shouldGenerateDiff = (
190
+ actual: unknown,
191
+ expected: unknown,
192
+ ): boolean =>
193
+ actual !== undefined && expected !== undefined && actual !== expected;
194
+
195
+ /**
196
+ * Creates a corrected value for a given type
197
+ *
198
+ * @function
199
+ */
200
+ const createCorrectValueForType = (
201
+ expectedType: string,
202
+ actualValue: unknown,
203
+ ): unknown => {
204
+ switch (expectedType) {
205
+ case 'array':
206
+ return isArray(actualValue) ? actualValue : [];
207
+ case 'boolean':
208
+ return typeof actualValue === 'boolean'
209
+ ? actualValue
210
+ : Boolean(actualValue);
211
+ case 'null':
212
+ return null;
213
+ case 'number':
214
+ return typeof actualValue === 'number'
215
+ ? actualValue
216
+ : Number(actualValue) || 0;
217
+ case 'object':
218
+ return typeof actualValue === 'object' && actualValue !== null
219
+ ? actualValue
220
+ : {};
221
+ case 'string':
222
+ if (typeof actualValue === 'string') {
223
+ return actualValue;
224
+ }
225
+ if (actualValue == null) {
226
+ return '';
227
+ }
228
+ if (typeof actualValue === 'object') {
229
+ return stringify(actualValue);
230
+ }
231
+ // For primitive types (number, boolean, etc.) - we know it's not an object here
232
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
233
+ return `${actualValue}`;
234
+ case 'undefined':
235
+ return undefined;
236
+ default:
237
+ return `<${expectedType}>`;
238
+ }
239
+ };
240
+
241
+ /**
242
+ * Custom deep clone implementation for values that structuredClone can't
243
+ * handle. Used as fallback when structuredClone fails.
244
+ *
245
+ * @function
246
+ */
247
+ const customDeepClone = (value: unknown): unknown => {
248
+ if (value === null || typeof value !== 'object') {
249
+ return value;
250
+ }
251
+ if (isArray(value)) {
252
+ return value.map(customDeepClone);
253
+ }
254
+ // For objects, create a new object and copy properties
255
+ const cloned: Record<string, unknown> = {};
256
+ for (const [key, val] of entries(value)) {
257
+ cloned[key] = customDeepClone(val);
258
+ }
259
+ return cloned;
260
+ };
261
+
262
+ /**
263
+ * Deep clone a value to avoid mutating the original. Uses the native
264
+ * structuredClone API which handles more types and circular references better
265
+ * than a custom implementation. Falls back to custom implementation for values
266
+ * containing functions.
267
+ *
268
+ * @function
269
+ */
270
+ const deepClone = (value: unknown): unknown => {
271
+ try {
272
+ return structuredClone(value);
273
+ } catch (error) {
274
+ // Handle DataCloneError for values that can't be cloned (functions, symbols, etc.)
275
+ if (error instanceof Error && error.name === 'DataCloneError') {
276
+ return customDeepClone(value);
277
+ }
278
+ throw error;
279
+ }
280
+ };
281
+
282
+ /**
283
+ * Gets a value at a specific path in an object
284
+ *
285
+ * @function
286
+ */
287
+ const getValueAtPath = (
288
+ obj: unknown,
289
+ path: readonly (number | string)[],
290
+ ): unknown => {
291
+ if (path.length === 0) {
292
+ return obj;
293
+ }
294
+
295
+ let current = obj;
296
+ for (const key of path) {
297
+ if (current == null) {
298
+ return undefined;
299
+ }
300
+ current = (current as Record<number | string, unknown>)[key];
301
+ }
302
+ return current;
303
+ };
304
+
305
+ /**
306
+ * Returns a new object with the value at the specified path set to the given
307
+ * value.
308
+ *
309
+ * @function
310
+ */
311
+ const setValueAtPath = (
312
+ obj: unknown,
313
+ path: readonly (number | string)[],
314
+ value: unknown,
315
+ ): unknown => {
316
+ if (path.length === 0) {
317
+ return value;
318
+ }
319
+
320
+ if (obj == null) {
321
+ // Need to create the structure
322
+ obj = typeof path[0] === 'number' ? [] : {};
323
+ }
324
+
325
+ const result = isArray(obj)
326
+ ? [...(obj as unknown[])]
327
+ : { ...(obj as Record<string, unknown>) };
328
+ const [head, ...tail] = path;
329
+
330
+ if (head !== undefined) {
331
+ if (tail.length === 0) {
332
+ (result as Record<number | string, unknown>)[head] = value;
333
+ } else {
334
+ (result as Record<number | string, unknown>)[head] = setValueAtPath(
335
+ (result as Record<number | string, unknown>)[head],
336
+ tail,
337
+ value,
338
+ );
339
+ }
340
+ }
341
+
342
+ return result;
343
+ };
package/src/error.ts CHANGED
@@ -16,6 +16,23 @@ import {
16
16
  } from './constant.js';
17
17
  import { isA } from './guards.js';
18
18
 
19
+ const { hasOwn } = Object;
20
+
21
+ /**
22
+ * Options for {@link AssertionError}'s constructor
23
+ *
24
+ * @remarks
25
+ * Based on Node.js' {@link NodeAssertionError}'s constructor options, sans
26
+ * `operator`.
27
+ * @group Error Options
28
+ */
29
+ export type AssertionErrorOptions = Partial<
30
+ Omit<
31
+ NonNullable<ConstructorParameters<typeof NodeAssertionError>[0]>,
32
+ 'operator'
33
+ >
34
+ >;
35
+
19
36
  /**
20
37
  * Options for {@link AssertionImplementationError}
21
38
  *
@@ -80,6 +97,10 @@ export class AssertionError extends NodeAssertionError {
80
97
 
81
98
  override name = 'AssertionError';
82
99
 
100
+ constructor(options?: AssertionErrorOptions) {
101
+ super(options);
102
+ }
103
+
83
104
  /**
84
105
  * Type guard for an instance of this error.
85
106
  *
@@ -88,10 +109,19 @@ export class AssertionError extends NodeAssertionError {
88
109
  */
89
110
  static isAssertionError(value: unknown): value is AssertionError {
90
111
  return (
91
- isA(value, NodeAssertionError) &&
92
- Object.hasOwn(value, kBupkisAssertionError)
112
+ isA(value, NodeAssertionError) && hasOwn(value, kBupkisAssertionError)
93
113
  );
94
114
  }
115
+
116
+ toJSON() {
117
+ return {
118
+ actual: this.actual,
119
+ expected: this.expected,
120
+ message: this.message,
121
+ name: this.name,
122
+ stack: this.stack,
123
+ };
124
+ }
95
125
  }
96
126
 
97
127
  /**
@@ -108,7 +138,7 @@ export class BupkisError extends Error {
108
138
  override name = 'BupkisError';
109
139
 
110
140
  static isBupkisError(err: unknown): err is BupkisError {
111
- return isA(err, Error) && Object.hasOwn(err, kBupkisError);
141
+ return isA(err, Error) && hasOwn(err, kBupkisError);
112
142
  }
113
143
  }
114
144
 
@@ -151,8 +181,7 @@ export class FailAssertionError extends AssertionError {
151
181
 
152
182
  static isFailAssertionError(err: unknown): err is FailAssertionError {
153
183
  return (
154
- isA(err, FailAssertionError) &&
155
- Object.hasOwn(err, kBupkisFailAssertionError)
184
+ isA(err, FailAssertionError) && hasOwn(err, kBupkisFailAssertionError)
156
185
  );
157
186
  }
158
187
  }
@@ -211,8 +240,7 @@ export class NegatedAssertionError extends AssertionError {
211
240
 
212
241
  static isNegatedAssertionError(err: unknown): err is NegatedAssertionError {
213
242
  return (
214
- isA(err, AssertionError) &&
215
- Object.hasOwn(err, kBupkisNegatedAssertionError)
243
+ isA(err, AssertionError) && hasOwn(err, kBupkisNegatedAssertionError)
216
244
  );
217
245
  }
218
246
  }
package/src/expect.ts CHANGED
@@ -181,6 +181,9 @@ export function createExpectAsyncFunction<
181
181
  expect?.assertions.length ?? 0,
182
182
  assertions.length + (expect?.assertions.length ?? 0),
183
183
  );
184
+ /**
185
+ * @function
186
+ */
184
187
  const expectAsyncFunction = async (...args: readonly unknown[]) => {
185
188
  await Promise.resolve();
186
189
  const [isNegated, processedArgs] = maybeProcessNegation(args);
@@ -359,6 +362,9 @@ export function createExpectSyncFunction<
359
362
  expect?.assertions.length ?? 0,
360
363
  assertions.length + (expect?.assertions.length ?? 0),
361
364
  );
365
+ /**
366
+ * @function
367
+ */
362
368
  const expectFunction = (...args: readonly unknown[]) => {
363
369
  const [isNegated, processedArgs] = maybeProcessNegation(args);
364
370
  const candidates: Array<{
@@ -406,6 +412,7 @@ export function createExpectSyncFunction<
406
412
  * @privateRemarks
407
413
  * This is here because `Assertion` doesn't know anything about negation and
408
414
  * probably shouldn't.
415
+ * @function
409
416
  * @param assertion - The assertion to execute
410
417
  * @param parsedValues - Parsed values for the assertion
411
418
  * @param args - Original arguments passed to expect
@@ -474,6 +481,7 @@ const execute = <
474
481
  * @privateRemarks
475
482
  * This is here because `Assertion` doesn't know anything about negation and
476
483
  * probably shouldn't.
484
+ * @function
477
485
  * @param assertion - The assertion to execute
478
486
  * @param parsedValues - Parsed values for the assertion
479
487
  * @param args - Original arguments passed to expectAsync
@@ -533,6 +541,7 @@ const executeAsync = async <
533
541
  * requested along with arguments stripped of the leading negation (to enable
534
542
  * assertion matching).
535
543
  *
544
+ * @function
536
545
  * @internal
537
546
  */
538
547
  const maybeProcessNegation = (
@@ -555,6 +564,7 @@ const maybeProcessNegation = (
555
564
  * Throws an error indicating that no valid assertion could be found for the
556
565
  * provided arguments.
557
566
  *
567
+ * @function
558
568
  * @param args The arguments that were passed to the expect function
559
569
  * @internal
560
570
  */
@@ -571,6 +581,7 @@ const throwInvalidParametersError = (args: readonly unknown[]): never => {
571
581
  * Detects if an assertion phrase starts with "not " and returns the cleaned
572
582
  * phrase.
573
583
  *
584
+ * @function
574
585
  * @param phrase - The assertion phrase to check
575
586
  * @returns Object with `isNegated` flag and `cleanedPhrase`
576
587
  */
@@ -595,6 +606,8 @@ const detectNegation = (
595
606
 
596
607
  /**
597
608
  * {@inheritdoc FailFn}
609
+ *
610
+ * @function
598
611
  */
599
612
  const fail: FailFn = (reason?: string): never => {
600
613
  throw new FailAssertionError({ message: reason });
package/src/guards.ts CHANGED
@@ -16,7 +16,6 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
 
19
- import { type Primitive } from 'type-fest';
20
19
  import { z } from 'zod/v4';
21
20
 
22
21
  import type {
@@ -28,6 +27,8 @@ import type { Constructor, ExpectItExecutor, ZodTypeMap } from './types.js';
28
27
 
29
28
  import { kExpectIt } from './constant.js';
30
29
 
30
+ const { isArray } = Array;
31
+
31
32
  /**
32
33
  * Returns `true` if the given value looks like a Zod v4 schema, determined by
33
34
  * the presence of an internal {@link z.core.$ZodTypeDef} field.
@@ -43,6 +44,7 @@ export function isZodType<T extends keyof ZodTypeMap>(
43
44
  value: unknown,
44
45
  type: T,
45
46
  ): value is ZodTypeMap[T];
47
+
46
48
  /**
47
49
  * Returns `true` if the given value looks like a Zod v4 schema, determined by
48
50
  * the presence of an internal {@link z.core.$ZodTypeDef} field.
@@ -65,8 +67,12 @@ export function isZodType<T extends keyof ZodTypeMap>(
65
67
  typeof value.def === 'object' &&
66
68
  'type' in value.def;
67
69
 
68
- if (!isValid) return false;
69
- if (type === undefined) return true;
70
+ if (!isValid) {
71
+ return false;
72
+ }
73
+ if (type === undefined) {
74
+ return true;
75
+ }
70
76
 
71
77
  return (value as z.ZodType).def.type === type;
72
78
  }
@@ -74,16 +80,18 @@ export function isZodType<T extends keyof ZodTypeMap>(
74
80
  /**
75
81
  * Type guard for a plain object.
76
82
  *
83
+ * @function
77
84
  * @param value Value to test
78
85
  * @returns `true` if the value is a plain object, `false` otherwise
79
86
  */
80
87
  export const isObject = (value: unknown): value is NonNullable<object> => {
81
- return typeof value === 'object' && value !== null && !Array.isArray(value);
88
+ return typeof value === 'object' && value !== null && !isArray(value);
82
89
  };
83
90
 
84
91
  /**
85
92
  * Returns `true` if the given value is a {@link z.ZodPromise} schema.
86
93
  *
94
+ * @function
87
95
  * @param value - Value to test
88
96
  * @returns `true` if the value is a `ZodPromise` schema; `false` otherwise
89
97
  */
@@ -93,6 +101,7 @@ export const isZodPromise = (value: unknown): value is z.ZodPromise =>
93
101
  /**
94
102
  * Checks if a value is "promise-like", meaning it is a "thenable" object.
95
103
  *
104
+ * @function
96
105
  * @param value - Value to test
97
106
  * @returns `true` if the value is promise-like, `false` otherwise
98
107
  */
@@ -114,6 +123,7 @@ export const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>
114
123
  * This may be the only way we can determine, at runtime, if a function is a
115
124
  * constructor without actually calling it. I am unsure if this only works for
116
125
  * classes.
126
+ * @function
117
127
  * @param fn - Function to test
118
128
  * @returns Whether the function is constructable
119
129
  */
@@ -123,7 +133,7 @@ export const isConstructible = (fn: unknown): fn is Constructor => {
123
133
  }
124
134
  try {
125
135
  // this will throw if there is no `[[construct]]` slot.. or so I've heard.
126
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
136
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, custom/require-function-tag-in-arrow-functions
127
137
  new new Proxy(fn as any, { construct: () => ({}) })();
128
138
  return true;
129
139
  } catch {
@@ -134,6 +144,7 @@ export const isConstructible = (fn: unknown): fn is Constructor => {
134
144
  /**
135
145
  * Type guard for a boolean value
136
146
  *
147
+ * @function
137
148
  * @param value Value to check
138
149
  * @returns `true` if the value is a boolean, `false` otherwise
139
150
  */
@@ -143,6 +154,7 @@ export const isBoolean = (value: unknown): value is boolean =>
143
154
  /**
144
155
  * Type guard for a function value
145
156
  *
157
+ * @function
146
158
  * @param value Value to check
147
159
  * @returns `true` if the value is a function, `false` otherwise
148
160
  */
@@ -169,6 +181,7 @@ const AssertionFailureSchema: z.ZodType<AssertionFailure> = z.object({
169
181
  /**
170
182
  * Type guard for a {@link AssertionFailure} object
171
183
  *
184
+ * @function
172
185
  * @param value Value to check
173
186
  * @returns `true` if the value is an `AssertionFailure`, `false` otherwise
174
187
  * @internal
@@ -179,6 +192,7 @@ export const isAssertionFailure = (value: unknown): value is AssertionFailure =>
179
192
  /**
180
193
  * Type guard for a string value
181
194
  *
195
+ * @function
182
196
  * @param value Value to check
183
197
  * @returns `true` if the value is a string, `false` otherwise
184
198
  */
@@ -188,27 +202,20 @@ export const isString = (value: unknown): value is string =>
188
202
  /**
189
203
  * Type guard for a non-null object value
190
204
  *
205
+ * @function
191
206
  * @param value Value to check
192
207
  * @returns `true` if the value is an object and not null, `false` otherwise
193
208
  */
194
209
  export const isNonNullObject = (value: unknown): value is object =>
195
210
  typeof value === 'object' && value !== null;
196
211
 
197
- /**
198
- * Type guard for a null or non-object value
199
- *
200
- * @param value Value to check
201
- * @returns `true` if the value is null or not an object, `false` otherwise
202
- */
203
- export const isNullOrNonObject = (value: unknown): value is null | Primitive =>
204
- typeof value !== 'object' || value === null;
205
-
206
212
  /**
207
213
  * Type guard for a valid WeakKey (object, function, or symbol).
208
214
  *
209
215
  * WeakMaps and WeakSets can only use objects (including functions) or symbols
210
216
  * as keys, not primitives like strings, numbers, booleans, null, or undefined.
211
217
  *
218
+ * @function
212
219
  * @param value Value to check
213
220
  * @returns `true` if the value is a valid WeakKey (object, function, or
214
221
  * symbol), `false` otherwise
@@ -221,6 +228,7 @@ export const isWeakKey = (value: unknown): value is WeakKey =>
221
228
  /**
222
229
  * Type guard for a {@link PhraseLiteralChoice}, which is a tuple of strings.
223
230
  *
231
+ * @function
224
232
  * @param value Assertion part to check
225
233
  * @returns `true` if the part is a `PhraseLiteralChoice`, `false` otherwise
226
234
  * @internal
@@ -228,12 +236,13 @@ export const isWeakKey = (value: unknown): value is WeakKey =>
228
236
  export const isPhraseLiteralChoice = (
229
237
  value: AssertionPart,
230
238
  ): value is PhraseLiteralChoice =>
231
- Array.isArray(value) && value.every(isPhraseLiteral);
239
+ isArray(value) && value.every(isPhraseLiteral);
232
240
 
233
241
  /**
234
242
  * Type guard for a {@link PhraseLiteral}, which is just a string that does not
235
243
  * begin with `not `.
236
244
  *
245
+ * @function
237
246
  * @param value Assertion part to check
238
247
  * @returns `true` if the part is a `PhraseLiteral`, `false` otherwise
239
248
  * @internal
@@ -260,6 +269,7 @@ export const isPhraseLiteral = (value: AssertionPart): value is string =>
260
269
  * ```
261
270
  *
262
271
  * @template T - The constructor type to check against
272
+ * @function
263
273
  * @param value - Value to test
264
274
  * @param ctor - Constructor function to check instanceof
265
275
  * @returns `true` if the value is an instance of the constructor, `false`
@@ -292,6 +302,7 @@ export const isA = <T extends Constructor>(
292
302
  * }
293
303
  * ```
294
304
  *
305
+ * @function
295
306
  * @param value - Value to test
296
307
  * @returns `true` if the value is an Error instance, `false` otherwise
297
308
  */
@@ -318,6 +329,7 @@ export const isError = (value: unknown): value is Error => isA(value, Error);
318
329
  * ```
319
330
  *
320
331
  * @template Subject - The subject type that the executor function operates on
332
+ * @function
321
333
  * @param value - Value to test
322
334
  * @returns `true` if the value is an ExpectItExecutor function, `false`
323
335
  * otherwise
package/src/schema.ts CHANGED
@@ -50,6 +50,8 @@ import {
50
50
  type MutableOrReadonly,
51
51
  } from './types.js';
52
52
 
53
+ const { getPrototypeOf, prototype: objectPrototype } = Object;
54
+
53
55
  /**
54
56
  * A Zod schema that validates JavaScript constructible functions.
55
57
  *
@@ -386,7 +388,7 @@ export const WrappedPromiseLikeSchema = z
386
388
  */
387
389
  export const DictionarySchema = z
388
390
  .custom<Record<PropertyKey, unknown>>(
389
- (value) => isNonNullObject(value) && Object.getPrototypeOf(value) === null,
391
+ (value) => isNonNullObject(value) && getPrototypeOf(value) === null,
390
392
  )
391
393
  .describe('Object with null prototype')
392
394
  .register(BupkisRegistry, { name: 'dictionary' });
@@ -450,7 +452,7 @@ export const NullProtoObjectSchema = DictionarySchema;
450
452
  * @group Schema
451
453
  */
452
454
  export const AsyncFunctionSchema = FunctionSchema.refine(
453
- (value) => Object.prototype.toString.call(value) === '[object AsyncFunction]',
455
+ (value) => objectPrototype.toString.call(value) === '[object AsyncFunction]',
454
456
  )
455
457
  .describe('Function declared with the `async` keyword')
456
458
  .register(BupkisRegistry, { name: 'async-function' });