Skip to content

Commit

Permalink
Implement TypeScript namespace support
Browse files Browse the repository at this point in the history
This also fixes enum not adding the respective declaration to the scope
during the typescript visitation.
  • Loading branch information
Wolvereness committed Apr 11, 2019
1 parent 165ef29 commit 62f2195
Show file tree
Hide file tree
Showing 45 changed files with 455 additions and 9 deletions.
4 changes: 3 additions & 1 deletion packages/babel-plugin-transform-typescript/src/enum.js
Expand Up @@ -24,7 +24,9 @@ export default function transpileEnum(path, t) {
path.remove();
} else {
const isGlobal = t.isProgram(path.parent); // && !path.parent.body.some(t.isModuleDeclaration);
path.replaceWith(makeVar(node.id, t, isGlobal ? "var" : "let"));
path.scope.registerDeclaration(
path.replaceWith(makeVar(node.id, t, isGlobal ? "var" : "let"))[0],
);
}
break;
}
Expand Down
6 changes: 2 additions & 4 deletions packages/babel-plugin-transform-typescript/src/index.js
Expand Up @@ -3,6 +3,7 @@ import syntaxTypeScript from "@babel/plugin-syntax-typescript";
import { types as t } from "@babel/core";

import transpileEnum from "./enum";
import transpileNamespace from "./namespace";

