Repository: karen-irc/option-t
Pull request #1548 (2023-02-03 12:47:27)

asMut should check whether the passed value is frozen
If the input is frozen, that's function will not work well. We should throw an error about it to notice the situation to user.

This change the behavior of function, but I think this is not a braking change because of that it would throw an error in another place if the passed value is frozen in this function's situation. This is just an early error.


Patch:
diff --git a/src/PlainOption/asMut.ts b/src/PlainOption/asMut.ts
index 759e54f9..9e168c78 100644
--- a/src/PlainOption/asMut.ts
+++ b/src/PlainOption/asMut.ts
@@ -1,5 +1,6 @@
-import { Mutable } from '../internal/Mutable.js';
-import { Option } from './Option.js';
+import { assertIsFrozen } from '../internal/assert.js';
+import type { Mutable } from '../internal/Mutable.js';
+import type { Option } from './Option.js';
 
 /**
  *  This allows to mutate the value to save needless allocation.
@@ -9,6 +10,11 @@ import { Option } from './Option.js';
  */
 export type MutOption<T> = Mutable<Option<T>>;
 
+/**
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
+ */
 export function asMutOption<T>(input: Option<T>): MutOption<T> {
+    assertIsFrozen(input);
     return input as MutOption<T>;
 }
diff --git a/src/PlainOption/drop.ts b/src/PlainOption/drop.ts
index a6bc857e..31854ab6 100644
--- a/src/PlainOption/drop.ts
+++ b/src/PlainOption/drop.ts
@@ -1,6 +1,6 @@
 import type { Mutable } from '../internal/Mutable.js';
 import type { EffectFn } from '../internal/Function.js';
-import type { Option, Some } from './Option.js';
+import { type Option, type Some, isSome } from './Option.js';
 import { asMutOption } from './asMut.js';
 
 export type MutSome<T> = Mutable<Some<T>>;
@@ -25,10 +25,13 @@ export type UnsafeSomeDestructorFn<T> = EffectFn<MutSome<T>>;
  *  Compared to Rust, JavaScript does not have ownership semantics in language
  *  and this API is designed to use as a destructor or similar fashions.
  *  So if you call this for same object more than once, your code might contain "double free" problem.
