Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve transform error handling with compiled function annotations #88

Merged
merged 13 commits into from
Dec 4, 2024
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
2 changes: 1 addition & 1 deletion deps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * as path from "jsr:@std/[email protected]";
export * as path from "https://deno.land/[email protected]/path/mod.ts";
export * as html from "jsr:@std/[email protected]";

export * as astring from "jsr:@davidbonnet/[email protected]";
Expand Down
41 changes: 10 additions & 31 deletions src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import tokenize, { Token } from "./tokenizer.ts";

import type { Loader } from "./loader.ts";
import { transformTemplateCode } from "./transformer.ts";
import { TransformError, transformTemplateCode } from "./transformer.ts";

export interface TemplateResult {
content: string;
Expand Down Expand Up @@ -139,21 +139,8 @@ export class Environment {
try {
code = transformTemplateCode(code, dataVarname);
} catch (error) {
const end = (error as ParseError).start;
const lastPos = code.slice(0, end).lastIndexOf("__pos = ");
const lastPosEnd = code.slice(lastPos).indexOf(";");

if (lastPos > -1 && lastPosEnd > lastPos) {
const pos = parseInt(
code.slice(lastPos + 8, lastPos + lastPosEnd),
10,
);
throw this.createError(
path || "",
source,
pos,
new Error((error as ParseError).message),
);
if (error instanceof TransformError) {
throw this.createError(path, source, error.pos, error);
}

throw error;
Expand Down Expand Up @@ -192,7 +179,7 @@ export class Environment {
const { position, error } = result;

if (error) {
throw this.createError(path || "unknown", source, position, error);
throw this.createError(path, source, position, error);
}

for (const tokenPreprocessor of this.tokenPreprocessors) {
Expand Down Expand Up @@ -325,19 +312,17 @@ export class Environment {
}

createError(
path: string,
source: string,
position: number,
path: string = "unknown",
source: string = "<empty file>",
position: number = 0,
cause: Error,
): Error {
if (!source) {
return cause;
}

const [line, column, code] = errorLine(source, position);

return new Error(
`Error in the template ${path}:${line}:${column}\n\n${code.trim()}\n\n> ${cause.message}\n`,
`Error in the template ${path}:${line}:${column}\n\n${code.trim()}\n\n${
cause.message.replaceAll(/^/gm, "> ")
}\n`,
{ cause },
);
}
Expand Down Expand Up @@ -386,9 +371,3 @@ export function errorLine(
function checkAsync(fn: () => unknown): boolean {
return fn.constructor?.name === "AsyncFunction";
}

interface ParseError extends Error {
start: number;
end: number;
range: [number, number];
}
52 changes: 48 additions & 4 deletions src/transformer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
import { astring, ESTree, meriyah, walker } from "../deps.ts";

// Declare types
interface ParseError extends Error {
start: number;
end: number;
range: [number, number];
loc: Record<"start" | "end", Record<"line" | "column", number>>;
description: string;
}

interface TransformErrorOptions extends ErrorOptions {
pos?: number;
}

export class TransformError extends Error {
pos?: number;
constructor(message: string, options?: TransformErrorOptions) {
super(message);
this.name = "TransformError";
this.pos = options?.pos;
}
}

// List of identifiers that are in globalThis
// but should be accessed as templateState.identifier
const INCLUDE_GLOBAL = [
Expand Down Expand Up @@ -119,7 +141,29 @@ export function transformTemplateCode(
return code;
}

const parsed = meriyah.parseScript(code, { module: true }) as ESTree.Program;
let parsed;
try {
parsed = meriyah.parseScript(code, { module: true }) as ESTree.Program;
} catch (error) {
const { message, start, loc } = error as ParseError;

// Use information from `meriyah` to annotate the part of
// the compiled template function that triggered the ParseError
const annotation = code.split("\n")[loc.start.line - 1] + "\n" +
" ".repeat(loc.start.column) + "\x1b[31m^\x1b[0m";

// Grab the last instance of Vento's `__pos` variable before the
// error was thrown. Pass this back to Vento's createError to
// tie this error with problmatic template code
const matches = [...code.slice(0, start).matchAll(/__pos = (\d+);/g)];
const pos = Number(matches.at(-1)?.[1]);

throw new TransformError(
`[meriyah] ${message}\nthrown while parsing compiled template function:\n\n${annotation}`,
{ pos },
);
}

const tracker = new ScopeTracker();

const exclude = [
Expand All @@ -128,11 +172,11 @@ export function transformTemplateCode(
];

if (parsed.type !== "Program") {
throw new Error("Expected a program");
throw new TransformError("[meriyah] Expected a program");
}

if (parsed.body.length === 0) {
throw new Error("Empty program");
throw new TransformError("[meriyah] Empty program");
}

// Transforms an identifier to a MemberExpression
Expand Down Expand Up @@ -166,7 +210,7 @@ export function transformTemplateCode(
}

walker.walk(parsed, {
enter(node) {
enter(node: ESTree.Node) {
switch (node.type) {
// Track variable declarations
case "VariableDeclaration":
Expand Down
Loading