start porting errors

pull/11294/head
Rich Harris 2 years ago
parent 6671dd68f0
commit 45f375a5eb

@ -2,6 +2,7 @@ packages/**/dist/*.js
packages/**/build/*.js
packages/**/npm/**/*
packages/**/config/*.js
packages/svelte/src/compiler/errors.js
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual*

@ -0,0 +1,7 @@
## unclosed_element
%element% was left open
## unclosed_block
Block was left open

@ -0,0 +1,215 @@
// @ts-check
import fs from 'node:fs';
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import * as esrap from 'esrap';
const messages = {};
const seen = new Set();
for (const category of fs.readdirSync('messages')) {
messages[category] = {};
for (const file of fs.readdirSync(`messages/${category}`)) {
if (!file.endsWith('.md')) continue;
const markdown = fs.readFileSync(`messages/${category}/${file}`, 'utf-8');
for (const match of markdown.matchAll(/## ([\w]+)\n\n([^]+?)(?=$|\n\n## )/g)) {
const [_, code, text] = match;
if (seen.has(code)) {
throw new Error(`Duplicate message code ${category}/${code}`);
}
seen.add(code);
messages[category][code] = text.trim();
}
}
}
function transform(name, dest) {
const source = fs.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8');
const comments = [];
const ast = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'module',
onComment: (block, value, start, end) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({ type: block ? 'Block' : 'Line', value, start, end });
}
});
walk(ast, null, {
_(node, { next }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = comments.shift();
// @ts-expect-error
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const slice = source.slice(node.end, comments[0].start);
if (/^[,) \t]*$/.test(slice)) {
// @ts-expect-error
node.trailingComments = [comments.shift()];
}
}
}
});
const category = messages[name];
// find the `export function CODE` node
const index = ast.body.findIndex((node) => {
if (
node.type === 'ExportNamedDeclaration' &&
node.declaration &&
node.declaration.type === 'FunctionDeclaration'
) {
return node.declaration.id.name === 'CODE';
}
});
if (index === -1) throw new Error(`missing export function CODE in ${name}.js`);
const template_node = ast.body[index];
ast.body.splice(index, 1);
for (const code in category) {
const message = category[code];
const vars = [];
for (const match of message.matchAll(/%(\w+)%/g)) {
const name = match[1];
if (!vars.includes(name)) {
vars.push(match[1]);
}
}
const clone = walk(/** @type {import('estree').Node} */ (template_node), null, {
// @ts-expect-error Block is a block comment, which is not recognised
Block(node, context) {
if (!node.value.includes('PARAMETER')) return;
const value = node.value
.split('\n')
.map((line) => {
if (line.includes('PARAMETER')) {
return vars.map((name) => ` * @param {string} ${name}`).join('\n');
}
return line;
})
.join('\n');
if (value !== node.value) {
return { ...node, value };
}
},
FunctionDeclaration(node, context) {
if (node.id.name !== 'CODE') return;
const params = [];
for (const param of node.params) {
if (param.type === 'Identifier' && param.name === 'PARAMETER') {
params.push(...vars.map((name) => ({ type: 'Identifier', name })));
} else {
params.push(param);
}
}
return /** @type {import('estree').FunctionDeclaration} */ ({
.../** @type {import('estree').FunctionDeclaration} */ (context.next()),
params,
id: {
...node.id,
name: code
}
});
},
Literal(node) {
if (node.value === 'CODE') {
return {
type: 'Literal',
value: code
};
}
},
Identifier(node) {
if (node.name !== 'MESSAGE') return;
if (/%\w+%/.test(message)) {
const parts = message.split(/(%\w+%)/);
/** @type {import('estree').Expression[]} */
const expressions = [];
/** @type {import('estree').TemplateElement[]} */
const quasis = [];
for (let i = 0; i < parts.length; i += 1) {
const part = parts[i];
if (i % 2 === 0) {
quasis.push({
type: 'TemplateElement',
value: {
raw: part,
cooked: part
},
tail: i === parts.length - 1
});
} else {
expressions.push({
type: 'Identifier',
name: part.slice(1, -1)
});
}
}
return {
type: 'TemplateLiteral',
expressions,
quasis
};
}
return {
type: 'Literal',
value: message
};
}
});
// @ts-expect-error
ast.body.push(clone);
}
// @ts-expect-error
const module = esrap.print(ast);
fs.writeFileSync(
dest,
`/* This file is generated by scripts/process-messages.js. Do not edit! */\n\n` + module.code,
'utf-8'
);
}
transform('compile-errors', 'src/compiler/errors.js');

