tsickle
Version:
Transpile TypeScript code to JavaScript with Closure annotations.
228 lines • 12.2 kB
JavaScript
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.namespaceTransformer = void 0;
/**
* @fileoverview Transformer to convert namespaces with nested
* types into a form that the JSCompiler understands.
*/
const ts = require("typescript");
const transformer_util_1 = require("./transformer_util");
/**
* Transforms declaration merging namespaces.
*
* A (non-ambient) namespace NS that has the same name as a class OC adds all
* its declarations to OC. Currently, only class and enum declarations inside NS
* are supported. The declarations are renamed and hoisted to the file level. A
* JSCompiler type alias property for each declaration in NS is added to class
* OC. The alias introduces a qualified name for the inner class or enum. The
* namespace is then eliminated so that tsickle does not generate an iife.
*
* Example:
* class Outer { }
* namespace Outer {
* export class InnerClass = { }
* export enum InnerEnum = { }
* }
*
* The above is transformed into:
*
* class Outer { }
* class Outer$InnerClass = { }
* enum Outer$InnerEnum = { }
* /** const * / Outer.InnerClass = Outer$InnerClass; // JSCompiler type alias
* /** const * / Outer.InnerEnum = Outer$InnerEnum; // JSCompiler type alias
*
*/
function namespaceTransformer(host, tsOptions, typeChecker, diagnostics) {
return (context) => {
return (sourceFile) => {
let haveTransformedNs = false;
let haveSeenError = false;
const transformedStmts = [];
for (const stmt of sourceFile.statements) {
visitTopLevelStatement(stmt);
}
if (!haveTransformedNs) {
return sourceFile;
}
return ts.factory.updateSourceFile(sourceFile, ts.setTextRange(ts.factory.createNodeArray(transformedStmts), sourceFile.statements));
// Local functions follow.
// Namespace `ns` has the same name as `mergedClass`. Their declarations
// are merged. Attaches the declarations defined in ns to mergedClass.
// Returns the transformed module body statements, or [ns] if the
// transformation fails.
function transformNamespace(ns, mergedClass) {
if (!ns.body || !ts.isModuleBlock(ns.body)) {
return [ns];
}
const nsName = (0, transformer_util_1.getIdentifierText)(ns.name);
const transformedNsStmts = [];
for (const stmt of ns.body.statements) {
if (ts.isEmptyStatement(stmt))
continue;
if (ts.isClassDeclaration(stmt)) {
transformInnerClass(stmt);
}
else if (ts.isEnumDeclaration(stmt)) {
transformInnerEnum(stmt);
}
else {
error(stmt, `unsupported statement in declaration merging namespace '${nsName}'`);
}
}
if (haveSeenError) {
// Drop the transformation.
return [ns];
}
// The namespace is now essentially empty. All the declarations have
// been hoisted out of it. Wrap it in a NotEmittedStatement to
// prevent the compiler from emitting an iife.
(0, transformer_util_1.markAsTransformedDeclMergeNs)(ns);
haveTransformedNs = true;
transformedNsStmts.push(ts.factory.createNotEmittedStatement(ns));
return transformedNsStmts;
// Local functions follow.
function transformInnerClass(classDecl) {
checkReferences(classDecl);
checkIsExported(classDecl);
if (!classDecl.name || !ts.isIdentifier(classDecl.name)) {
error(classDecl, "Expected class name");
return;
}
const className = (0, transformer_util_1.getIdentifierText)(classDecl.name);
const hoistedClassName = `${nsName}$${className}`;
const hoistedClassIdent = ts.factory.createIdentifier(hoistedClassName);
// The hoisted class is not directly exported.
const notExported = ts.factory.createModifiersFromModifierFlags(ts.getCombinedModifierFlags(classDecl) &
(~ts.ModifierFlags.Export));
const hoistedClassDecl = ts.factory.updateClassDeclaration(classDecl, classDecl.decorators, notExported, hoistedClassIdent, classDecl.typeParameters, classDecl.heritageClauses, classDecl.members);
ts.setOriginalNode(hoistedClassDecl, classDecl);
ts.setTextRange(hoistedClassDecl, classDecl);
transformedNsStmts.push(hoistedClassDecl);
// Add alias `/** @const */ nsName.className = hoistedClassName;`
transformedNsStmts.push(createInnerNameAlias(className, hoistedClassIdent, classDecl));
}
function transformInnerEnum(enumDecl) {
checkReferences(enumDecl);
checkIsExported(enumDecl);
// Hoist enum to top level and rename to ns$enumName.
const enumName = (0, transformer_util_1.getIdentifierText)(enumDecl.name);
const hoistedEnumName = `${nsName}$${enumName}`;
const hoistedEnumIdent = ts.factory.createIdentifier(hoistedEnumName);
// The hoisted enum is not directly exported.
const notExported = ts.factory.createModifiersFromModifierFlags(ts.getCombinedModifierFlags(enumDecl) &
(~ts.ModifierFlags.Export));
const hoistedEnumDecl = ts.factory.updateEnumDeclaration(enumDecl, enumDecl.decorators, notExported, hoistedEnumIdent, enumDecl.members);
ts.setOriginalNode(hoistedEnumDecl, enumDecl);
ts.setTextRange(hoistedEnumDecl, enumDecl);
transformedNsStmts.push(hoistedEnumDecl);
// Add alias `/** @const */ nsName.enumName = hoistedEnumName;`
transformedNsStmts.push(createInnerNameAlias(enumName, hoistedEnumIdent, enumDecl));
}
function createInnerNameAlias(propName, initializer, original) {
const prop = ts.factory.createExpressionStatement(ts.factory.createAssignment(ts.factory.createPropertyAccessExpression(mergedClass.name, propName), initializer));
ts.setTextRange(prop, original);
ts.setOriginalNode(prop, original);
return ts.addSyntheticLeadingComment(prop, ts.SyntaxKind.MultiLineCommentTrivia, '* @const ',
/* hasTrailingNewLine */ true);
}
function checkIsExported(decl) {
if (!(0, transformer_util_1.hasModifierFlag)(decl, ts.ModifierFlags.Export)) {
error(decl, `'${(0, transformer_util_1.getIdentifierText)(decl.name)}' must be exported`);
}
}
function checkNamespaceRef(ref) {
if (ts.isQualifiedName(ref)) {
checkNamespaceRef(ref.left);
return;
}
// ref is an unqulaified name. If it refers to a symbol that is
// defined in the namespace, it is missing a qualifier.
const sym = typeChecker.getSymbolAtLocation(ref);
// Property 'parent' is marked @internal, need to cast to access.
const parent = sym && sym.parent;
if (sym && parent &&
((parent.flags & ts.SymbolFlags.ValueModule) !== 0)) {
const parentName = parent.getName();
if (parentName === nsName) {
// This identifier should be qualified with the parentName.
const name = (0, transformer_util_1.getIdentifierText)(ref);
error(ref, `Name '${name}' must be qualified as '${parentName}.${name}'.`);
}
}
}
function checkReferences(node) {
// Visitor function that ensures that all references to namespace
// local symbols are properly qualified.
// TODO: Are there other node types that need to be handled?
function refCheckVisitor(node) {
if (ts.isTypeReferenceNode(node)) {
checkNamespaceRef(node.typeName);
return node;
}
if (ts.isPropertyAccessExpression(node)) {
// We only need to look at the right side of the '.'
return refCheckVisitor(node.expression);
}
if (!ts.isIdentifier(node)) {
return ts.visitEachChild(node, refCheckVisitor, context);
}
// node is a ts.Identifier.
if (node.parent &&
(ts.isClassDeclaration(node.parent) ||
ts.isEnumDeclaration(node.parent))) {
// Do not check the name of the class or enum declaration.
return node;
}
checkNamespaceRef(node);
return node;
}
ts.visitEachChild(node, refCheckVisitor, context);
}
}
function visitTopLevelStatement(node) {
if (!ts.isModuleDeclaration(node) || (0, transformer_util_1.isAmbient)(node)) {
transformedStmts.push(node);
return;
}
// Check if the namespace is merged with an existing class.
const ns = node;
const sym = typeChecker.getSymbolAtLocation(ns.name);
if (!sym || ns.name.kind === ts.SyntaxKind.StringLiteral) {
// Must have a symbol name for declaration merging.
transformedStmts.push(ns);
return;
}
// For a merged namespace, the symbol must already have been declared
// prior to the namespace declaration, or the compiler reports TS2434.
const isMergedNs = sym.valueDeclaration && sym.valueDeclaration.pos !== ns.pos;
if (!isMergedNs) {
transformedStmts.push(ns); // Nothing to do here.
error(ns.name, 'transformation of plain namespace not supported.');
return;
}
if (!ts.isClassDeclaration(sym.valueDeclaration) ||
(0, transformer_util_1.isAmbient)(sym.valueDeclaration)) {
// The previous declaration is not a class.
transformedStmts.push(ns); // Nothing to do here.
error(ns.name, 'declaration merging with non-class is not supported');
return;
}
transformedStmts.push(...transformNamespace(ns, sym.valueDeclaration));
}
function error(node, message) {
(0, transformer_util_1.reportDiagnostic)(diagnostics, node, message);
haveSeenError = true;
}
};
};
}
exports.namespaceTransformer = namespaceTransformer;
//# sourceMappingURL=ns_transformer.js.map
;