Skip to content

Commit

Permalink
Make ParseError much simpler now that we can use TypeScript (#14796)
Browse files Browse the repository at this point in the history
  • Loading branch information
tolmasky committed Jul 25, 2022
1 parent 02df091 commit 24d5fce
Show file tree
Hide file tree
Showing 10 changed files with 707 additions and 883 deletions.
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.',
),
});
};

0 comments on commit 24d5fce

Please sign in to comment.