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

Make ParseError Much Simpler now that we can use TypeScript #14796

Merged
merged 8 commits into from Jul 25, 2022
144 changes: 83 additions & 61 deletions packages/babel-parser/src/parse-error.ts
Expand Up @@ -2,9 +2,10 @@ import { Position } from "./util/location";
import type { NodeBase } from "./types";
import {
instantiate,
type ParseErrorCode,
ParseErrorCodes,
ParseErrorCode,
type ParseErrorCredentials,
type ToMessage,
type SyntaxPlugin,
} from "./parse-error/credentials";
import type { Undone } from "../src/parser/node";

Expand All @@ -23,7 +24,7 @@ interface ParseErrorSpecification<ErrorDetails> {
// as readonly, so let's just not worry about it for now.
code: ParseErrorCode;
reasonCode: string;
syntaxPlugin?: string;
syntaxPlugin?: SyntaxPlugin;
missingPlugin?: string | string[];
loc: Position;
details: ErrorDetails;
Expand Down Expand Up @@ -56,7 +57,7 @@ function toParseErrorConstructor<ErrorDetails>({
};

return function constructor({ loc, details }: ConstructorArgument) {
return instantiate<SyntaxError, ParseError<ErrorDetails>>(
return instantiate(
SyntaxError,
{ ...properties, loc },
{
Expand All @@ -66,14 +67,11 @@ function toParseErrorConstructor<ErrorDetails>({
details?: ErrorDetails;
} = {},
) {
const loc = overrides.loc || {};
const loc = (overrides.loc || {}) as Partial<Position>;
return constructor({
loc: new Position(
// @ts-expect-error line has been guarded
"line" in loc ? loc.line : this.loc.line,
// @ts-expect-error column has been guarded
"column" in loc ? loc.column : this.loc.column,
// @ts-expect-error index has been guarded
"index" in loc ? loc.index : this.loc.index,
),
details: { ...this.details, ...overrides.details },
Expand All @@ -96,82 +94,106 @@ function toParseErrorConstructor<ErrorDetails>({
enumerable: true,
},
},
);
) as ParseError<ErrorDetails>;
};
}

// 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
// `ParseErrorConstructor<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 during
// its actual runtime operation, but pretending to return the
// `ParseErrorConstructor<ErrorDetails>` that we won't actually have until later
// to the type system, avoiding the need to do so with $ObjMap (which doesn't
// work) in `ParseErrorEnum`. This hack won't be necessary when we switch to
// Typescript.
export function toParseErrorCredentials(
message: string,
credentials?: { code?: ParseErrorCode; reasonCode?: string },
): ParseErrorConstructor<{}>;

export function toParseErrorCredentials<ErrorDetails>(
toMessage: (details: ErrorDetails) => string,
credentials?: { code?: ParseErrorCode; reasonCode?: string },
): ParseErrorConstructor<ErrorDetails>;

export function toParseErrorCredentials(
toMessageOrMessage: string | ((details: unknown) => string),
credentials?: any,
) {
return {
toMessage:
typeof toMessageOrMessage === "string"
? () => toMessageOrMessage
: toMessageOrMessage,
...credentials,
};
}
type ParseErrorTemplate =
| string
| ToMessage<any>
| { message: string | ToMessage<any> };

// This is the templated form.
export function ParseErrorEnum(a: TemplateStringsArray): typeof ParseErrorEnum;
type ParseErrorTemplates = { [reasonCode: string]: ParseErrorTemplate };

// This is the templated form of `ParseErrorEnum`.
//
// Note: We could factor out the return type calculation into something like
// `ParseErrorConstructor<T extends ParseErrorTemplates>`, and then we could
// reuse it in the non-templated form of `ParseErrorEnum`, but TypeScript
// doesn't seem to drill down that far when showing you the computed type of
// an object in an editor, so we'll leave it inlined for now.
export function ParseErrorEnum(a: TemplateStringsArray): <
T extends ParseErrorTemplates,
>(
parseErrorTemplates: T,
) => {
[K in keyof T]: ParseErrorConstructor<
T[K] extends { message: string | ToMessage<any> }
? T[K]["message"] extends ToMessage<any>
? Parameters<T[K]["message"]>[0]
: {}
: T[K] extends ToMessage<any>
? Parameters<T[K]>[0]
: {}
>;
};

export function ParseErrorEnum<
T extends (a: typeof toParseErrorCredentials) => unknown,
>(toParseErrorCredentials: T, syntaxPlugin?: string): ReturnType<T>;
export function ParseErrorEnum<T extends ParseErrorTemplates>(
parseErrorTemplates: T,
syntaxPlugin?: SyntaxPlugin,
): {
[K in keyof T]: ParseErrorConstructor<
T[K] extends { message: string | ToMessage<any> }
? T[K]["message"] extends ToMessage<any>
? Parameters<T[K]["message"]>[0]
: {}
: T[K] extends ToMessage<any>
? Parameters<T[K]>[0]
: {}
>;
};

// You call `ParseErrorEnum` with a mapping from `ReasonCode`'s to either error
// messages, or `toMessage` functions that define additional necessary `details`
// needed by the `ParseError`:
// You call `ParseErrorEnum` with a mapping from `ReasonCode`'s to either:
//
// ParseErrorEnum`optionalSyntaxPlugin` (_ => ({
// ErrorWithStaticMessage: _("message"),
// ErrorWithDynamicMessage: _<{ type: string }>(({ type }) => `${type}`),
// 1. a static error message,
// 2. `toMessage` functions that define additional necessary `details` needed by
// the `ParseError`, or
// 3. Objects that contain a `message` of one of the above and overridden `code`
// and/or `reasonCode`:
//
// ParseErrorEnum `optionalSyntaxPlugin` ({
// ErrorWithStaticMessage: "message",
// ErrorWithDynamicMessage: ({ type } : { type: string }) => `${type}`),
// ErrorWithOverriddenCodeAndOrReasonCode: {
// message: ({ type }: { type: string }) => `${type}`),
// code: ParseErrorCode.SourceTypeModuleError,
// ...(BABEL_8_BREAKING ? { } : { reasonCode: "CustomErrorReasonCode" })
// }
// });
//
export function ParseErrorEnum(argument: any, syntaxPlugin?: string) {
export function ParseErrorEnum(
argument: TemplateStringsArray | ParseErrorTemplates,
syntaxPlugin?: 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 (Array.isArray(argument)) {
return (toParseErrorCredentialsMap: any) =>
ParseErrorEnum(toParseErrorCredentialsMap, argument[0]);
return (parseErrorTemplates: ParseErrorTemplates) =>
ParseErrorEnum(parseErrorTemplates, argument[0]);
}

const partialCredentials = argument(toParseErrorCredentials);
const ParseErrorConstructors = {} as Record<
string,
ParseErrorConstructor<unknown>
>;

for (const reasonCode of Object.keys(partialCredentials)) {
for (const reasonCode of Object.keys(argument)) {
const template = (argument as ParseErrorTemplates)[reasonCode];
const { message, ...rest } =
typeof template === "string"
? { message: () => template }
: typeof template === "function"
? { message: template }
: template;
const toMessage = typeof message === "string" ? () => message : message;

ParseErrorConstructors[reasonCode] = toParseErrorConstructor({
code: ParseErrorCodes.SyntaxError,
code: ParseErrorCode.SyntaxError,
reasonCode,
toMessage,
...(syntaxPlugin ? { syntaxPlugin } : {}),
...partialCredentials[reasonCode],
...rest,
});
}

Expand Down
15 changes: 6 additions & 9 deletions packages/babel-parser/src/parse-error/credentials.ts
@@ -1,10 +1,7 @@
export const ParseErrorCodes = Object.freeze({
SyntaxError: "BABEL_PARSER_SYNTAX_ERROR",
SourceTypeModuleError: "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED",
});

export type ParseErrorCode =
typeof ParseErrorCodes[keyof typeof ParseErrorCodes];
export const enum ParseErrorCode {
SyntaxError = "BABEL_PARSER_SYNTAX_ERROR",
SourceTypeModuleError = "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED",
}

export type SyntaxPlugin =
| "flow"
Expand Down Expand Up @@ -40,7 +37,7 @@ const reflect = (keys: string[], last = keys.length - 1) => ({
},
});

const instantiate = <T, U extends T>(
const instantiate = <T>(
constructor: new () => T,
properties: any,
descriptors: any,
Expand All @@ -62,7 +59,7 @@ const instantiate = <T, U extends T>(
configurable: true,
...descriptor,
}),
Object.assign(new constructor() as U, properties),
Object.assign(new constructor(), properties),
);

export { instantiate };
22 changes: 11 additions & 11 deletions packages/babel-parser/src/parse-error/module-errors.ts
@@ -1,12 +1,12 @@
import { ParseErrorCodes, toParseErrorCredentials } from "../parse-error";
import { ParseErrorCode } from "../parse-error";

export default (_: typeof toParseErrorCredentials) => ({
ImportMetaOutsideModule: _(
`import.meta may appear only with 'sourceType: "module"'`,
{ code: ParseErrorCodes.SourceTypeModuleError },
),
ImportOutsideModule: _(
`'import' and 'export' may appear only with 'sourceType: "module"'`,
{ code: ParseErrorCodes.SourceTypeModuleError },
),
});
export default {
ImportMetaOutsideModule: {
message: `import.meta may appear only with 'sourceType: "module"'`,
code: ParseErrorCode.SourceTypeModuleError,
},
ImportOutsideModule: {
message: `'import' and 'export' may appear only with 'sourceType: "module"'`,
code: ParseErrorCode.SourceTypeModuleError,
},
};
68 changes: 28 additions & 40 deletions packages/babel-parser/src/parse-error/pipeline-operator-errors.ts
@@ -1,63 +1,51 @@
import { toParseErrorCredentials } from "../parse-error";
import toNodeDescription from "./to-node-description";

const UnparenthesizedPipeBodyDescriptionsList = [
export const UnparenthesizedPipeBodyDescriptions = new Set([
"ArrowFunctionExpression",
"AssignmentExpression",
"ConditionalExpression",
"YieldExpression",
] as const;
export const UnparenthesizedPipeBodyDescriptions = new Set(
UnparenthesizedPipeBodyDescriptionsList,
);
] as const);

export default (_: typeof toParseErrorCredentials) => ({
type GetSetMemberType<T extends Set<any>> = T extends Set<infer M>
? M
: unknown;

type UnparanthesizedPipeBodyTypes = GetSetMemberType<
typeof UnparenthesizedPipeBodyDescriptions
>;

export default {
// This error is only used by the smart-mix proposal
PipeBodyIsTighter: _(
PipeBodyIsTighter:
"Unexpected yield after pipeline body; any yield expression acting as Hack-style pipe body must be parenthesized due to its loose operator precedence.",
),
PipeTopicRequiresHackPipes: _(
PipeTopicRequiresHackPipes:
'Topic reference is used, but the pipelineOperator plugin was not passed a "proposal": "hack" or "smart" option.',
),
PipeTopicUnbound: _(
PipeTopicUnbound:
"Topic reference is unbound; it must be inside a pipe body.",
),
PipeTopicUnconfiguredToken: _<{ token: string }>(
({ token }) =>
`Invalid topic token ${token}. In order to use ${token} as a topic reference, the pipelineOperator plugin must be configured with { "proposal": "hack", "topicToken": "${token}" }.`,
),
PipeTopicUnused: _(
PipeTopicUnconfiguredToken: ({ token }: { token: string }) =>
`Invalid topic token ${token}. In order to use ${token} as a topic reference, the pipelineOperator plugin must be configured with { "proposal": "hack", "topicToken": "${token}" }.`,
PipeTopicUnused:
"Hack-style pipe body does not contain a topic reference; Hack-style pipes must use topic at least once.",
),
PipeUnparenthesizedBody: _<{
type: typeof UnparenthesizedPipeBodyDescriptionsList[number];
}>(
({ type }) =>
`Hack-style pipe body cannot be an unparenthesized ${toNodeDescription({
type,
})}; please wrap it in parentheses.`,
),
PipeUnparenthesizedBody: ({ type }: { type: UnparanthesizedPipeBodyTypes }) =>
`Hack-style pipe body cannot be an unparenthesized ${toNodeDescription({
type,
})}; please wrap it in parentheses.`,

// Messages whose codes start with “Pipeline” or “PrimaryTopic”
// are retained for backwards compatibility
// with the deprecated smart-mix pipe operator proposal plugin.
// They are subject to removal in a future major version.
PipelineBodyNoArrow: _(
PipelineBodyNoArrow:
'Unexpected arrow "=>" after pipeline body; arrow function in pipeline body must be parenthesized.',
),
PipelineBodySequenceExpression: _(
PipelineBodySequenceExpression:
"Pipeline body may not be a comma-separated sequence expression.",
),
PipelineHeadSequenceExpression: _(
PipelineHeadSequenceExpression:
"Pipeline head should not be a comma-separated sequence expression.",
),
PipelineTopicUnused: _(
PipelineTopicUnused:
"Pipeline is in topic style but does not use topic reference.",
),
PrimaryTopicNotAllowed: _(
PrimaryTopicNotAllowed:
"Topic reference was used in a lexical context without topic binding.",
),
PrimaryTopicRequiresSmartPipeline: _(
PrimaryTopicRequiresSmartPipeline:
'Topic reference is used, but the pipelineOperator plugin was not passed a "proposal": "hack" or "smart" option.',
),
});
};