Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,11 @@ To be released.
multiple source contexts with the same `id`. Duplicate ids now throw a
`TypeError` instead of producing order-dependent results. [[#495], [#746]]

- Fixed `runWith()` and `runWithSync()` discarding the original parse error
when a source context's disposal also throws. The disposal error now
wraps both failures in a `SuppressedError` (following TC39 conventions)
instead of silently replacing the parse error. [[#246], [#771]]

- Fixed `optional()` and `withDefault()` crashing when the parser's state
is an annotation-injected object instead of `undefined`. The state
discrimination in `modifiers.ts` now uses `Array.isArray(state)` to
Expand Down Expand Up @@ -1182,6 +1187,7 @@ To be released.
[#241]: https://github.com/dahlia/optique/issues/241
[#242]: https://github.com/dahlia/optique/issues/242
[#245]: https://github.com/dahlia/optique/issues/245
[#246]: https://github.com/dahlia/optique/issues/246
[#247]: https://github.com/dahlia/optique/issues/247
[#248]: https://github.com/dahlia/optique/issues/248
[#249]: https://github.com/dahlia/optique/issues/249
Expand Down Expand Up @@ -1451,6 +1457,7 @@ To be released.
[#765]: https://github.com/dahlia/optique/pull/765
[#768]: https://github.com/dahlia/optique/issues/768
[#769]: https://github.com/dahlia/optique/pull/769
[#771]: https://github.com/dahlia/optique/pull/771

### @optique/config

Expand Down
250 changes: 250 additions & 0 deletions packages/core/src/facade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6300,6 +6300,131 @@ describe("runWith", () => {
});
assert.equal(disposed, 1);
});

it("should throw SuppressedError when parse fails and disposal also fails", async () => {
let disposed = false;
const context: SourceContext = {
id: Symbol.for("@test/dispose-shadow"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed = true;
throw new Error("dispose failed.");
},
};

const parser = object({
port: argument(integer()),
});

try {
await runWith(parser, "test", [context], {
args: ["not-a-number"],
});
assert.fail("Expected an error to be thrown");
} catch (error) {
assert.ok(error instanceof Error);
assert.equal(error.name, "SuppressedError");
const se = error as Error & { suppressed: unknown; error: unknown };
assert.ok(
se.suppressed instanceof Error,
"suppressed should be the parse error",
);
assert.ok(
se.error instanceof Error,
"error should be the disposal error",
);
assert.equal(
(se.error as Error).message,
"dispose failed.",
);
}

assert.ok(disposed);
});

it("should throw SuppressedError with AggregateError when parse fails and multiple disposals fail", async () => {
const disposed: string[] = [];

const context1: SourceContext = {
id: Symbol.for("@test/dispose-shadow-multi-1"),
getAnnotations() {
return {};
},
[Symbol.asyncDispose]() {
disposed.push("context1");
throw new Error("dispose 1 failed.");
},
};

const context2: SourceContext = {
id: Symbol.for("@test/dispose-shadow-multi-2"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed.push("context2");
throw new Error("dispose 2 failed.");
},
};

const parser = object({
port: argument(integer()),
});

try {
await runWith(parser, "test", [context1, context2], {
args: ["not-a-number"],
});
assert.fail("Expected an error to be thrown");
} catch (error) {
assert.ok(error instanceof Error);
assert.equal(error.name, "SuppressedError");
const se = error as Error & { suppressed: unknown; error: unknown };
assert.ok(
se.suppressed instanceof Error,
"suppressed should be the parse error",
);
assert.ok(
se.error instanceof AggregateError,
"error should be AggregateError from multiple disposal failures",
);
assert.equal((se.error as AggregateError).errors.length, 2);
}

assert.deepEqual(disposed, ["context1", "context2"]);
});

it("should preserve parse error when disposal succeeds", async () => {
let disposed = false;
const context: SourceContext = {
id: Symbol.for("@test/dispose-no-shadow"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed = true;
},
};

const parser = object({
port: argument(integer()),
});

let errorCaught: unknown;
try {
await runWith(parser, "test", [context], {
args: ["not-a-number"],
});
} catch (error) {
errorCaught = error;
}

assert.ok(disposed);
assert.notEqual((errorCaught as Error).name, "SuppressedError");
assert.ok(errorCaught instanceof Error);
});
});

describe("options passthrough", () => {
Expand Down Expand Up @@ -6997,6 +7122,131 @@ describe("runWithSync", () => {
});
assert.equal(disposed, 1);
});

it("should throw SuppressedError when sync parse fails and disposal also fails", () => {
let disposed = false;
const context: SourceContext = {
id: Symbol.for("@test/sync-dispose-shadow"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed = true;
throw new Error("sync dispose failed.");
},
};

const parser = object({
port: argument(integer()),
});

try {
runWithSync(parser, "test", [context], {
args: ["not-a-number"],
});
assert.fail("Expected an error to be thrown");
} catch (error) {
assert.ok(error instanceof Error);
assert.equal(error.name, "SuppressedError");
const se = error as Error & { suppressed: unknown; error: unknown };
assert.ok(
se.suppressed instanceof Error,
"suppressed should be the parse error",
);
assert.ok(
se.error instanceof Error,
"error should be the disposal error",
);
assert.equal(
(se.error as Error).message,
"sync dispose failed.",
);
}

assert.ok(disposed);
});

it("should throw SuppressedError with AggregateError when sync parse fails and multiple disposals fail", () => {
const disposed: string[] = [];

const context1: SourceContext = {
id: Symbol.for("@test/sync-dispose-shadow-multi-1"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed.push("context1");
throw new Error("sync dispose 1 failed.");
},
};

const context2: SourceContext = {
id: Symbol.for("@test/sync-dispose-shadow-multi-2"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed.push("context2");
throw new Error("sync dispose 2 failed.");
},
};

const parser = object({
port: argument(integer()),
});

try {
runWithSync(parser, "test", [context1, context2], {
args: ["not-a-number"],
});
assert.fail("Expected an error to be thrown");
} catch (error) {
assert.ok(error instanceof Error);
assert.equal(error.name, "SuppressedError");
const se = error as Error & { suppressed: unknown; error: unknown };
assert.ok(
se.suppressed instanceof Error,
"suppressed should be the parse error",
);
assert.ok(
se.error instanceof AggregateError,
"error should be AggregateError from multiple disposal failures",
);
assert.equal((se.error as AggregateError).errors.length, 2);
}

assert.deepEqual(disposed, ["context1", "context2"]);
});

it("should preserve sync parse error when disposal succeeds", () => {
let disposed = false;
const context: SourceContext = {
id: Symbol.for("@test/sync-dispose-no-shadow"),
getAnnotations() {
return {};
},
[Symbol.dispose]() {
disposed = true;
},
};

const parser = object({
port: argument(integer()),
});

let errorCaught: unknown;
try {
runWithSync(parser, "test", [context], {
args: ["not-a-number"],
});
} catch (error) {
errorCaught = error;
}

assert.ok(disposed);
assert.notEqual((errorCaught as Error).name, "SuppressedError");
assert.ok(errorCaught instanceof Error);
});
});

describe("priority handling (sync)", () => {
Expand Down
Loading
Loading