Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 49 additions & 21 deletions src/externs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ import * as jsdoc from './jsdoc';
import {escapeForComment, maybeAddHeritageClauses, maybeAddTemplateClause} from './jsdoc_transformer';
import {ModuleTypeTranslator} from './module_type_translator';
import * as path from './path';
import {getEntityNameText, getIdentifierText, hasModifierFlag, isAmbient, isDtsFileName, reportDiagnostic} from './transformer_util';
import {getEntityNameText, getIdentifierText, hasModifierFlag, isAmbient, isEntityNameExpression, isDtsFileName, reportDiagnostic, getEntityNameExpressionText} from './transformer_util';
import {isValidClosurePropertyName} from './type_translator';

/**
Expand Down Expand Up @@ -139,14 +139,17 @@ export function getGeneratedExterns(
}

/**
* isInGlobalAugmentation returns true if declaration is the immediate child of a 'declare global'
* isInGlobalAugmentation returns true if declaration is the descendent of a 'declare global'
* block.
*/
function isInGlobalAugmentation(declaration: ts.Declaration): boolean {
// declare global { ... } creates a ModuleDeclaration containing a ModuleBlock containing the
// declaration, with the ModuleDeclaration having the GlobalAugmentation flag set.
if (!declaration.parent || !declaration.parent.parent) return false;
return (declaration.parent.parent.flags & ts.NodeFlags.GlobalAugmentation) !== 0;
let node: ts.Node = declaration;
while (node = node.parent) {
if ((node.flags & ts.NodeFlags.GlobalAugmentation) !== 0) return true;
}
return false;
}