+ *
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
  */
 export function unsafeDropForOption<T>(input: Option<T>, mutator: UnsafeSomeDestructorFn<T>): void {
     const mutable = asMutOption(input);
-    if (mutable.ok) {
+    if (isSome(mutable)) {
         mutator(mutable);
         mutable.val = undefined as never;
     }
diff --git a/src/PlainResult/asMut.ts b/src/PlainResult/asMut.ts
index 9159199a..0e9b0dd7 100644
--- a/src/PlainResult/asMut.ts
+++ b/src/PlainResult/asMut.ts
@@ -1,5 +1,6 @@
-import { Mutable } from '../internal/Mutable.js';
-import { Result } from './Result.js';
+import { assertIsFrozen } from '../internal/assert.js';
+import type { Mutable } from '../internal/Mutable.js';
+import type { Result } from './Result.js';
 
 /**
  *  This allows to mutate the value to save needless allocation.
@@ -9,6 +10,11 @@ import { Result } from './Result.js';
  */
 export type MutResult<T, E> = Mutable<Result<T, E>>;
 
+/**
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
+ */
 export function asMutResult<T, E>(input: Result<T, E>): MutResult<T, E> {
+    assertIsFrozen(input);
     return input as MutResult<T, E>;
 }
diff --git a/src/PlainResult/drop.ts b/src/PlainResult/drop.ts
index 95635b06..18a5cb4b 100644
--- a/src/PlainResult/drop.ts
+++ b/src/PlainResult/drop.ts
@@ -1,6 +1,6 @@
 import type { Mutable } from '../internal/Mutable.js';
 import type { EffectFn } from '../internal/Function.js';
-import type { Result, Ok, Err } from './Result.js';
+import { type Result, type Ok, type Err, isOk } from './Result.js';
 import { asMutResult } from './asMut.js';
 
 export type MutOk<T> = Mutable<Ok<T>>;
@@ -32,6 +32,9 @@ function noop<T, E>(_input: MutResult<T, E>): void {}
  *  Compared to Rust, JavaScript does not have ownership semantics in language
  *  and this API is designed to use as a destructor or similar fashions.
  *  So if you call this for same object more than once, your code might contain "double free" problem.
+ *
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
  */
 export function unsafeDropBothForResult<T, E>(
     input: Result<T, E>,
@@ -39,7 +42,7 @@ export function unsafeDropBothForResult<T, E>(
     errMutator: UnsafeErrDestructorFn<E>
 ): void {
     const mutable = asMutResult(input);
-    if (mutable.ok) {
+    if (isOk(mutable)) {
         okMutator(mutable);
         mutable.val = undefined as never;
     } else {
@@ -73,6 +76,9 @@ export function unsafeDropBothForResult<T, E>(
  *  Compared to Rust, JavaScript does not have ownership semantics in language
  *  and this API is designed to use as a destructor or similar fashions.
  *  So if you call this for same object more than once, your code might contain "double free" problem.
+ *
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
  */
 export function unsafeDropOkForResult<T, E>(
     input: Result<T, E>,
@@ -99,6 +105,9 @@ export function unsafeDropOkForResult<T, E>(
  *  Compared to Rust, JavaScript does not have ownership semantics in language
  *  and this API is designed to use as a destructor or similar fashions.
  *  So if you call this for same object more than once, your code might contain "double free" problem.
+ *
+ *  @throws
+ *  This throw an `Error` instance if the _input_ is frozen.
  */
 export function unsafeDropErrForResult<T, E>(
     input: Result<T, E>,
diff --git a/src/internal/ErrorMessage.ts b/src/internal/ErrorMessage.ts
index 60d4b72d..8eed9fe1 100644
--- a/src/internal/ErrorMessage.ts
+++ b/src/internal/ErrorMessage.ts
@@ -28,3 +28,6 @@ export const ERR_MSG_THROWN_VALUE_IS_NOT_BUILTIN_ERROR_INSTANCE =
 
 export const ERR_MSG_CONTAINED_TYPE_E_SHOULD_BE_BUILTIN_ERROR_INSTANCE =
     'The contained E should be ' + MSG_BUILTIN_ERROR_INSTANCE;
+
+export const ERR_MSG_INPUT_IS_FROZEN_NOT_CAST_TO_MUTABLE =
+    'input is frozen, cannot cast to mutable';
diff --git a/src/internal/assert.ts b/src/internal/assert.ts
index 4caa1d5a..9af8e81f 100644
--- a/src/internal/assert.ts
+++ b/src/internal/assert.ts
@@ -1,3 +1,5 @@
+import { ERR_MSG_INPUT_IS_FROZEN_NOT_CAST_TO_MUTABLE } from './ErrorMessage.js';
+
 export function assertIsPromise(
     input: unknown,
     message: string
@@ -21,3 +23,9 @@ export function assertIsErrorInstance(input: unknown, message: string): asserts
         });
     }
 }
+
+export function assertIsFrozen(input: unknown): void {
+    if (Object.isFrozen(input)) {
+        throw new TypeError(ERR_MSG_INPUT_IS_FROZEN_NOT_CAST_TO_MUTABLE);
+    }
+}


Test patch:
diff --git a/__tests__/PlainOption/as_mut.test.mjs b/__tests__/PlainOption/as_mut.test.mjs
index 6f5b5cc5..e7277c6f 100644
--- a/__tests__/PlainOption/as_mut.test.mjs
+++ b/__tests__/PlainOption/as_mut.test.mjs
@@ -8,7 +8,7 @@ const FUNC_LIST = [createSome, createNone];
 for (const factory of FUNC_LIST) {
     const funcname = factory.name;
 
-    test(`asMutResult does not change the shape & object created by ${funcname}`, (t) => {
+    test(`asMutOption does not change the shape & object created by ${funcname}`, (t) => {
         const INT = Symbol('');
         const original = factory(INT);
         const actual = asMutOption(original);
@@ -16,3 +16,22 @@ for (const factory of FUNC_LIST) {
         t.deepEqual(actual, original, 'should be the same shape');
     });
 }
+
+const TEST_CASE_LIST = [
+    ['Some', createSome(Math.random())],
+    ['None', createNone()],
+];
+for (const [typename, inputValue] of TEST_CASE_LIST) {
+    test(`should throw if the passed value is frozen: ${typename}`, (t) => {
+        const input = Object.freeze(inputValue);
+        t.throws(
+            () => {
+                asMutOption(input);
+            },
+            {
+                instanceOf: TypeError,
+                message: `input is frozen, cannot cast to mutable`,
+            }
+        );
+    });
+}
diff --git a/__tests__/PlainOption/drop.test.mjs b/__tests__/PlainOption/drop.test.mjs
index aa4d1ccd..72858741 100644
--- a/__tests__/PlainOption/drop.test.mjs
+++ b/__tests__/PlainOption/drop.test.mjs
@@ -26,3 +26,39 @@ test('unsafeDropForOption() with None', (t) => {
     t.is(actual.ok, false, 'should not be modified');
     t.true(Object.isFrozen(actual), 'should be frozen');
 });
+
+test(`should throw if the input is frozen: Some`, (t) => {
+    const inputValue = createSome(Math.random);
+    const input = Object.freeze(inputValue);
+    t.true(Object.isFrozen(input), 'input must be frozen');
+
+    t.throws(
+        () => {
+            unsafeDropForOption(input, (_v) => {
+                t.fail('Do not enter this path');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
+
+test(`should throw if the input is frozen: None`, (t) => {
+    const inputValue = createNone();
+    const input = Object.freeze(inputValue);
+    t.true(Object.isFrozen(input), 'input must be frozen');
+
+    t.throws(
+        () => {
+            unsafeDropForOption(input, (_v) => {
+                t.fail('Do not enter this path');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
diff --git a/__tests__/PlainResult/as_mut.test.mjs b/__tests__/PlainResult/as_mut.test.mjs
index 6d568937..c46c31b7 100644
--- a/__tests__/PlainResult/as_mut.test.mjs
+++ b/__tests__/PlainResult/as_mut.test.mjs
@@ -16,3 +16,22 @@ for (const factory of FUNC_LIST) {
         t.deepEqual(actual, original, 'should be the same shape');
     });
 }
+
+const TEST_CASE_LIST = [
+    ['Ok', createOk(Math.random())],
+    ['Err', createErr(new Error())],
+];
+for (const [typename, inputValue] of TEST_CASE_LIST) {
+    test(`should throw if the passed value is frozen: ${typename}`, (t) => {
+        const input = Object.freeze(inputValue);
+        t.throws(
+            () => {
+                asMutResult(input);
+            },
+            {
+                instanceOf: TypeError,
+                message: `input is frozen, cannot cast to mutable`,
+            }
+        );
+    });
+}
diff --git a/__tests__/PlainResult/drop/test_drop_both.test.mjs b/__tests__/PlainResult/drop/test_drop_both.test.mjs
index 7920eaad..274e1a15 100644
--- a/__tests__/PlainResult/drop/test_drop_both.test.mjs
+++ b/__tests__/PlainResult/drop/test_drop_both.test.mjs
@@ -43,3 +43,47 @@ test('with Err', (t) => {
     t.is(actual.err, undefined, 'should be released');
     t.true(Object.isFrozen(actual), 'should be frozen');
 });
+
+test('should throw if the passed value is frozen: Ok', (t) => {
+    const input = createOk(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropBothForResult(
+                actual,
+                (_ok) => {
+                    t.fail('Do not enter this path. _ok callback');
+                },
+                (_err) => {
+                    t.fail('Do not enter this path. _err callback');
+                }
+            );
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
+
+test('should throw if the passed value is frozen: Err', (t) => {
+    const input = createErr(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropBothForResult(
+                actual,
+                (_ok) => {
+                    t.fail('Do not enter this path. _ok callback');
+                },
+                (_err) => {
+                    t.fail('Do not enter this path. _err callback');
+                }
+            );
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
diff --git a/__tests__/PlainResult/drop/test_drop_err.test.mjs b/__tests__/PlainResult/drop/test_drop_err.test.mjs
index 272c1964..464775f2 100644
--- a/__tests__/PlainResult/drop/test_drop_err.test.mjs
+++ b/__tests__/PlainResult/drop/test_drop_err.test.mjs
@@ -28,3 +28,35 @@ test('with Err', (t) => {
     t.is(actual.err, undefined, 'should be released');
     t.true(Object.isFrozen(actual), 'should be frozen');
 });
+
+test('should throw if the passed value is frozen: Ok', (t) => {
+    const input = createOk(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropErrForResult(actual, (_err) => {
+                t.fail('Do not enter this path.');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
+
+test('should throw if the passed value is frozen: Err', (t) => {
+    const input = createErr(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropErrForResult(actual, (_err) => {
+                t.fail('Do not enter this path.');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
diff --git a/__tests__/PlainResult/drop/test_drop_ok.test.mjs b/__tests__/PlainResult/drop/test_drop_ok.test.mjs
index 7d73721b..924d8782 100644
--- a/__tests__/PlainResult/drop/test_drop_ok.test.mjs
+++ b/__tests__/PlainResult/drop/test_drop_ok.test.mjs
@@ -28,3 +28,35 @@ test('with Err', (t) => {
     t.is(actual.ok, false, 'should not be modified');
     t.true(Object.isFrozen(actual), 'should be frozen');
 });
+
+test('should throw if the passed value is frozen: Ok', (t) => {
+    const input = createOk(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropOkForResult(actual, (_ok) => {
+                t.fail('Do not enter this path.');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});
+
+test('should throw if the passed value is frozen: Err', (t) => {
+    const input = createErr(Math.random());
+    const actual = Object.freeze(input);
+    t.throws(
+        () => {
+            unsafeDropOkForResult(actual, (_ok) => {
+                t.fail('Do not enter this path.');
+            });
+        },
+        {
+            instanceOf: TypeError,
+            message: `input is frozen, cannot cast to mutable`,
+        }
+    );
+});