Skip to content

Commit

Permalink
Add typings to create-class-features-plugin helper (#13570)
Browse files Browse the repository at this point in the history
Co-authored-by: Nicolò Ribaudo <nicolo.ribaudo@gmail.com>
  • Loading branch information
colinaaa and nicolo-ribaudo committed Aug 2, 2021
1 parent b3ab476 commit 1960f23
Show file tree
Hide file tree
Showing 10 changed files with 576 additions and 328 deletions.
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { types as t, template } from "@babel/core";
import type { File } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import ReplaceSupers from "@babel/helper-replace-supers";
import nameFunction from "@babel/helper-function-name";

export function hasOwnDecorators(node) {
type Decorable = Extract<t.Node, { decorators?: t.Decorator[] | null }>;

export function hasOwnDecorators(node: t.Node) {
// @ts-expect-error(flow->ts) TODO: maybe we could add t.isDecoratable to make ts happy
return !!(node.decorators && node.decorators.length);
}

export function hasDecorators(node) {
export function hasDecorators(node: t.Class) {
return hasOwnDecorators(node) || node.body.body.some(hasOwnDecorators);
}

function prop(key, value) {
function prop(key: string, value?: t.Expression) {
if (!value) return null;
return t.objectProperty(t.identifier(key), value);
}

function method(key, body) {
function method(key: string, body: t.Statement[]) {
return t.objectMethod(
"method",
t.identifier(key),
Expand All @@ -24,8 +29,8 @@ function method(key, body) {
);
}

function takeDecorators(node) {
let result;
function takeDecorators(node: Decorable) {
let result: t.ArrayExpression | undefined;
if (node.decorators && node.decorators.length > 0) {
result = t.arrayExpression(
node.decorators.map(decorator => decorator.expression),
Expand All @@ -47,7 +52,12 @@ function getKey(node) {

// NOTE: This function can be easily bound as .bind(file, classRef, superRef)
// to make it easier to use it in a loop.
function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
function extractElementDescriptor(
this: File,
classRef: t.Identifier,
superRef: t.Identifier,
path: ClassElementPath,
) {
const { node, scope } = path;
const isMethod = path.isClassMethod();

Expand All @@ -68,17 +78,17 @@ function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
}).replace();

const properties: t.ObjectExpression["properties"] = [
prop("kind", t.stringLiteral(isMethod ? node.kind : "field")),
prop("decorators", takeDecorators(node)),
prop("kind", t.stringLiteral(t.isClassMethod(node) ? node.kind : "field")),
prop("decorators", takeDecorators(node as Decorable)),
prop("static", node.static && t.booleanLiteral(true)),
prop("key", getKey(node)),
].filter(Boolean);

if (isMethod) {
if (t.isClassMethod(node)) {
const id = node.computed ? null : node.key;
t.toExpression(node);
properties.push(prop("value", nameFunction({ node, id, scope }) || node));
} else if (node.value) {
} else if (t.isClassProperty(node) && node.value) {
properties.push(
method("value", template.statements.ast`return ${node.value}`),
);
Expand All @@ -91,7 +101,7 @@ function extractElementDescriptor(/* this: File, */ classRef, superRef, path) {
return t.objectExpression(properties);
}

function addDecorateHelper(file) {
function addDecorateHelper(file: File) {
try {
return file.addHelper("decorate");
} catch (err) {
Expand All @@ -105,7 +115,15 @@ function addDecorateHelper(file) {
}
}

export function buildDecoratedClass(ref, path, elements, file) {
type ClassElement = t.Class["body"]["body"][number];
type ClassElementPath = NodePath<ClassElement>;

export function buildDecoratedClass(
ref: t.Identifier,
path: NodePath<t.Class>,
elements: ClassElementPath[],
file: File,
) {
const { node, scope } = path;
const initializeId = scope.generateUidIdentifier("initialize");
const isDeclaration = node.id && path.isDeclaration();
Expand All @@ -115,7 +133,7 @@ export function buildDecoratedClass(ref, path, elements, file) {
node.type = "ClassDeclaration";
if (!node.id) node.id = t.cloneNode(ref);

let superId;
let superId: t.Identifier;
if (superClass) {
superId = scope.generateUidIdentifierBasedOnNode(node.superClass, "super");
node.superClass = superId;
Expand All @@ -124,7 +142,7 @@ export function buildDecoratedClass(ref, path, elements, file) {
const classDecorators = takeDecorators(node);
const definitions = t.arrayExpression(
elements
// Ignore TypeScript's abstract methods (see #10514)
// @ts-expect-error Ignore TypeScript's abstract methods (see #10514)
.filter(element => !element.node.abstract)
.map(extractElementDescriptor.bind(file, node.id, superId)),
);
Expand Down Expand Up @@ -155,9 +173,9 @@ export function buildDecoratedClass(ref, path, elements, file) {

return {
instanceNodes: [template.statement.ast`${t.cloneNode(initializeId)}(this)`],
wrapClass(path) {
wrapClass(path: NodePath<t.Class>) {
path.replaceWith(replacement);
return path.get(classPathDesc);
return path.get(classPathDesc) as NodePath;
},
};
}
24 changes: 14 additions & 10 deletions packages/babel-helper-create-class-features-plugin/src/features.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { File } from "@babel/core";
import type { NodePath } from "@babel/traverse";
import { hasOwnDecorators } from "./decorators";

export const FEATURES = Object.freeze({
Expand Down Expand Up @@ -36,7 +38,7 @@ const looseKey = "@babel/plugin-class-features/looseKey";
const looseLowPriorityKey =
"@babel/plugin-class-features/looseLowPriorityKey/#__internal__@babel/preset-env__please-overwrite-loose-instead-of-throwing";

export function enableFeature(file, feature, loose) {
export function enableFeature(file: File, feature: number, loose: boolean) {
// We can't blindly enable the feature because, if it was already set,
// "loose" can't be changed, so that
// @babel/plugin-class-properties { loose: true }
Expand All @@ -46,12 +48,14 @@ export function enableFeature(file, feature, loose) {
if (!hasFeature(file, feature) || canIgnoreLoose(file, feature)) {
file.set(featuresKey, file.get(featuresKey) | feature);
if (
// @ts-expect-error
loose ===
"#__internal__@babel/preset-env__prefer-true-but-false-is-ok-if-it-prevents-an-error"
) {
setLoose(file, feature, true);
file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) | feature);
} else if (
// @ts-expect-error
loose ===
"#__internal__@babel/preset-env__prefer-false-but-true-is-ok-if-it-prevents-an-error"
) {
Expand All @@ -62,8 +66,8 @@ export function enableFeature(file, feature, loose) {
}
}

let resolvedLoose: void | true | false;
let higherPriorityPluginName: void | string;
let resolvedLoose: boolean | undefined;
let higherPriorityPluginName: string | undefined;

for (const [mask, name] of featuresSameLoose) {
if (!hasFeature(file, mask)) continue;
Expand Down Expand Up @@ -103,26 +107,26 @@ export function enableFeature(file, feature, loose) {
}
}

function hasFeature(file, feature) {
function hasFeature(file: File, feature: number) {
return !!(file.get(featuresKey) & feature);
}

export function isLoose(file, feature) {
export function isLoose(file: File, feature: number) {
return !!(file.get(looseKey) & feature);
}

function setLoose(file, feature, loose) {
function setLoose(file: File, feature: number, loose: boolean) {
if (loose) file.set(looseKey, file.get(looseKey) | feature);
else file.set(looseKey, file.get(looseKey) & ~feature);

file.set(looseLowPriorityKey, file.get(looseLowPriorityKey) & ~feature);
}

function canIgnoreLoose(file, feature) {
function canIgnoreLoose(file: File, feature: number) {
return !!(file.get(looseLowPriorityKey) & feature);
}

export function verifyUsedFeatures(path, file) {
export function verifyUsedFeatures(path: NodePath, file: File) {
if (hasOwnDecorators(path.node)) {
if (!hasFeature(file, FEATURES.decorators)) {
throw path.buildCodeFrameError(
Expand All @@ -145,8 +149,8 @@ export function verifyUsedFeatures(path, file) {
}
}

// NOTE: path.isPrivateMethod() it isn't supported in <7.2.0
if (path.isPrivateMethod?.()) {
// NOTE: path.isClassPrivateMethod() it isn't supported in <7.2.0
if (path.isClassPrivateMethod?.()) {
if (!hasFeature(file, FEATURES.privateMethods)) {
throw path.buildCodeFrameError("Class private methods are not enabled.");
}
Expand Down
Loading

0 comments on commit 1960f23

Please sign in to comment.