/**
Expand Down Expand Up @@ -217,9 +220,11 @@ export function generateExterns(
* If `someName` is `declare global { namespace someName {...} }`, tsickle must not qualify access
* to it with the mangled module namespace as it is emitted in the global namespace. Similarly, if
* the symbol is declared in a non-module context, it must not be mangled.
* Typescript parses `someName` as an EntityNameExpression for `export`, and as `EntityName` for
* `import`.
*/
function qualifiedNameToMangledIdentifier(name: ts.Identifier|ts.QualifiedName) {
const entityName = getEntityNameText(name);
function qualifiedNameToMangledIdentifier(
name: ts.EntityName|ts.EntityNameExpression, entityName: string) {
let symbol = typeChecker.getSymbolAtLocation(name);
if (symbol) {
// If this is an aliased name (e.g. from an import), use the alias to refer to it.
Expand All @@ -228,7 +233,9 @@ export function generateExterns(
}
const alias = mtt.symbolsToAliasedNames.get(symbol);
if (alias) return alias;
const isGlobalSymbol = symbol && symbol.declarations && symbol.declarations.some(d => {
// If at least one declaration is local to a module, we can always use that and there won't be
// any name clash in generating externs for `export as namespace ${output}`. See test_files/underscore.
const isGlobalSymbol = symbol && symbol.declarations && symbol.declarations.every(d => {
if (isInGlobalAugmentation(d)) return true;
// If the declaration's source file is not a module, it must be global.
// If it is a module, the identifier must be local to this file, or handled above via the
Expand All @@ -240,25 +247,30 @@ export function generateExterns(
return rootNamespace + '.' + entityName;
}

function extractExportedNamespaceFromExportAssignment(exportAssignment:ts.ExportAssignment) {
const {expression} = exportAssignment;
if (isEntityNameExpression(expression)) {
// E.g. export = someName, export default someName;
// If someName is "declare global { namespace someName {...} }", tsickle must not qualify
// access to it with module namespace as it is emitted in the global namespace.
return qualifiedNameToMangledIdentifier(expression, getEntityNameExpressionText(expression));
} else {
reportDiagnostic(
diagnostics, expression,
`export =/default expression must be a qualified name, got ${
ts.SyntaxKind[exportAssignment.expression.kind]}.`);
return rootNamespace;
}
}

if (output && isExternalModule) {
// If tsickle generated any externs and this is an external module, prepend the namespace
// declaration for it.
output = `/** @const */\nvar ${rootNamespace} = {};\n` + output;

let exportedNamespace = rootNamespace;
if (exportAssignment && hasExportEquals) {
if (ts.isIdentifier(exportAssignment.expression) ||
ts.isQualifiedName(exportAssignment.expression)) {
// E.g. export = someName;
// If someName is "declare global { namespace someName {...} }", tsickle must not qualify
// access to it with module namespace as it is emitted in the global namespace.
exportedNamespace = qualifiedNameToMangledIdentifier(exportAssignment.expression);
} else {
reportDiagnostic(
diagnostics, exportAssignment.expression,
`export = expression must be a qualified name, got ${
ts.SyntaxKind[exportAssignment.expression.kind]}.`);
}
exportedNamespace = extractExportedNamespaceFromExportAssignment(exportAssignment);
// Assign the actually exported namespace object (which lives somewhere under rootNamespace)
// into the module's namespace.
emit(`/**\n * export = ${exportAssignment.expression.getText()}\n * @const\n */\n`);
Expand Down Expand Up @@ -537,6 +549,18 @@ export function generateExterns(
}
}

function writeExportAssignment(
exportAssignment: ts.ExportAssignment, namespace:ReadonlyArray<string>) {
// export = ... is handled at the file level.
if (exportAssignment.isExportEquals) return;
// export default
emit('/** @const */\n');
writeVariableStatement(
"default", namespace,
extractExportedNamespaceFromExportAssignment(exportAssignment)
);
}

/**
* Adds aliases for the symbols imported in the given declaration, so that their types get
* printed as the fully qualified name, and not just as a reference to the local import alias.
Expand Down Expand Up @@ -766,7 +790,8 @@ export function generateExterns(
addImportAliases(importEquals);
break;
}
const qn = qualifiedNameToMangledIdentifier(importEquals.moduleReference);
const qn = qualifiedNameToMangledIdentifier(importEquals.moduleReference,
getEntityNameText(importEquals.moduleReference));
// @const so that Closure Compiler understands this is an alias.
emit('/** @const */\n');
writeVariableStatement(localName, namespace, qn);
Expand Down Expand Up @@ -805,9 +830,12 @@ export function generateExterns(
addImportAliases(node as ts.ImportDeclaration);
break;
case ts.SyntaxKind.NamespaceExportDeclaration:
case ts.SyntaxKind.ExportAssignment:
// Handled on the file level.
break;
case ts.SyntaxKind.ExportAssignment:
const exportAssignment = node as ts.ExportAssignment;
writeExportAssignment(exportAssignment, namespace);
break;
case ts.SyntaxKind.ExportDeclaration:
const exportDeclaration = node as ts.ExportDeclaration;
writeExportDeclaration(exportDeclaration, namespace);
Expand Down
20 changes: 20 additions & 0 deletions src/transformer_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ export function getEntityNameText(name: ts.EntityName): string {
return getEntityNameText(name.left) + '.' + getIdentifierText(name.right);
}

export function getEntityNameExpressionText(name:ts.EntityNameExpression) :string{
if (ts.isIdentifier(name)) {
return getIdentifierText(name);
}
return getEntityNameExpressionText(name.expression) + '.' + getIdentifierText(name.name);
}

/**
* Returns true if node is ts.EntityNameExpression. This internal API of typescript is replicated here
* in order to be used in externs.ts.
*/
export function isEntityNameExpression(node: ts.Node): node is ts.EntityNameExpression {
return node.kind === ts.SyntaxKind.Identifier || isPropertyAccessEntityNameExpression(node);
}

function isPropertyAccessEntityNameExpression(node: ts.Node): node is ts.PropertyAccessEntityNameExpression {
return ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.name) &&
isEntityNameExpression(node.expression);
}

/**
* Converts an escaped TypeScript name into the original source name.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @fileoverview Tests declaring a namespace in a module .d.ts file, as a globally available symbol
* using a `declare global` block, where the .
* using a `declare global` block, where the symbol is nested in several namespaces.
*/

declare global {
Expand All @@ -12,6 +12,5 @@ declare global {
}

// tsickle must emit the `globalParentNamespace` reference below as a global name, not the mangled
// module scoped name. This is currently unsupported, tsickle reports an error for this pattern (see
// dtsdiagnostics.txt).
// module scoped name.
export = globalParentNamespace.globalNestedNamespace;
1 change: 0 additions & 1 deletion test_files/declare_export_global/dtsdiagnostics.txt

This file was deleted.

2 changes: 1 addition & 1 deletion test_files/declare_export_global/externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@ globalParentNamespace.globalNestedNamespace.x;
* export = globalParentNamespace.globalNestedNamespace
* @const
*/
var test_files$declare_export_global$declare_export_global_nested = test_files$declare_export_global$declare_export_global_nested_;
var test_files$declare_export_global$declare_export_global_nested = globalParentNamespace.globalNestedNamespace;
2 changes: 2 additions & 0 deletions test_files/declare_import/externs.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ var goog_imported$closure$Class = {};
* @struct
*/
goog_imported$closure$Class.ClosureClazz = function() {};
/** @const */
goog_imported$closure$Class.default = ClosureClazz;
12 changes: 12 additions & 0 deletions test_files/export_default_dts/export_default.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* @fileoverview Tests that default exports from .d.ts are defined on the root namespace with
* "default" property. Expressions in default export in d.ts are limited to identifiers and
* qualified names, and we test here that qualified name works.
*/
declare namespace innerNamespace {
class DefaultExportedClass{
property:string
}
}

export default innerNamespace.DefaultExportedClass;
19 changes: 19 additions & 0 deletions test_files/export_default_dts/externs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @externs
* @suppress {duplicate,checkTypes,missingOverride}
*/
// NOTE: generated by tsickle, do not edit.
// externs from test_files/export_default_dts/export_default.d.ts:
/** @const */
var test_files$export_default_dts$export_default = {};
/** @const */
test_files$export_default_dts$export_default.innerNamespace = {};
/**
* @constructor
* @struct
*/
test_files$export_default_dts$export_default.innerNamespace.DefaultExportedClass = function() {};
/** @type {string} */
test_files$export_default_dts$export_default.innerNamespace.DefaultExportedClass.prototype.property;
/** @const */
test_files$export_default_dts$export_default.default = test_files$export_default_dts$export_default.innerNamespace.DefaultExportedClass;