Skip to content

Commit

Permalink
Get rid of ParseError class and use SyntaxErrors instead.
Browse files Browse the repository at this point in the history
Reviewed by @tolmasky.
  • Loading branch information
tolmasky committed Mar 4, 2022
1 parent db4b017 commit fa2c7c7
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 195 deletions.
254 changes: 107 additions & 147 deletions packages/babel-parser/src/parse-error.js
Expand Up @@ -3,198 +3,158 @@
import { Position } from "./util/location";
import type { NodeBase } from "./types";
import {
instantiate,
type ParseErrorCode,
ParseErrorCodes,
type ParseErrorCredentials,
} from "./parse-error/credentials";

const ArrayIsArray = Array.isArray;
const {
assign: ObjectAssign,
defineProperty: ObjectDefineProperty,
getPrototypeOf: ObjectGetPrototypeOf,
keys: ObjectKeys,
} = Object;

type ToMessage<ErrorDetails> = (self: ErrorDetails) => string;

const StandardMessage = Symbol("StandardMessage");

// This should really be an abstract class, but that concept doesn't exist in
// Flow, outside of just creating an interface, but then you can't specify that
// it's a subclass of `SyntaxError`. You could do something like:
//
// interface IParseError<ErrorDetails> { ... }
// type ParseError<ErrorDetails> = SyntaxError & IParseError<ErrorDetails>;
//
// But this is just a more complicated way of getting the same behavior, with
// the added clumsiness of not being able to extends ParseError directly. So,
// to keep things simple and prepare for a Typescript future, we just make it a
// "private" superclass that we exclusively subclass:

export class ParseError<ErrorDetails> extends SyntaxError {
// Babel uses "normal" SyntaxErrors for it's errors, but adds some extra
// functionality. This functionality is defined in the
// `ParseErrorSpecification` interface below. We may choose to change to someday
// give our errors their own full-blown class, but until then this allow us to
// keep all the desirable properties of SyntaxErrors (like their name in stack
// traces, etc.), and also allows us to punt on any publically facing
// class-hierarchy decisions until Babel 8.
interface ParseErrorSpecification<ErrorDetails> {
// Look, these *could* be readonly, but then Flow complains when we initially
// set them. We could do a whole dance and make a special interface that's not
// readonly for when we create the error, then cast it to the readonly
// interface for public use, but the previous implementation didn't have them
// as readonly, so let's just not worry about it for now.
code: ParseErrorCode;
reasonCode: string;
syntaxPlugin?: string;

missingPlugin?: string | string[];

loc: Position;
details: ErrorDetails;

// There are no optional fields in classes in Flow, so we can't list these
// here: https://github.com/facebook/flow/issues/2859
// syntaxPlugin?: SyntaxPlugin
// missingPlugin?: string[]

constructor({
loc,
details,
}: {
loc: Position,
details: ErrorDetails,
}): ParseError<ErrorDetails> {
super();

this.loc = loc;

ObjectDefineProperty(this, "details", {
value: details,
enumerable: false,
});

// $FlowIgnore
if (details.missingPlugin) {
ObjectDefineProperty(this, "missingPlugin", {
get() {
return this.details.missingPlugin;
},
enumerable: true,
});
}

return this;
}

clone({
loc,
details,
}: {
loc?: Position,
details?: ErrorDetails,
} = {}) {
return new (ObjectGetPrototypeOf(this).constructor)({
loc: loc || this.loc,
details: { ...this.details, ...details },
});
}

get pos() {
return this.loc.index;
}
// We should consider removing this as it now just contains the same
// information as `loc.index`.
// pos: number;
}

function toParseErrorClass<ErrorDetails>(
toMessage: ToMessage<ErrorDetails>,
credentials: ParseErrorCredentials,
): Class<ParseError<ErrorDetails>> {
return class extends ParseError<ErrorDetails> {
#message: typeof StandardMessage | string = StandardMessage;

constructor(...args): ParseError<ErrorDetails> {
super(...args);
// $FlowIgnore - Only necessary because we can't make syntaxPlugin optional.
ObjectAssign(this, credentials);
return this;
}

get message() {
return this.#message !== StandardMessage
? String(this.#message)
: `${toMessage(this.details)} (${this.loc.line}:${this.loc.column})`;
}

set message(message) {
this.#message = message;
}
};
export type ParseError<ErrorDetails> = SyntaxError &
ParseErrorSpecification<ErrorDetails>;

// By `ParseErrorConstructor`, we mean something like new-less style
// ErrorConstructor[1], since `ParseError`'s are not themselves actually
// separate classes from `SyntaxError`'s. Something like `toParseError` would
// also make sense, but that gets confusing in a second when we have
// `toParseError` factories, which would then be `toToParseErrors`.
//
// 1. https://github.com/microsoft/TypeScript/blob/v4.5.5/lib/lib.es5.d.ts#L1027
export type ParseErrorConstructor<ErrorDetails> = ({
loc: Position,
details: ErrorDetails,
}) => ParseError<ErrorDetails>;

function toParseErrorConstructor<ErrorDetails: Object>({
toMessage,
...properties
}: ParseErrorCredentials<ErrorDetails>): ParseErrorConstructor<ErrorDetails> {
return ({ loc, details }: { loc: Position, details: ErrorDetails }) =>
instantiate<ParseError<ErrorDetails>>(
SyntaxError,
{ ...properties, loc },
{
details: { value: details, enumerable: false },
message: {
get: ({ details, loc }) =>
`${toMessage(details)} (${loc.line}:${loc.column})`,
set: (self, value) =>
Object.defineProperty(self, "message", { value }),
},
pos: "loc.index",
missingPlugin: "missingPlugin" in details && "details.missingPlugin",
},
);
}

// This part is tricky, and only necessary due to some bugs in Flow that won't
// be fixed for a year(?): https://github.com/facebook/flow/issues/8838
// Flow has a very difficult time extracting the parameter types of functions,
// so we are forced to pretend the class exists earlier than it does.
// `toParseErrorCredentials` *does not* actuall return a
// `Class<ParseError<ErrorDetails>>`, but we simply mark it as such to "carry"
// the `ErrorDetails` type parameter around. This is not a problem in Typescript
// where this intermediate function actually won't be needed at all.
// This part is tricky. You'll probably notice from the name of this function
// that it is supposed to return `ParseErrorCredentials`, but instead these
// declarations seem to instead imply that they return `ErrorDetails` instead.
// This is because in Flow we can't easily extract parameter types (either from
// functions, like with Typescript's Parameters<f> utility type, or from generic
// types either). As such, this function does double duty, packaging up the
// credentials, but pretending to return `ErrorDetails` to the type system so
// we can make use of that information to put together our
// `ParseErrorConstructor<ReasonCode, ErrorDetails>` types in `toParseError`'s
// $ObjMapi below. This hack won't be necessary when we switch to Typescript.
declare function toParseErrorCredentials<T: string>(
T,
?{ code?: ParseErrorCode, reasonCode?: string } | boolean,
): Class<ParseError<{||}>>;
): ParseErrorConstructor<{||}>;

// ESLint seems to erroneously think that Flow's overloading syntax is an
// accidental redeclaration of the function:
// https://github.com/babel/eslint-plugin-babel/issues/162
// eslint-disable-next-line no-redeclare
declare function toParseErrorCredentials<T>(
(T) => string,
declare function toParseErrorCredentials<ErrorDetails>(
(ErrorDetails) => string,
?{ code?: ParseErrorCode, reasonCode?: string } | boolean,
): Class<ParseError<T>>;
): ParseErrorConstructor<ErrorDetails>;

// See comment about eslint and Flow overloading above.
// eslint-disable-next-line no-redeclare
export function toParseErrorCredentials(toMessageOrMessage, credentials) {
return [
typeof toMessageOrMessage === "string"
? () => toMessageOrMessage
: toMessageOrMessage,
credentials,
];
return {
toMessage:
typeof toMessageOrMessage === "string"
? () => toMessageOrMessage
: toMessageOrMessage,
...credentials,
};
}

declare function toParseErrorClasses(string[]): typeof toParseErrorClasses;
// This is the templated form.
declare function ParseErrorEnum(string[]): typeof ParseErrorEnum;

// See comment about eslint and Flow overloading above.
// eslint-disable-next-line no-redeclare
declare function toParseErrorClasses<T: Object>(
toClasses: (typeof toParseErrorCredentials) => T,
declare function ParseErrorEnum<T>(
toParseErrorCredentials: (typeof toParseErrorCredentials) => T,
syntaxPlugin?: string,
): T;

// toParseErrorClasses can optionally be template tagged to provide a
// syntaxPlugin:
// This could perhaps more appropriately be called `toToParseErrors`, or
// `toParseErrorConstructors` (in the new-less `Error`-constructor sense), but
// `toParseErrors` is probably less confusing when called.
//
// toParseErrorClasses`syntaxPlugin` (_ => ... )
// You call `toParseErrors` with a mapping from `ReasonCode`'s to either error
// messages, or `toMessage` functions that define additional necessary `details`
// needed by the `ParseError`:
//
// toParseErrors`optionalSyntaxPlugin` (_ => ({
// ErrorWithStaticMessage: _("message"),
// ErrorWithDynamicMessage: _<{ type: string }>(({ type }) => `${type}`),
// });
//
// See comment about eslint and Flow overloading above.
// eslint-disable-next-line no-redeclare
export function toParseErrorClasses(argument, syntaxPlugin) {
export function ParseErrorEnum(argument, syntaxPlugin) {
// If the first parameter is an array, that means we were called with a tagged
// template literal. Extract the syntaxPlugin from this, and call again in
// the "normalized" form.
if (ArrayIsArray(argument)) {
return toClasses => toParseErrorClasses(toClasses, argument[0]);
if (Array.isArray(argument)) {
return toParseErrorCredentialsMap =>
ParseErrorEnum(toParseErrorCredentialsMap, argument[0]);
}

const classes = argument(toParseErrorCredentials);
const partialCredentials = argument(toParseErrorCredentials);
const ParseErrorConstructors = {};

for (const reasonCode of ObjectKeys(classes)) {
const [toMessage, credentials = {}] = classes[reasonCode];
const ParseErrorClass = toParseErrorClass(toMessage, {
code: credentials.code || ParseErrorCodes.SyntaxError,
reasonCode: credentials.reasonCode || reasonCode,
for (const reasonCode of Object.keys(partialCredentials)) {
ParseErrorConstructors[reasonCode] = toParseErrorConstructor({
code: ParseErrorCodes.SyntaxError,
reasonCode,
...(syntaxPlugin ? { syntaxPlugin } : {}),
});

classes[reasonCode] = ParseErrorClass;

// We do this for backwards compatibility so that all errors just have the
// "SyntaxError" name in their messages instead of leaking the private
// subclass name.
ObjectDefineProperty(ParseErrorClass.prototype.constructor, "name", {
value: "SyntaxError",
...partialCredentials[reasonCode],
});
}

return classes;
return ParseErrorConstructors;
}

export type RaiseProperties<ErrorDetails> = {|
Expand All @@ -208,10 +168,10 @@ import StrictModeErrors from "./parse-error/strict-mode-errors";
import PipelineOperatorErrors from "./parse-error/pipeline-operator-errors";

export const Errors = {
...toParseErrorClasses(ModuleErrors),
...toParseErrorClasses(StandardErrors),
...toParseErrorClasses(StrictModeErrors),
...toParseErrorClasses`pipelineOperator`(PipelineOperatorErrors),
...ParseErrorEnum(ModuleErrors),
...ParseErrorEnum(StandardErrors),
...ParseErrorEnum(StrictModeErrors),
...ParseErrorEnum`pipelineOperator`(PipelineOperatorErrors),
};

export type { LValAncestor } from "./parse-error/standard-errors";
Expand Down
47 changes: 46 additions & 1 deletion packages/babel-parser/src/parse-error/credentials.js
Expand Up @@ -14,8 +14,53 @@ export type SyntaxPlugin =
| "pipelineOperator"
| "placeholders";

export type ParseErrorCredentials = {
export type ToMessage<ErrorDetails> = (self: ErrorDetails) => string;

export type ParseErrorCredentials<ErrorDetails> = {
code: ParseErrorCode,
reasonCode: string,
syntaxPlugin?: SyntaxPlugin,

toMessage: ToMessage<ErrorDetails>,
};

const thisify = <T>(f: (self: any, ...args: any[]) => T): ((...any[]) => T) =>
function (...args: any[]): T {
return ((f(this, ...args): any): T);
};

const reflect = (keys: string[], last = keys.length - 1) => ({
get: self => keys.reduce((object, key) => object[key], self),
set: (self, value) =>
keys.reduce(
(item, key, i) => (i === last ? (item[key] = value) : item[key]),
self,
),
});

const instantiate = <T>(
constructor: () => any,
properties: Object,
descriptors: Object,
) =>
Object.keys(descriptors)
.map(key => [key, descriptors[key]])
.filter(([, descriptor]) => !!descriptor)
.map(([key, descriptor]) => [
key,
typeof descriptor === "string"
? reflect(descriptor.split("."))
: descriptor,
])
.reduce(
(instance, [key, descriptor]) =>
Object.defineProperty(instance, key, {
configurable: true,
...descriptor,
...(descriptor.get && { get: thisify<any>(descriptor.get) }),
...(descriptor.set && { set: thisify<void>(descriptor.set) }),
}),
Object.assign((new constructor(): T), properties),
);

export { instantiate };
2 changes: 1 addition & 1 deletion packages/babel-parser/src/parser/expression.js
Expand Up @@ -67,7 +67,7 @@ import {
newAsyncArrowScope,
newExpressionScope,
} from "../util/expression-scope";
import { Errors, ParseError } from "../parse-error";
import { Errors, type ParseError } from "../parse-error";
import { UnparenthesizedPipeBodyDescriptions } from "../parse-error/pipeline-operator-errors";
import { setInnerComments } from "./comments";
import { cloneIdentifier } from "./node";
Expand Down

0 comments on commit fa2c7c7

Please sign in to comment.