function isInType(path) {
switch (path.parent.type) {
Expand Down Expand Up @@ -239,10 +240,7 @@ export default declare((api, { jsxPragma = "React" }) => {
},

TSModuleDeclaration(path) {
if (!path.node.declare && path.node.id.type !== "StringLiteral") {
throw path.buildCodeFrameError("Namespaces are not supported.");
}
path.remove();
transpileNamespace(path, t);
},

TSInterfaceDeclaration(path) {
Expand Down
199 changes: 199 additions & 0 deletions packages/babel-plugin-transform-typescript/src/namespace.js
@@ -0,0 +1,199 @@
const SHARED_NAMESPACE_ERRORS = {
TSEnumDeclaration: "An enum may not share the name of its parent namespace.",
FunctionDeclaration:
"A function may not share the name of its parent namespace.",
ClassDeclaration: "A class may not share the name of its parent namespace.",
VariableDeclaration:
"A variable may not share the name of its parent namespace.",
TSModuleDeclaration:
"A namespace may not share the name of its parent namespace.",
};

export default function transpileNamespace(path, t) {
if (path.node.declare || path.node.id.type === "StringLiteral") {
path.remove();
return;
}

const name = path.node.id.name;
const value = handleNested(path, t, JSON.parse(JSON.stringify(path.node)));
if (path.parent.type === "ExportNamedDeclaration") {
path.parentPath.insertAfter(value);
path.replaceWith(getDeclaration(t, name));
path.scope.registerDeclaration(path.parentPath);
} else if (path.scope.hasOwnBinding(name)) {
path.replaceWith(value);
} else {
path.scope.registerDeclaration(
path.replaceWithMultiple([getDeclaration(t, name), value])[0],
);
}
}

function getDeclaration(t, name) {
return t.variableDeclaration("let", [
t.variableDeclarator(t.identifier(name)),
]);
}

function handleNested(path, t, node, parentExportName) {
const names = [];
const name = node.id.name;
const namespaceTopLevel = node.body.body;
for (let i = 0; i < namespaceTopLevel.length; i++) {
const subNode = namespaceTopLevel[i];
switch (subNode.type) {
case "TSModuleDeclaration": {
const moduleName = subNode.id.name;
if (moduleName === name) {
throw path.hub.file.buildCodeFrameError(
subNode,
SHARED_NAMESPACE_ERRORS.TSModuleDeclaration,
);
}
if (names[moduleName]) {
namespaceTopLevel[i] = handleNested(path, t, subNode);
} else {
names[moduleName] = true;
namespaceTopLevel.splice(
i++,
1,
getDeclaration(t, moduleName),
handleNested(path, t, subNode),
);
}
continue;
}
case "TSEnumDeclaration":
case "FunctionDeclaration":
case "ClassDeclaration": {
const itemName = subNode.id.name;
if (itemName === name) {
throw path.hub.file.buildCodeFrameError(
subNode,
SHARED_NAMESPACE_ERRORS[subNode.type],
);
}
names[itemName] = true;
continue;
}
case "VariableDeclaration":
for (const variable of subNode.declarations) {
const variableName = variable.id.name;
if (variableName === name) {
throw path.hub.file.buildCodeFrameError(
variable,
SHARED_NAMESPACE_ERRORS.VariableDeclaration,
);
}
names[variableName] = true;
}
continue;
default:
continue;
case "ExportNamedDeclaration":
}
switch (subNode.declaration.type) {
case "TSEnumDeclaration":
case "FunctionDeclaration":
case "ClassDeclaration": {
const itemName = subNode.declaration.id.name;
if (itemName === name) {
throw path.hub.file.buildCodeFrameError(
subNode.declaration,
SHARED_NAMESPACE_ERRORS[subNode.declaration.type],
);
}
names[itemName] = true;
namespaceTopLevel.splice(
i++,
1,
subNode.declaration,
t.expressionStatement(
t.assignmentExpression(
"=",
t.memberExpression(t.identifier(name), t.identifier(itemName)),
t.identifier(itemName),
),
),
);
break;
}
case "VariableDeclaration":
if (subNode.declaration.kind !== "const") {
throw path.hub.file.buildCodeFrameError(
subNode.declaration,
"Namespaces exporting non-const are unsupported.",
);
}
for (const variable of subNode.declaration.declarations) {
const variableName = variable.id.name;
if (variableName === name) {
throw path.hub.file.buildCodeFrameError(
variable,
SHARED_NAMESPACE_ERRORS.VariableDeclaration,
);
}
variable.init = t.assignmentExpression(
"=",
t.memberExpression(t.identifier(name), t.identifier(variableName)),
variable.init,
);
}
namespaceTopLevel[i] = subNode.declaration;
break;
case "TSModuleDeclaration": {
const moduleName = subNode.declaration.id.name;
if (moduleName === name) {
throw path.hub.file.buildCodeFrameError(
subNode.declaration,
SHARED_NAMESPACE_ERRORS.TSModuleDeclaration,
);
}
if (names[moduleName]) {
namespaceTopLevel[i] = handleNested(
path,
t,
subNode.declaration,
name,
);
} else {
names[moduleName] = true;
namespaceTopLevel.splice(
i++,
1,
getDeclaration(t, moduleName),
handleNested(path, t, subNode.declaration, name),
);
}
}
}
}

const derivedParameter = t.logicalExpression(
"||",
t.identifier(name),
t.assignmentExpression("=", t.identifier(name), t.objectExpression([])),
);
return t.expressionStatement(
t.callExpression(
t.functionExpression(
null,
[t.identifier(name)],
t.blockStatement(namespaceTopLevel),
),
[
parentExportName
? t.assignmentExpression(
"=",
t.memberExpression(
t.identifier(parentExportName),
t.identifier(name),
),
derivedParameter,
)
: derivedParameter,
],
),
);
}
@@ -0,0 +1,4 @@
; // Otherwise-empty file
export declare namespace P {
export namespace C {}
}
@@ -0,0 +1 @@
; // Otherwise-empty file
@@ -0,0 +1,3 @@
namespace N {
export class N {}
}
@@ -0,0 +1,3 @@
{
"throws": "A class may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
class N {}
}
@@ -0,0 +1,3 @@
{
"throws": "A class may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
export enum N {}
}
@@ -0,0 +1,3 @@
{
"throws": "An enum may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
enum N {}
}
@@ -0,0 +1,3 @@
{
"throws": "An enum may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
export function N() {}
}
@@ -0,0 +1,3 @@
{
"throws": "A function may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
function N() {}
}
@@ -0,0 +1,3 @@
{
"throws": "A function may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
export let V;
}
@@ -0,0 +1,3 @@
{
"throws": "Namespaces exporting non-const are unsupported."
}
@@ -0,0 +1,3 @@
namespace N {
export namespace N {}
}
@@ -0,0 +1,3 @@
{
"throws": "A namespace may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
namespace N {}
}
@@ -0,0 +1,3 @@
{
"throws": "A namespace may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
export const N;
}
@@ -0,0 +1,3 @@
{
"throws": "A variable may not share the name of its parent namespace."
}
@@ -0,0 +1,3 @@
namespace N {
let N;
}
@@ -0,0 +1,3 @@
{
"throws": "A variable may not share the name of its parent namespace."
}
@@ -0,0 +1,35 @@
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
constructor() {
console.log("1");
}
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

let strings = ["Hello", "98052", "101"];

let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

0 comments on commit 62f2195

Please sign in to comment.