@ -0,0 +1,74 @@
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
name = 'CompileError';
/** @type {import('#compiler').CompileError['filename']} */
filename = undefined;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
}
}
/**
*
* @param {number | NodeLike} node
* @param {string} code
* @param {string} message
* @returns {never}
*/
function __error(node, code, message) {
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(
code,
message,
start !== undefined && end !== undefined ? [start, end] : undefined
);
}
/**
* @param {number | NodeLike} node
* @param {string} PARAMETER
* @returns {never}
*/
export function CODE(node, PARAMETER) {
__error(node, 'CODE', MESSAGE);
}

@ -0,0 +1,76 @@
/* This file is generated by scripts/process-messages.js. Do not edit! */
/** @typedef {{ start?: number, end?: number }} NodeLike */
// interface is duplicated between here (used internally) and ./interfaces.js
// (exposed publicly), and I'm not sure how to avoid that
export class CompileError extends Error {
name = 'CompileError';
/** @type {import('#compiler').CompileError['filename']} */
filename = undefined;
/** @type {import('#compiler').CompileError['position']} */
position = undefined;
/** @type {import('#compiler').CompileError['start']} */
start = undefined;
/** @type {import('#compiler').CompileError['end']} */
end = undefined;
/**
*
* @param {string} code
* @param {string} message
* @param {[number, number] | undefined} position
*/
constructor(code, message, position) {
super(message);
this.code = code;
this.position = position;
}
toString() {
let out = `${this.name}: ${this.message}`;
out += `\n(${this.code})`;
if (this.filename) {
out += `\n${this.filename}`;
if (this.start) {
out += `${this.start.line}:${this.start.column}`;
}
}
return out;
}
}
/**
*
* @param {number | NodeLike} node
* @param {string} code
* @param {string} message
* @returns {never}
*/
function __error(node, code, message) {
const start = typeof node === 'number' ? node : node?.start;
const end = typeof node === 'number' ? node : node?.end;
throw new CompileError(code, message, start !== undefined && end !== undefined ? [start, end] : undefined);
}
/**
* @param {number | NodeLike} node
* @param {string} element
* @returns {never}
*/
export function unclosed_element(node, element) {
__error(node, "unclosed_element", `${element} was left open`);
}
/**
* @param {number | NodeLike} node
* @returns {never}
*/
export function unclosed_block(node) {
__error(node, "unclosed_block", "Block was left open");
}

@ -5,6 +5,7 @@ import { regex_whitespace } from '../patterns.js';
import { reserved } from './utils/names.js';
import full_char_code_at from './utils/full_char_code_at.js';
import { error } from '../../errors-tmp.js';
import * as errors from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { getLocator } from 'locate-character';
@ -92,10 +93,10 @@ export class Parser {
if (current.type === 'RegularElement') {
current.end = current.start + 1;
error(current, 'unclosed-element', current.name);
errors.unclosed_element(current, `<${current.name}>`);
} else {
current.end = current.start + 1;
error(current, 'unclosed-block');
errors.unclosed_block(current);
}
}

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'unclosed-block',
code: 'unclosed_block',
message: 'Block was left open',
position: [0, 1]
}

@ -2,7 +2,7 @@ import { test } from '../../test';
export default test({
error: {
code: 'unclosed-element',
code: 'unclosed_element',
message: '<div> was left open',
position: [0, 1]
}

Loading…
Cancel
Save