Compare commits

...

1 Commits

Author SHA1 Message Date
Don Isaac
df422ca59a build: auto-generate JS error factories 2025-03-25 14:05:04 -07:00
2 changed files with 280 additions and 25 deletions

View File

@@ -1,18 +1,49 @@
// used by generate-node-errors.ts
type ErrorCodeMapping = Array<
[
/** error.code */
code: string,
/** Constructor **/
ctor: typeof TypeError | typeof RangeError | typeof Error | typeof SyntaxError,
/** error.name. Defaults to `Constructor.name` (that is, mapping[1].name */
name?: string,
(typeof TypeError | typeof RangeError | typeof Error | typeof SyntaxError)?,
(typeof TypeError | typeof RangeError | typeof Error | typeof SyntaxError)?,
]
>;
const errors: ErrorCodeMapping = [
type ErrorConstructor = typeof TypeError | typeof RangeError | typeof Error | typeof SyntaxError;
type SimpleMapping = [
/** error.code */
code: string,
/** Constructor **/
ctor: ErrorConstructor,
/** error.name. Defaults to `Constructor.name` (that is, mapping[1].name */
name?: string,
/**
* Other error classes this error can derive from. Separate factories will be generated for each.
*/
...extraCtors: ErrorConstructor[],
];
type CompleteMapping = [
/** `error.code` */
code: string,
{
/**
* Message template. When not provided, this error will need a hand-written factory.
* @default undefined
*/
message?: string;
/**
* @default Error
*/
ctor?: ErrorConstructor;
/**
* Other error classes this error can derive from. Separate factories will be generated for each.
*/
extraCtors?: ErrorConstructor[];
/**
* `error.name`.
* @default ctor.name
*/
name?: string;
},
];
type ErrorCodeMapping = SimpleMapping | CompleteMapping;
const errors: ErrorCodeMapping[] = [
["ABORT_ERR", Error, "AbortError"],
["ERR_ACCESS_DENIED", Error],
["ERR_AMBIGUOUS_ARGUMENT", TypeError],
@@ -131,7 +162,7 @@ const errors: ErrorCodeMapping = [
["ERR_INVALID_PROTOCOL", TypeError],
["ERR_INVALID_RETURN_VALUE", TypeError],
["ERR_INVALID_STATE", Error, undefined, TypeError, RangeError],
["ERR_INVALID_THIS", TypeError],
["ERR_INVALID_THIS", { ctor: TypeError, message: 'Value of \\"this\\" must be of type {type}' }],
["ERR_INVALID_URI", URIError],
["ERR_INVALID_URL_SCHEME", TypeError],
["ERR_INVALID_URL", TypeError],
@@ -246,4 +277,6 @@ const errors: ErrorCodeMapping = [
["MODULE_NOT_FOUND", Error],
["ERR_INTERNAL_ASSERTION", Error],
];
export type { ErrorCodeMapping, ErrorConstructor, SimpleMapping, CompleteMapping };
export default errors;

View File

@@ -1,16 +1,15 @@
import path from "node:path";
import NodeErrors from "../bun.js/bindings/ErrorCode.ts";
import assert from "node:assert";
import NodeErrors, { ErrorCodeMapping, ErrorConstructor, SimpleMapping } from "../bun.js/bindings/ErrorCode.ts";
const outputDir = process.argv[2];
if (!outputDir) {
throw new Error("Missing output directory");
}
const extra_count = NodeErrors.map(x => x.slice(3))
.filter(x => x.length > 0)
.reduce((ac, cv) => ac + cv.length, 0);
const count = NodeErrors.length + extra_count;
const Errors: NormalizedMapping[] = NodeErrors.map(normalizeMapping);
const count = Errors.reduce((count, error) => count + error.extraCtors.length, Errors.length);
if (count > 256) {
// increase size of enum's to have more tags
// src/bun.js/node/types.zig#Encoding
@@ -18,9 +17,10 @@ if (count > 256) {
throw new Error("NodeError count exceeds u8");
}
let enumHeader = ``;
let listHeader = ``;
let zig = ``;
let enumHeader = ``; // ErrorCode+List.h
let listHeader = ``; // ErrorCode+Data.h
let zig = ``; // ErrorCode.zig
const factory = { cpp: ``, h: `` }; // ErrorCode+Factory.{h,cpp}
enumHeader = `
// clang-format off
@@ -82,10 +82,35 @@ pub const Error = enum(u8) {
`;
factory.h = /* cpp */ `
// clang-format off
// Generated by: src/codegen/generate-node-errors.ts
#pragma once
#include <JavaScriptCore/JSObject.h>
#include <JavaScriptCore/CallFrame.h>
#include <wtf/text/WTFString.h>
namespace Bun {
namespace ERR {
`;
factory.cpp = /* cpp */ `
// clang-format off
// Generated by: src/codegen/generate-node-errors.ts
#include "ErrorCode+Factory.h"
#include "ErrorCode+List.h"
#include "ErrorCode.h"
#include <wtf/text/StringBuilder.h>
namespace Bun {
namespace ERR {
`;
let i = 0;
let listForUsingNamespace = "";
for (let [code, constructor, name, ...other_constructors] of NodeErrors) {
if (name == null) name = constructor.name;
for (const mapping of Errors) {
let { code, ctor: constructor, name, message, extraCtors } = mapping;
enumHeader += ` ${code} = ${i},\n`;
listHeader += ` { JSC::ErrorType::${constructor.name}, "${name}"_s, "${code}"_s },\n`;
zig += ` /// ${name}: ${code} (instanceof ${constructor.name})\n`;
@@ -96,7 +121,14 @@ for (let [code, constructor, name, ...other_constructors] of NodeErrors) {
listForUsingNamespace += ` }\n`;
i++;
for (const con of other_constructors) {
if (message) {
const template = parseTemplate(message);
const { declarations, impls } = renderErrorFactory(mapping, template);
factory.h += declarations + "\n";
factory.cpp += impls + "\n\n";
}
for (const con of extraCtors) {
if (con == null) continue;
if (name == null) name = con.name;
enumHeader += ` ${code}_${con.name} = ${i},\n`;
@@ -120,6 +152,17 @@ listHeader += `
};
`;
factory.h += `
} // namespace ERR
} // namespace Bun
`;
factory.cpp += `
} // namespace ERR
} // namespace Bun
`;
zig += `
@@ -156,3 +199,182 @@ ${listForUsingNamespace}
await Bun.write(path.join(outputDir, "ErrorCode+List.h"), enumHeader);
await Bun.write(path.join(outputDir, "ErrorCode+Data.h"), listHeader);
await Bun.write(path.join(outputDir, "ErrorCode.zig"), zig);
await Bun.write(path.join(outputDir, "ErrorCode+Factory.h"), factory.h);
await Bun.write(path.join(outputDir, "ErrorCode+Factory.cpp"), factory.cpp);
// =============================================================================
function renderErrorFactory(error: NormalizedMapping, template: TemplatePart[]) {
const { code, ctor, name, message } = error;
assert(message && template.length, "Error factories can only be created for errors with a message");
const concreteParams = template
.filter((part): part is TemplatePlaceholder => part.kind === "placeholder")
.map(({ name }) => ({ name, type: `WTF::String` }) as const);
const concreteSignature = concreteParams.map(({ name, type }) => `const ${type}& ${name}`).join(", ");
var declarations = /* cpp */ `
/// Create a new ${code} error object. This factory may throw an exception.
/// \`${message}\`
JSC::JSObject* CREATE_${code}(JSC::JSGlobalObject* globalObject, const JSC::CallFrame* callFrame);
/// \`${message}\`
JSC::JSObject* CREATE_${code}(JSC::JSGlobalObject* globalObject, ${concreteSignature});
`;
var jsFunctionImpl: string = [
`JSC::JSObject* CREATE_${code}(JSC::JSGlobalObject* globalObject, const JSC::CallFrame* callFrame) {`,
` JSC::VM& vm = globalObject->vm();`,
` auto scope = DECLARE_THROW_SCOPE(vm);`,
``
].join("\n");
let i = 0;
for (const param of concreteParams) {
jsFunctionImpl += [
` auto arg${i} = callFrame->argument(${i});`,
` ${param.type} ${param.name} = arg${i}.toWTFString(globalObject);`,
` RETURN_IF_EXCEPTION(scope, {});`,
].join("\n");
i++;
}
jsFunctionImpl += [
``,
` return CREATE_${code}(globalObject, ${concreteParams.map(({ name }) => name).join(", ")});`,
`}`,
].join("\n");
var concreteImpl = [
`JSC::JSObject* CREATE_${code}(JSC::JSGlobalObject* globalObject, ${concreteSignature}) {`,
` WTF::StringBuilder builder;`,
``,
].join("\n");
for (const part of template) {
switch (part.kind) {
case "string":
concreteImpl += ` builder.append("${part.text}"_s);\n`;
break;
case "placeholder":
concreteImpl += ` builder.append(${part.name});\n`;
break;
default:
throw new TypeError(`Invalid template part: unknown kind ${(part as any).kind}`);
}
}
concreteImpl += ` return createError(globalObject, Bun::ErrorCode::${code}, builder.toString());\n`;
concreteImpl += `}`;
var impls = jsFunctionImpl + "\n" + concreteImpl;
return {
declarations,
impls,
};
}
// =============================================================================
function isSimpleMapping(mapping: ErrorCodeMapping): mapping is SimpleMapping {
if (mapping.length == 1 || mapping.length > 2) return true;
const [, ctor] = mapping;
return ctor == null || typeof ctor === "function";
}
type NormalizedMapping = ReturnType<typeof normalizeMapping>;
function normalizeMapping(mapping: ErrorCodeMapping) {
assert(mapping.length > 0);
if (isSimpleMapping(mapping)) {
// simple mapping
var [code, ctor = Error, name = ctor.name, ...extraCtors] = mapping;
return {
code,
ctor,
name,
extraCtors,
message: undefined,
} as const;
} else {
const [code, { ctor = Error, name = ctor.name, message, extraCtors = [] }] = mapping;
return {
code,
ctor,
name,
extraCtors,
message,
} as const;
}
}
type TemplateText = {
kind: "string";
text: string;
};
type TemplatePlaceholder = {
kind: "placeholder";
name: string;
};
type TemplatePart = TemplateText | TemplatePlaceholder;
/**
* Parse a template like `Unexpected value {value}: {reason}` into a list of
* {@link TemplatePart}
*
* @note does not allow nested braces
*/
function parseTemplate(messageTemplate: string): TemplatePart[] {
if (!messageTemplate?.length) {
throw new SyntaxError("Invalid template: template string is empty");
}
const parts: TemplatePart[] = [];
// -1 means we're not inside a brace
let braceStart = -1;
let start = 0;
const BRACE_OPEN = "{".charCodeAt(0);
const BRACE_CLOSE = "}".charCodeAt(0);
for (let i = 0; i < messageTemplate.length; i++) {
const char = messageTemplate.charCodeAt(i);
switch (char) {
case BRACE_OPEN: {
if (braceStart !== -1)
throw new SyntaxError("Invalid template: an opening brace was found without a closing brace");
braceStart = i;
if (i > start) {
// push text
parts.push({
kind: "string",
text: messageTemplate.slice(start, i),
});
start = i;
}
break;
}
case BRACE_CLOSE: {
if (braceStart === -1)
throw new SyntaxError("Invalid template: a closing brace was found without an opening brace");
parts.push({
kind: "placeholder",
name: messageTemplate.slice(braceStart + 1, i),
});
braceStart = -1;
break;
}
default: // text
break;
}
}
if (braceStart !== -1) {
throw new SyntaxError("Invalid template: an opening brace was found without a closing brace");
}
return parts;
}