diff --git a/reconcile-js/src/core.ts b/reconcile-js/src/core.ts index 1139dd5..333eda6 100644 --- a/reconcile-js/src/core.ts +++ b/reconcile-js/src/core.ts @@ -254,6 +254,7 @@ export interface ReconcileApi { * @param diff - The compact diff array (inserts as strings, deletes as negative integers, retained spans as positive integers) * @param tokenizer - The tokenisation strategy, which is the same as used in `reconcile`. * @returns The reconstructed changed text as a string. + * @throws {Error} If the diff contains a non-integer, or spans more characters than `original` has. */ undiff( original: string, diff --git a/reconcile-js/src/index.test.ts b/reconcile-js/src/index.test.ts index e88f2ed..4d9efb5 100644 --- a/reconcile-js/src/index.test.ts +++ b/reconcile-js/src/index.test.ts @@ -91,6 +91,27 @@ describe.each(backends)('reconcile [$name]', ({ api }) => { expect(withBigints.some((item) => typeof item === 'bigint')).toBe(true); expect(undiff(original, withBigints)).toEqual(changed); }); + + // A trap would also be an `Error` (`RuntimeError: unreachable`), so check the message + it.each([ + { invalidDiff: [10], message: 'Invalid diff' }, + { invalidDiff: [2 ** 32], message: 'Invalid diff' }, + { invalidDiff: [-(2 ** 40)], message: 'Invalid diff' }, + { invalidDiff: [2n ** 40n], message: 'Invalid diff' }, + { invalidDiff: [1.5], message: 'must be an integer' }, + { invalidDiff: [-0.4, 'X', 3], message: 'must be an integer' }, + { invalidDiff: [NaN], message: 'NaN' }, + ])('undiff rejects $invalidDiff with an Error', ({ invalidDiff, message }) => { + let thrown: unknown; + try { + undiff('abc', invalidDiff); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(Error); + expect((thrown as Error).message).toContain(message); + }); }); describe.each(backends)('WASM cleanup after exceptions [$name]', ({ api, bindings }) => { diff --git a/src/wasm.rs b/src/wasm.rs index 06cccbe..e7195a1 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -81,45 +81,44 @@ pub fn generic_reconcile( /// /// # Errors /// -/// Returns a JS error if integer overflow occurs during diff computation. +/// Throws a JS `Error` if integer overflow occurs during diff computation. #[wasm_bindgen(js_name = diff)] pub fn diff( parent: &str, changed: &TextWithCursors, tokenizer: BuiltinTokenizer, -) -> Result, JsValue> { +) -> Result, JsError> { set_panic_hook(); let edited_text = EditedText::from_strings_with_tokenizer(parent, changed, &*tokenizer); edited_text .to_diff() .map(|diff| diff.into_iter().map(std::convert::Into::into).collect()) - .map_err(|e| JsValue::from_str(&e.to_string())) + .map_err(JsError::from) } /// Inverse of `diff`, applies a compact diff representation to a parent text /// /// # Errors /// -/// Returns a JS error if the diff format is invalid or references ranges +/// Throws a JS `Error` if the diff format is invalid or references ranges /// exceeding the original text length. #[wasm_bindgen(js_name = undiff)] pub fn undiff( parent: &str, diff: Vec, tokenizer: BuiltinTokenizer, -) -> Result { +) -> Result { set_panic_hook(); let parsed_diff: Vec<_> = diff .into_iter() .map(std::convert::TryInto::try_into) - .collect::>() - .map_err(|e: crate::types::number_or_text::DeserialisationError| -> JsValue { e.into() })?; + .collect::>()?; EditedText::from_diff(parent, parsed_diff, &*tokenizer) .map(|edited_text| edited_text.apply().text()) - .map_err(|e| JsValue::from_str(&e.to_string())) + .map_err(JsError::from) } fn set_panic_hook() {