Skip to content

Commit

Permalink
chore: refine visitor typings (#14862)
Browse files Browse the repository at this point in the history
  • Loading branch information
JLHwung committed Aug 21, 2022
1 parent 3445cb3 commit aa07f5f
Show file tree
Hide file tree
Showing 9 changed files with 47 additions and 35 deletions.
Expand Up @@ -107,7 +107,6 @@ interface PrivateNameVisitorState {
function privateNameVisitorFactory<S>(
visitor: Visitor<PrivateNameVisitorState & S>,
) {
// @ts-expect-error Fixme: TS complains _exploded: boolean does not satisfy visitor functions
const privateNameVisitor: Visitor<PrivateNameVisitorState & S> = {
...visitor,

Expand Down
17 changes: 5 additions & 12 deletions packages/babel-traverse/src/index.ts
Expand Up @@ -20,18 +20,11 @@ export type { HubInterface } from "./hub";

export { visitors };

// fixme: The TraverseOptions should have been { scope ... } & Visitor<S>
// however TS does not support excluding certain string literals from general string
// type. If we change here to { scope ... } & Visitor<S>, TS will throw
// noScope: boolean because it matched `noScope` to the [k in string]: VisitNode<> catch-all
// in Visitor
export type TraverseOptions<S = t.Node> =
| {
scope?: Scope;
noScope?: boolean;
denylist?: string[];
}
| Visitor<S>;
export type TraverseOptions<S = t.Node> = {
scope?: Scope;
noScope?: boolean;
denylist?: string[];
} & Visitor<S>;

function traverse<S>(
parent: t.Node,
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-traverse/src/path/index.ts
Expand Up @@ -62,7 +62,7 @@ class NodePath<T extends t.Node = t.Node> {
listKey: string | null = null;
key: string | number | null = null;
node: T = null;
type: string | null = null;
type: T["type"] | null = null;

static get({
hub,
Expand Down
4 changes: 1 addition & 3 deletions packages/babel-traverse/src/path/lib/virtual-types.ts
Expand Up @@ -21,9 +21,7 @@ export interface VirtualTypeAliases {
Var: t.VariableDeclaration;
}

type NodeTypes = t.Node["type"] | t.Comment["type"] | keyof t.Aliases;

type VirtualTypeMapping = readonly NodeTypes[] | null;
type VirtualTypeMapping = readonly (t.Node["type"] | keyof t.Aliases)[] | null;

export const ReferencedIdentifier: VirtualTypeMapping = [
"Identifier",
Expand Down
5 changes: 4 additions & 1 deletion packages/babel-traverse/src/types.ts
Expand Up @@ -11,7 +11,10 @@ export type Visitor<S = {}> = VisitNodeObject<S, t.Node> & {
} & {
[K in keyof InternalVisitorFlags]?: InternalVisitorFlags[K];
} & {
[k: string]: VisitNode<S, t.Node>;
// Babel supports `NodeTypesWithoutComment | NodeTypesWithoutComment | ... ` but it is
// too complex for TS. So we type it as a general visitor only if the key contains `|`
// this is good enough for non-visitor traverse options e.g. `noScope`
[k: `${string}|${string}`]: VisitNode<S, t.Node>;
};

/** @internal */
Expand Down
33 changes: 24 additions & 9 deletions packages/babel-traverse/src/visitors.ts
Expand Up @@ -28,7 +28,7 @@ export function explode(visitor: Visitor) {
visitor._exploded = true;

// normalise pipes
for (const nodeType of Object.keys(visitor)) {
for (const nodeType of Object.keys(visitor) as (keyof Visitor)[]) {
if (shouldIgnoreKey(nodeType)) continue;

const parts: Array<string> = nodeType.split("|");
Expand All @@ -38,6 +38,7 @@ export function explode(visitor: Visitor) {
delete visitor[nodeType];

for (const part of parts) {
// @ts-expect-error part will be verified by `verify` later
visitor[part] = fns;
}
}
Expand All @@ -47,6 +48,7 @@ export function explode(visitor: Visitor) {

// make sure there's no __esModule type since this is because we're using loose mode
// and it sets __esModule to be enumerable on all modules :(
// @ts-expect-error ESModule interop
delete visitor.__esModule;

// ensure visitors are objects
Expand Down Expand Up @@ -88,12 +90,12 @@ export function explode(visitor: Visitor) {
}

// add aliases
for (const nodeType of Object.keys(visitor)) {
for (const nodeType of Object.keys(visitor) as (keyof Visitor)[]) {
if (shouldIgnoreKey(nodeType)) continue;

const fns = visitor[nodeType];

let aliases: Array<string> | undefined = FLIPPED_ALIAS_KEYS[nodeType];
let aliases = FLIPPED_ALIAS_KEYS[nodeType];

const deprecatedKey = DEPRECATED_KEYS[nodeType];
if (deprecatedKey) {
Expand All @@ -113,6 +115,7 @@ export function explode(visitor: Visitor) {
if (existing) {
mergePair(existing, fns);
} else {
// @ts-expect-error Expression produces a union type that is too complex to represent.
visitor[alias] = { ...fns };
}
}
Expand Down Expand Up @@ -140,7 +143,7 @@ export function verify(visitor: Visitor) {
);
}

for (const nodeType of Object.keys(visitor)) {
for (const nodeType of Object.keys(visitor) as (keyof Visitor)[]) {
if (nodeType === "enter" || nodeType === "exit") {
validateVisitorMethods(nodeType, visitor[nodeType]);
}
Expand Down Expand Up @@ -208,15 +211,16 @@ export function merge(

explode(visitor);

for (const type of Object.keys(visitor)) {
for (const type of Object.keys(visitor) as (keyof Visitor)[]) {
let visitorType = visitor[type];

// if we have state or wrapper then overload the callbacks to take it
if (state || wrapper) {
visitorType = wrapWithStateOrWrapper(visitorType, state, wrapper);
}

const nodeVisitor = (rootVisitor[type] = rootVisitor[type] || {});
// @ts-expect-error: Expression produces a union type that is too complex to represent.
const nodeVisitor = (rootVisitor[type] ||= {});
mergePair(nodeVisitor, visitorType);
}
}
Expand All @@ -231,7 +235,7 @@ function wrapWithStateOrWrapper<State>(
) {
const newVisitor: Visitor = {};

for (const key of Object.keys(oldVisitor)) {
for (const key of Object.keys(oldVisitor) as (keyof Visitor<State>)[]) {
let fns = oldVisitor[key];

// not an enter/exit array of callbacks
Expand Down Expand Up @@ -260,18 +264,20 @@ function wrapWithStateOrWrapper<State>(
return newFn;
});

// @ts-expect-error: Expression produces a union type that is too complex to represent.
newVisitor[key] = fns;
}

return newVisitor;
}

function ensureEntranceObjects(obj: Visitor) {
for (const key of Object.keys(obj)) {
for (const key of Object.keys(obj) as (keyof Visitor)[]) {
if (shouldIgnoreKey(key)) continue;

const fns = obj[key];
if (typeof fns === "function") {
// @ts-expect-error: Expression produces a union type that is too complex to represent.
obj[key] = { enter: fns };
}
}
Expand All @@ -294,7 +300,16 @@ function wrapCheck(nodeType: VIRTUAL_TYPES, fn: Function) {
return newFn;
}

function shouldIgnoreKey(key: string) {
function shouldIgnoreKey(
key: string,
): key is
| "enter"
| "exit"
| "shouldSkip"
| "denylist"
| "noScope"
| "skipKeys"
| "blacklist" {
// internal/hidden key
if (key[0] === "_") return true;

Expand Down
2 changes: 1 addition & 1 deletion packages/babel-types/src/definitions/placeholders.ts
Expand Up @@ -9,7 +9,7 @@ export const PLACEHOLDERS = [
"BlockStatement",
"ClassBody",
"Pattern",
];
] as const;

export const PLACEHOLDERS_ALIAS: Record<string, string[]> = {
Declaration: ["Statement"],
Expand Down
17 changes: 10 additions & 7 deletions packages/babel-types/src/definitions/utils.ts
Expand Up @@ -3,11 +3,12 @@ import { validateField, validateChild } from "../validators/validate";
import type * as t from "..";

export const VISITOR_KEYS: Record<string, string[]> = {};
export const ALIAS_KEYS: Record<string, string[]> = {};
export const FLIPPED_ALIAS_KEYS: Record<string, string[]> = {};
export const ALIAS_KEYS: Partial<Record<NodeTypesWithoutComment, string[]>> =
{};
export const FLIPPED_ALIAS_KEYS: Record<string, NodeTypesWithoutComment[]> = {};
export const NODE_FIELDS: Record<string, FieldDefinitions> = {};
export const BUILDER_KEYS: Record<string, string[]> = {};
export const DEPRECATED_KEYS: Record<string, string> = {};
export const DEPRECATED_KEYS: Record<string, NodeTypesWithoutComment> = {};
export const NODE_PARENT_VALIDATIONS: Record<string, Validator> = {};

function getType(val: any) {
Expand All @@ -20,7 +21,9 @@ function getType(val: any) {
}
}

type NodeTypes = t.Node["type"] | t.Comment["type"] | keyof t.Aliases;
type NodeTypesWithoutComment = t.Node["type"] | keyof t.Aliases;

type NodeTypes = NodeTypesWithoutComment | t.Comment["type"];

type PrimitiveTypes = ReturnType<typeof getType>;

Expand Down Expand Up @@ -330,7 +333,7 @@ export default function defineType(type: string, opts: DefineTypeOpts = {}) {
}

if (opts.deprecatedAlias) {
DEPRECATED_KEYS[opts.deprecatedAlias] = type;
DEPRECATED_KEYS[opts.deprecatedAlias] = type as NodeTypesWithoutComment;
}

// ensure all field keys are represented in `fields`
Expand Down Expand Up @@ -360,10 +363,10 @@ export default function defineType(type: string, opts: DefineTypeOpts = {}) {
VISITOR_KEYS[type] = opts.visitor = visitor;
BUILDER_KEYS[type] = opts.builder = builder;
NODE_FIELDS[type] = opts.fields = fields;
ALIAS_KEYS[type] = opts.aliases = aliases;
ALIAS_KEYS[type as NodeTypesWithoutComment] = opts.aliases = aliases;
aliases.forEach(alias => {
FLIPPED_ALIAS_KEYS[alias] = FLIPPED_ALIAS_KEYS[alias] || [];
FLIPPED_ALIAS_KEYS[alias].push(type);
FLIPPED_ALIAS_KEYS[alias].push(type as NodeTypesWithoutComment);
});

if (opts.validate) {
Expand Down
1 change: 1 addition & 0 deletions packages/babel-types/src/validators/isType.ts
Expand Up @@ -19,6 +19,7 @@ export default function isType(nodeType: string, targetType: string): boolean {

// This is a fast-path. If the test above failed, but an alias key is found, then the
// targetType was a primary node type, so there's no need to check the aliases.
// @ts-expect-error targetType may not index ALIAS_KEYS
if (ALIAS_KEYS[targetType]) return false;

const aliases: Array<string> | undefined = FLIPPED_ALIAS_KEYS[targetType];
Expand Down

0 comments on commit aa07f5f

Please sign in to comment.