diff --git a/package.json b/package.json index ccbf77dab..1f44f6949 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "tsickle", "version": "0.23.3", "description": "Transpile TypeScript code to JavaScript with Closure annotations.", - "main": "built/src/tsickle.js", - "typings": "built/definitions/tsickle.d.ts", + "main": "built/src/index.js", + "typings": "built/definitions/index.d.ts", "bin": "built/src/main.js", "directories": { "test": "test" @@ -15,10 +15,11 @@ "source-map-support": "^0.4.2" }, "peerDependencies": { - "typescript": "2.3.1" + "typescript": "2.3.4" }, "devDependencies": { "@types/chai": "^3.4.32", + "@types/diff": "^3.2.0", "@types/glob": "^5.0.29", "@types/google-closure-compiler": "0.0.18", "@types/minimatch": "^2.0.28", @@ -30,6 +31,7 @@ "@types/source-map-support": "^0.2.27", "chai": "^3.5.0", "clang-format": "^1.0.51", + "diff": "^3.2.0", "glob": "^7.0.0", "google-closure-compiler": "^20161024.1.0", "gulp": "^3.8.11", diff --git a/src/decorator-annotator.ts b/src/decorator-annotator.ts index b01363a76..b076ac3a7 100644 --- a/src/decorator-annotator.ts +++ b/src/decorator-annotator.ts @@ -6,27 +6,32 @@ * found in the LICENSE file at https://angular.io/license */ -import {SourceMapGenerator} from 'source-map'; import * as ts from 'typescript'; import {getDecoratorDeclarations} from './decorators'; -import {Rewriter} from './rewriter'; +import {getIdentifierText, Rewriter} from './rewriter'; +import {SourceMapper} from './source_map_utils'; import {assertTypeChecked, TypeTranslator} from './type-translator'; import {toArray} from './util'; -// ClassRewriter rewrites a single "class Foo {...}" declaration. +// DecoratorClassVisitor rewrites a single "class Foo {...}" declaration. // It's its own object because we collect decorators on the class and the ctor // separately for each class we encounter. -class ClassRewriter extends Rewriter { +export class DecoratorClassVisitor { /** Decorators on the class itself. */ decorators: ts.Decorator[]; /** The constructor parameter list and decorators on each param. */ - ctorParameters: Array<[string | undefined, ts.Decorator[]|undefined]|null>; + ctorParameters: Array<[[ts.TypeNode, string] | undefined, ts.Decorator[]|undefined]|null>; /** Per-method decorators. */ propDecorators: Map; - constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) { - super(sourceFile); + constructor( + private typeChecker: ts.TypeChecker, private rewriter: Rewriter, + private classDecl: ts.ClassDeclaration) { + if (classDecl.decorators) { + const toLower = this.decoratorsToLower(classDecl); + if (toLower.length > 0) this.decorators = toLower; + } } /** @@ -69,49 +74,16 @@ class ClassRewriter extends Rewriter { return []; } - /** - * process is the main entry point, rewriting a single class node. - */ - process(node: ts.ClassDeclaration): {output: string, diagnostics: ts.Diagnostic[]} { - if (node.decorators) { - const toLower = this.decoratorsToLower(node); - if (toLower.length > 0) this.decorators = toLower; - } - - // Emit the class contents, but stop just before emitting the closing curly brace. - // (This code is the same as Rewriter.writeNode except for the curly brace handling.) - let pos = node.getFullStart(); - ts.forEachChild(node, child => { - // This forEachChild handles emitting the text between each child, while child.visit - // recursively emits the children themselves. - this.writeRange(pos, child.getFullStart()); - this.visit(child); - pos = child.getEnd(); - }); - - // At this point, we've emitted up through the final child of the class, so all that - // remains is the trailing whitespace and closing curly brace. - // The final character owned by the class node should always be a '}', - // or we somehow got the AST wrong and should report an error. - // (Any whitespace or semicolon following the '}' will be part of the next Node.) - if (this.file.text[node.getEnd() - 1] !== '}') { - this.error(node, 'unexpected class terminator'); - } - this.writeRange(pos, node.getEnd() - 1); - this.emitMetadata(); - this.emit('}'); - return this.getOutput(); - } - /** * gatherConstructor grabs the parameter list and decorators off the class * constructor, and emits nothing. */ private gatherConstructor(ctor: ts.ConstructorDeclaration) { - const ctorParameters: Array<[string | undefined, ts.Decorator[] | undefined]|null> = []; + const ctorParameters: + Array<[[ts.TypeNode, string] | undefined, ts.Decorator[] | undefined]|null> = []; let hasDecoratedParam = false; for (const param of ctor.parameters) { - let paramCtor: string|undefined; + let paramCtor: [ts.TypeNode, string]|undefined; let decorators: ts.Decorator[]|undefined; if (param.decorators) { decorators = this.decoratorsToLower(param); @@ -122,8 +94,9 @@ class ClassRewriter extends Rewriter { // Verify that "Bar" is a value (e.g. a constructor) and not just a type. const sym = this.typeChecker.getTypeAtLocation(param.type).getSymbol(); if (sym && (sym.flags & ts.SymbolFlags.Value)) { - paramCtor = new TypeTranslator(this.typeChecker, param.type) - .symbolToString(sym, /* useFqn */ true); + const typeStr = new TypeTranslator(this.typeChecker, param.type) + .symbolToString(sym, /* useFqn */ true); + paramCtor = [param.type, typeStr]; } } if (paramCtor || decorators) { @@ -147,7 +120,7 @@ class ClassRewriter extends Rewriter { if (!method.name || method.name.kind !== ts.SyntaxKind.Identifier) { // Method has a weird name, e.g. // [Symbol.foo]() {...} - this.error(method, 'cannot process decorators on strangely named method'); + this.rewriter.error(method, 'cannot process decorators on strangely named method'); return; } @@ -158,144 +131,204 @@ class ClassRewriter extends Rewriter { this.propDecorators.set(name, decorators); } - /** - * maybeProcess is called by the traversal of the AST. - * @return True if the node was handled, false to have the node emitted as normal. - */ - protected maybeProcess(node: ts.Node): boolean { + beforeProcessNode(node: ts.Node) { switch (node.kind) { - case ts.SyntaxKind.ClassDeclaration: - // Encountered a new class while processing this class; use a new separate - // rewriter to gather+emit its metadata. - const {output, diagnostics} = - new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration); - this.diagnostics.push(...diagnostics); - this.emit(output); - return true; case ts.SyntaxKind.Constructor: this.gatherConstructor(node as ts.ConstructorDeclaration); - return false; // Proceed with ordinary emit of the ctor. + break; case ts.SyntaxKind.PropertyDeclaration: case ts.SyntaxKind.SetAccessor: case ts.SyntaxKind.GetAccessor: case ts.SyntaxKind.MethodDeclaration: this.gatherMethodOrProperty(node as ts.Declaration); - return false; // Proceed with ordinary emit of the method. - case ts.SyntaxKind.Decorator: - if (this.shouldLower(node as ts.Decorator)) { - // Return true to signal that this node should not be emitted, - // but still emit the whitespace *before* the node. - this.writeRange(node.getFullStart(), node.getStart()); - return true; - } - return false; + break; default: - return false; } } + maybeProcessDecorator(node: ts.Node, start?: number): boolean { + if (this.shouldLower(node as ts.Decorator)) { + // Return true to signal that this node should not be emitted, + // but still emit the whitespace *before* the node. + if (!start) { + start = node.getFullStart(); + } + this.rewriter.writeRange(node, start, node.getStart()); + return true; + } + return false; + } + /** - * emitMetadata emits the various gathered metadata, as static fields. + * emits the types for the various gathered metadata to be used + * in the tsickle type annotations helper. */ - private emitMetadata() { + emitMetadataTypeAnnotationsHelpers() { + if (!this.classDecl.name) return; + const className = getIdentifierText(this.classDecl.name); + if (this.decorators) { + this.rewriter.emit(`/** @type {!Array<{type: !Function, args: (undefined|!Array)}>} */\n`); + this.rewriter.emit(`${className}.decorators;\n`); + } + if (this.decorators || this.ctorParameters) { + this.rewriter.emit(`/**\n`); + this.rewriter.emit(` * @nocollapse\n`); + this.rewriter.emit( + ` * @type {function(): !Array<(null|{type: ?, decorators: (undefined|!Array<{type: !Function, args: (undefined|!Array)}>)})>}\n`); + this.rewriter.emit(` */\n`); + this.rewriter.emit(`${className}.ctorParameters;\n`); + } + if (this.propDecorators) { + this.rewriter.emit( + `/** @type {!Object)}>>} */\n`); + this.rewriter.emit(`${className}.propDecorators;\n`); + } + } + + /** + * emits the various gathered metadata, as static fields. + */ + emitMetadataAsStaticProperties() { const decoratorInvocations = '{type: Function, args?: any[]}[]'; if (this.decorators) { - this.emit(`static decorators: ${decoratorInvocations} = [\n`); + this.rewriter.emit(`static decorators: ${decoratorInvocations} = [\n`); for (const annotation of this.decorators) { this.emitDecorator(annotation); - this.emit(',\n'); + this.rewriter.emit(',\n'); } - this.emit('];\n'); + this.rewriter.emit('];\n'); } if (this.decorators || this.ctorParameters) { - this.emit(`/** @nocollapse */\n`); + this.rewriter.emit(`/** @nocollapse */\n`); // ctorParameters may contain forward references in the type: field, so wrap in a function // closure - this.emit( + this.rewriter.emit( `static ctorParameters: () => ({type: any, decorators?: ` + decoratorInvocations + `}|null)[] = () => [\n`); + let emittedInline = false; for (const param of this.ctorParameters || []) { if (!param) { - this.emit('null,\n'); + if (emittedInline) { + this.rewriter.emit(' '); + } + this.rewriter.emit('null,'); + emittedInline = true; continue; } + if (emittedInline) { + this.rewriter.emit('\n'); + emittedInline = false; + } const [ctor, decorators] = param; - this.emit(`{type: ${ctor}, `); + this.rewriter.emit(`{type: `); + if (!ctor) { + this.rewriter.emit(`undefined`); + } else { + const [typeNode, typeStr] = ctor; + const emitNode = findTypeNameNodeWithRange( + typeNode, typeNode.getStart(), typeNode.getStart() + typeStr.length); + if (emitNode && emitNode.getText() === typeStr) { + this.rewriter.writeRange(emitNode, emitNode.getStart(), emitNode.getEnd()); + } else { + this.rewriter.emit(typeStr); + } + } + this.rewriter.emit(`, `); if (decorators) { - this.emit('decorators: ['); + this.rewriter.emit('decorators: ['); for (const decorator of decorators) { this.emitDecorator(decorator); - this.emit(', '); + this.rewriter.emit(', '); } - this.emit(']'); + this.rewriter.emit(']'); } - this.emit('},\n'); + this.rewriter.emit('},\n'); } - this.emit(`];\n`); + this.rewriter.emit(`];\n`); } if (this.propDecorators) { - this.emit(`static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`); + this.rewriter.emit( + `static propDecorators: {[key: string]: ` + decoratorInvocations + `} = {\n`); for (const name of toArray(this.propDecorators.keys())) { - this.emit(`'${name}': [`); + this.rewriter.emit(`"${name}": [`); for (const decorator of this.propDecorators.get(name)!) { this.emitDecorator(decorator); - this.emit(','); + this.rewriter.emit(','); } - this.emit('],\n'); + this.rewriter.emit('],\n'); } - this.emit('};\n'); + this.rewriter.emit('};\n'); } } private emitDecorator(decorator: ts.Decorator) { - this.emit('{ type: '); + this.rewriter.emit('{ type: '); const expr = decorator.expression; switch (expr.kind) { case ts.SyntaxKind.Identifier: // The decorator was a plain @Foo. - this.visit(expr); + this.rewriter.visit(expr); break; case ts.SyntaxKind.CallExpression: // The decorator was a call, like @Foo(bar). const call = expr as ts.CallExpression; - this.visit(call.expression); + this.rewriter.visit(call.expression); if (call.arguments.length) { - this.emit(', args: ['); + this.rewriter.emit(', args: ['); for (const arg of call.arguments) { - this.emit(arg.getText()); - this.emit(', '); + this.rewriter.writeNodeFrom(arg, arg.getStart()); + this.rewriter.emit(', '); } - this.emit(']'); + this.rewriter.emit(']'); } break; default: - this.errorUnimplementedKind(expr, 'gathering metadata'); - this.emit('undefined'); + this.rewriter.errorUnimplementedKind(expr, 'gathering metadata'); + this.rewriter.emit('undefined'); } - this.emit(' }'); + this.rewriter.emit(' }'); } } +function findTypeNameNodeWithRange(node: ts.Node, start: number, end: number): ts.Node|undefined { + if (node.getStart() === start && node.getEnd() === end) { + return node; + } + return ts.forEachChild(node, (child) => findTypeNameNodeWithRange(child, start, end)); +} + class DecoratorRewriter extends Rewriter { - constructor(private typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) { - super(sourceFile); + /** ComposableDecoratorRewriter when using tsickle as a TS transformer */ + private currentDecoratorConverter: DecoratorClassVisitor; + + constructor( + private typeChecker: ts.TypeChecker, file: ts.SourceFile, sourceMapper?: SourceMapper) { + super(file, sourceMapper); } - process(): {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} { + process(): {output: string, diagnostics: ts.Diagnostic[]} { this.visit(this.file); return this.getOutput(); } protected maybeProcess(node: ts.Node): boolean { + if (this.currentDecoratorConverter) { + this.currentDecoratorConverter.beforeProcessNode(node); + } switch (node.kind) { + case ts.SyntaxKind.Decorator: + return this.currentDecoratorConverter && + this.currentDecoratorConverter.maybeProcessDecorator(node); case ts.SyntaxKind.ClassDeclaration: - const {output, diagnostics} = - new ClassRewriter(this.typeChecker, this.file).process(node as ts.ClassDeclaration); - this.diagnostics.push(...diagnostics); - this.emit(output); + const oldDecoratorConverter = this.currentDecoratorConverter; + this.currentDecoratorConverter = + new DecoratorClassVisitor(this.typeChecker, this, node as ts.ClassDeclaration); + this.writeRange(node, node.getFullStart(), node.getStart()); + visitClassContent(node as ts.ClassDeclaration, this, this.currentDecoratorConverter); + this.currentDecoratorConverter = oldDecoratorConverter; return true; default: return false; @@ -303,8 +336,51 @@ class DecoratorRewriter extends Rewriter { } } -export function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): - {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} { +export function convertDecorators( + typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, + sourceMapper?: SourceMapper): {output: string, diagnostics: ts.Diagnostic[]} { assertTypeChecked(sourceFile); - return new DecoratorRewriter(typeChecker, sourceFile).process(); + return new DecoratorRewriter(typeChecker, sourceFile, sourceMapper).process(); +} + +export function visitClassContent( + classDecl: ts.ClassDeclaration, rewriter: Rewriter, decoratorVisitor?: DecoratorClassVisitor) { + let pos = classDecl.getStart(); + if (decoratorVisitor) { + // strip out decorators if needed + ts.forEachChild(classDecl, child => { + if (child.kind !== ts.SyntaxKind.Decorator) { + return; + } + // Note: The getFullStart() of the first decorator is the same + // as the getFullStart() of the class declaration. + // Therefore, we need to use Math.max to not print the whitespace + // of the class again. + const childStart = Math.max(pos, child.getFullStart()); + rewriter.writeRange(classDecl, pos, childStart); + if (decoratorVisitor.maybeProcessDecorator(child, childStart)) { + pos = child.getEnd(); + } + }); + } + if (classDecl.members.length > 0) { + rewriter.writeRange(classDecl, pos, classDecl.members[0].getFullStart()); + for (const member of classDecl.members) { + rewriter.visit(member); + } + pos = classDecl.getLastToken().getFullStart(); + } + // At this point, we've emitted up through the final child of the class, so all that + // remains is the trailing whitespace and closing curly brace. + // The final character owned by the class node should always be a '}', + // or we somehow got the AST wrong and should report an error. + // (Any whitespace or semicolon following the '}' will be part of the next Node.) + if (rewriter.file.text[classDecl.getEnd() - 1] !== '}') { + rewriter.error(classDecl, 'unexpected class terminator'); + } + rewriter.writeRange(classDecl, pos, classDecl.getEnd() - 1); + if (decoratorVisitor) { + decoratorVisitor.emitMetadataAsStaticProperties(); + } + rewriter.emit('}'); } diff --git a/src/es5processor.ts b/src/es5processor.ts index 381decf26..b7a3b1e43 100644 --- a/src/es5processor.ts +++ b/src/es5processor.ts @@ -8,7 +8,9 @@ import * as ts from 'typescript'; +import {ModulesManifest} from './modules_manifest'; import {getIdentifierText, Rewriter} from './rewriter'; +import {SourceMapper} from './source_map_utils'; import {toArray} from './util'; /** @@ -52,24 +54,24 @@ class ES5Processor extends Rewriter { unusedIndex = 0; constructor( - file: ts.SourceFile, private pathToModuleName: (context: string, fileName: string) => string, - private prelude: string) { - super(file); + private host: Host, private options: Options, file: ts.SourceFile, + sourceMapper?: SourceMapper) { + super(file, sourceMapper); } - process(moduleId: string, isES5: boolean): {output: string, referencedModules: string[]} { + process(moduleId: string): {output: string, referencedModules: string[]} { // TODO(evanm): only emit the goog.module *after* the first comment, // so that @suppress statements work. - const moduleName = this.pathToModuleName('', this.file.fileName); + const moduleName = this.host.pathToModuleName('', this.file.fileName); // NB: No linebreak after module call so sourcemaps are not offset. this.emit(`goog.module('${moduleName}');`); - if (this.prelude) this.emit(this.prelude); + if (this.options.prelude) this.emit(this.options.prelude); // Allow code to use `module.id` to discover its module URL, e.g. to resolve // a template URL against. // Uses 'var', as this code is inserted in ES6 and ES5 modes. // The following pattern ensures closure doesn't throw an error in advanced // optimizations mode. - if (isES5) { + if (this.options.es5Mode) { this.emit(`var module = module || {id: '${moduleId}'};`); } else { // The `exports = {}` serves as a default export to disable Closure Compiler's error checking @@ -82,11 +84,11 @@ class ES5Processor extends Rewriter { let pos = 0; for (const stmt of this.file.statements) { - this.writeRange(pos, stmt.getFullStart()); + this.writeRange(this.file, pos, stmt.getFullStart()); this.visitTopLevel(stmt); pos = stmt.getEnd(); } - this.writeRange(pos, this.file.getEnd()); + this.writeRange(this.file, pos, this.file.getEnd()); const referencedModules = toArray(this.moduleVariables.keys()); // Note: don't sort referencedModules, as the keys are in the same order @@ -140,7 +142,7 @@ class ES5Processor extends Rewriter { * comment(s). */ emitCommentWithoutStatementBody(node: ts.Node) { - this.writeRange(node.getFullStart(), node.getStart()); + this.writeRange(node, node.getFullStart(), node.getStart()); } /** isUseStrict returns true if node is a "use strict"; statement. */ @@ -177,7 +179,7 @@ class ES5Processor extends Rewriter { const call = decl.initializer as ts.CallExpression; const require = this.isRequire(call); if (!require) return false; - this.writeRange(node.getFullStart(), node.getStart()); + this.writeRange(node, node.getFullStart(), node.getStart()); this.emitGoogRequire(varName, require); return true; } else if (node.kind === ts.SyntaxKind.ExpressionStatement) { @@ -203,7 +205,7 @@ class ES5Processor extends Rewriter { } if (!require) return false; - this.writeRange(node.getFullStart(), node.getStart()); + this.writeRange(node, node.getFullStart(), node.getStart()); const varName = this.emitGoogRequire(null, require); if (isExport) { @@ -239,7 +241,7 @@ class ES5Processor extends Rewriter { modName = nsImport; isNamespaceImport = true; } else { - modName = this.pathToModuleName(this.file.fileName, tsImport); + modName = this.host.pathToModuleName(this.file.fileName, tsImport); } if (!varName) { @@ -321,7 +323,7 @@ class ES5Processor extends Rewriter { if (!this.namespaceImports.has(lhs)) break; // Emit the same expression, with spaces to replace the ".default" part // so that source maps still line up. - this.writeRange(node.getFullStart(), node.getStart()); + this.writeRange(node, node.getFullStart(), node.getStart()); this.emit(`${lhs} `); return true; default: @@ -336,6 +338,25 @@ class ES5Processor extends Rewriter { } } +export interface Host { + /** + * Takes a context (the current file) and the path of the file to import + * and generates a googmodule module name + */ + pathToModuleName(context: string, importPath: string): string; + /** + * If we do googmodule processing, we polyfill module.id, since that's + * part of ES6 modules. This function determines what the module.id will be + * for each file. + */ + fileNameToModuleId(fileName: string): string; +} + +export interface Options { + es5Mode?: boolean; + prelude?: string; +} + /** * Converts TypeScript's JS+CommonJS output to Closure goog.module etc. * For use as a postprocessing step *after* TypeScript emits JavaScript. @@ -350,10 +371,23 @@ class ES5Processor extends Rewriter { * @param prelude An additional prelude to insert after the `goog.module` call, * e.g. with additional imports or requires. */ -export function processES5( - fileName: string, moduleId: string, content: string, - pathToModuleName: (context: string, fileName: string) => string, isES5 = true, - prelude = ''): {output: string, referencedModules: string[]} { +export function processES5(host: Host, options: Options, fileName: string, content: string): + {output: string, referencedModules: string[]} { const file = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES5, true); - return new ES5Processor(file, pathToModuleName, prelude).process(moduleId, isES5); + const moduleId = host.fileNameToModuleId(fileName); + return new ES5Processor(host, options, file).process(moduleId); +} + +export function convertCommonJsToGoogModule( + host: Host, options: Options, modulesManifest: ModulesManifest, fileName: string, + content: string): string { + const {output, referencedModules} = processES5(host, options, fileName, content); + + const moduleName = host.pathToModuleName('', fileName); + modulesManifest.addModule(fileName, moduleName); + for (const referenced of referencedModules) { + modulesManifest.addReferencedModule(fileName, referenced); + } + + return output; } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..dd07c68ef --- /dev/null +++ b/src/index.ts @@ -0,0 +1,14 @@ +/** + * @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 + */ + +export {convertDecorators} from './decorator-annotator'; +export {processES5} from './es5processor'; +export {FileMap, ModulesManifest} from './modules_manifest'; +export {EmitResult, EmitTransformers, emitWithTsickle, TransformerHost, TransformerOptions} from './transformer'; +export {getGeneratedExterns} from './tsickle'; +export {Options, Pass, TsickleCompilerHost, TsickleHost} from './tsickle_compiler_host'; diff --git a/src/jsdoc.ts b/src/jsdoc.ts index 447861a0d..9b1527b81 100644 --- a/src/jsdoc.ts +++ b/src/jsdoc.ts @@ -245,8 +245,9 @@ export function toString(tags: Tag[], escapeExtraTags = new Set()): stri if (tags.length === 0) return ''; if (tags.length === 1) { const tag = tags[0]; - if (tag.tagName === 'type' && (!tag.text || !tag.text.match('\n'))) { - // Special-case one-liner "type" tags to fit on one line, e.g. + if ((tag.tagName === 'type' || tag.tagName === 'nocollapse') && + (!tag.text || !tag.text.match('\n'))) { + // Special-case one-liner "type" and "nocollapse" tags to fit on one line, e.g. // /** @type {foo} */ return '/**' + tagToString(tag, escapeExtraTags) + ' */\n'; } diff --git a/src/main.ts b/src/main.ts index c6b5a2d3e..df2d2060a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,8 @@ import * as ts from 'typescript'; import * as cliSupport from './cli_support'; import * as tsickle from './tsickle'; +import * as tsickleCompilerHost from './tsickle_compiler_host'; + import {toArray, createOutputRetainingCompilerHost, createSourceReplacingCompilerHost} from './util'; /** Tsickle settings passed on the command line. */ export interface Settings { @@ -125,10 +127,10 @@ function loadTscConfig(args: string[], allDiagnostics: ts.Diagnostic[]): } export interface ClosureJSOptions { - tsickleCompilerHostOptions: tsickle.Options; - tsickleHost: tsickle.TsickleHost; + tsickleCompilerHostOptions: tsickleCompilerHost.Options; + tsickleHost: tsickleCompilerHost.TsickleHost; files: Map; - tsicklePasses: tsickle.Pass[]; + tsicklePasses: tsickleCompilerHost.Pass[]; } function getDefaultClosureJSOptions(fileNames: string[], settings: Settings): ClosureJSOptions { @@ -145,7 +147,7 @@ function getDefaultClosureJSOptions(fileNames: string[], settings: Settings): Cl fileNameToModuleId: (fileName) => fileName, }, files: new Map(), - tsicklePasses: [tsickle.Pass.CLOSURIZE], + tsicklePasses: [tsickleCompilerHost.Pass.CLOSURIZE], }; } @@ -172,7 +174,7 @@ export function toClosureJS( const sourceReplacingHost = createSourceReplacingCompilerHost(closureJSOptions.files, outputRetainingHost); - const tch = new tsickle.TsickleCompilerHost( + const tch = new tsickleCompilerHost.TsickleCompilerHost( sourceReplacingHost, options, closureJSOptions.tsickleCompilerHostOptions, closureJSOptions.tsickleHost); @@ -187,13 +189,13 @@ export function toClosureJS( // Reparse and reload the program, inserting the tsickle output in // place of the original source. - if (closureJSOptions.tsicklePasses.indexOf(tsickle.Pass.DECORATOR_DOWNLEVEL) !== -1) { - tch.reconfigureForRun(program, tsickle.Pass.DECORATOR_DOWNLEVEL); + if (closureJSOptions.tsicklePasses.indexOf(tsickleCompilerHost.Pass.DECORATOR_DOWNLEVEL) !== -1) { + tch.reconfigureForRun(program, tsickleCompilerHost.Pass.DECORATOR_DOWNLEVEL); program = ts.createProgram(fileNames, options, tch); } - if (closureJSOptions.tsicklePasses.indexOf(tsickle.Pass.CLOSURIZE) !== -1) { - tch.reconfigureForRun(program, tsickle.Pass.CLOSURIZE); + if (closureJSOptions.tsicklePasses.indexOf(tsickleCompilerHost.Pass.CLOSURIZE) !== -1) { + tch.reconfigureForRun(program, tsickleCompilerHost.Pass.CLOSURIZE); program = ts.createProgram(fileNames, options, tch); } diff --git a/src/rewriter.ts b/src/rewriter.ts index 9ce45c80a..d8e2673da 100644 --- a/src/rewriter.ts +++ b/src/rewriter.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {SourceMapGenerator} from 'source-map'; import * as ts from 'typescript'; +import {NOOP_SOURCE_MAPPER, SourceMapper, SourcePosition} from './source_map_utils'; + /** * A Rewriter manages iterating through a ts.SourceFile, copying input * to output while letting the subclass potentially alter some nodes @@ -18,33 +19,29 @@ export abstract class Rewriter { private output: string[] = []; /** Errors found while examining the code. */ protected diagnostics: ts.Diagnostic[] = []; - /** The source map that's generated while rewriting this file. */ - private sourceMap: SourceMapGenerator; /** Current position in the output. */ - private position = {line: 1, column: 1}; + private position: SourcePosition = {line: 0, column: 0, position: 0}; /** * The current level of recursion through TypeScript Nodes. Used in formatting internal debug * print statements. */ private indent = 0; + /** + * Skip emitting any code before the given offset. Used to avoid emitting @fileoverview comments + * twice. + */ + public skipUpToOffset = 0; - constructor(protected file: ts.SourceFile) { - this.sourceMap = new SourceMapGenerator({file: file.fileName}); - this.sourceMap.addMapping({ - original: this.position, - generated: this.position, - source: file.fileName, - }); + constructor(public file: ts.SourceFile, private sourceMapper: SourceMapper = NOOP_SOURCE_MAPPER) { } - getOutput(): {output: string, diagnostics: ts.Diagnostic[], sourceMap: SourceMapGenerator} { + getOutput(): {output: string, diagnostics: ts.Diagnostic[]} { if (this.indent !== 0) { throw new Error('visit() failed to track nesting'); } return { output: this.output.join(''), diagnostics: this.diagnostics, - sourceMap: this.sourceMap, }; } @@ -71,7 +68,7 @@ export abstract class Rewriter { } /** writeNode writes a ts.Node, calling this.visit() on its children. */ - writeNode(node: ts.Node, skipComments = false) { + writeNode(node: ts.Node, skipComments = false, newLineIfCommentsStripped = true) { let pos = node.getFullStart(); if (skipComments) { // To skip comments, we skip all whitespace/comments preceding @@ -79,50 +76,51 @@ export abstract class Rewriter { // a newline in its place so that the node remains separated // from the previous node. TODO: don't skip anything here if // there wasn't any comment. - if (node.getFullStart() < node.getStart()) { + if (newLineIfCommentsStripped && node.getFullStart() < node.getStart()) { this.emit('\n'); } pos = node.getStart(); } + this.writeNodeFrom(node, pos); + } + + writeNodeFrom(node: ts.Node, pos: number) { ts.forEachChild(node, child => { - this.writeRange(pos, child.getFullStart()); + this.writeRange(node, pos, child.getFullStart()); this.visit(child); pos = child.getEnd(); }); - this.writeRange(pos, node.getEnd()); + this.writeRange(node, pos, node.getEnd()); } - /** - * Skip emitting any code before the given offset. Used to avoid emitting @fileoverview comments - * twice. - */ - protected skipUpToOffset = 0; - /** * Write a span of the input file as expressed by absolute offsets. * These offsets are found in attributes like node.getFullStart() and * node.getEnd(). */ - writeRange(from: number, to: number) { + writeRange(node: ts.Node, from: number, to: number) { from = Math.max(from, this.skipUpToOffset); + if (this.skipUpToOffset > 0 && to <= this.skipUpToOffset) { + return; + } + // Add a source mapping. writeRange(from, to) always corresponds to + // original source code, so add a mapping at the current location that + // points back to the location at `from`. The additional code generated + // by tsickle will then be considered part of the last mapped code + // section preceding it. That's arguably incorrect (e.g. for the fake + // methods defining properties), but is good enough for stack traces. + const pos = this.file.getLineAndCharacterOfPosition(from); + this.sourceMapper.addMapping( + node, {line: pos.line, column: pos.character, position: from}, this.position, to - from); // getSourceFile().getText() is wrong here because it has the text of // the SourceFile node of the AST, which doesn't contain the comments // preceding that node. Semantically these ranges are just offsets // into the original source file text, so slice from that. const text = this.file.text.slice(from, to); if (text) { - // Add a source mapping. writeRange(from, to) always corresponds to - // original source code, so add a mapping at the current location that - // points back to the location at `from`. The additional code generated - // by tsickle will then be considered part of the last mapped code - // section preceding it. That's arguably incorrect (e.g. for the fake - // methods defining properties), but is good enough for stack traces. - const pos = this.file.getLineAndCharacterOfPosition(from); - this.sourceMap.addMapping({ - original: {line: pos.line + 1, column: pos.character + 1}, - generated: this.position, - source: this.file.fileName, - }); + if (text.indexOf('A Used by implement_import.ts') !== -1) { + console.log('>>> now', node.kind, new Error().stack); + } this.emit(text); } } @@ -133,9 +131,10 @@ export abstract class Rewriter { this.position.column++; if (c === '\n') { this.position.line++; - this.position.column = 1; + this.position.column = 0; } } + this.position.position += str.length; } /** Removes comment metacharacters from a string, to make it safe to embed in a comment. */ diff --git a/src/source_map_utils.ts b/src/source_map_utils.ts index bafb94f5f..6fa347409 100644 --- a/src/source_map_utils.ts +++ b/src/source_map_utils.ts @@ -7,6 +7,7 @@ */ import {SourceMapConsumer, SourceMapGenerator} from 'source-map'; +import * as ts from 'typescript'; /** * Return a new RegExp object every time we want one because the @@ -96,3 +97,47 @@ export function sourceMapTextToGenerator(sourceMapText: string): SourceMapGenera const sourceMapJson: any = sourceMapText; return SourceMapGenerator.fromSourceMap(sourceMapTextToConsumer(sourceMapJson)); } + +export interface SourcePosition { + // 0 based + column: number; + // 0 based + line: number; + // 0 based + position: number; +} + +export interface SourceMapper { + addMapping( + originalNode: ts.Node, original: SourcePosition, generated: SourcePosition, + length: number): void; +} + +export const NOOP_SOURCE_MAPPER: SourceMapper = { + // tslint:disable-next-line:no-empty + addMapping: () => {} +}; + +export class DefaultSourceMapper implements SourceMapper { + /** The source map that's generated while rewriting this file. */ + public sourceMap = new SourceMapGenerator(); + + constructor(private fileName: string) { + this.sourceMap.addMapping({ + original: {line: 1, column: 1}, + generated: {line: 1, column: 1}, + source: this.fileName, + }); + } + + addMapping(node: ts.Node, original: SourcePosition, generated: SourcePosition, length: number): + void { + if (length > 0) { + this.sourceMap.addMapping({ + original: {line: original.line + 1, column: original.column + 1}, + generated: {line: generated.line + 1, column: generated.column + 1}, + source: this.fileName, + }); + } + } +} diff --git a/src/transformer.ts b/src/transformer.ts new file mode 100644 index 000000000..a790aacdb --- /dev/null +++ b/src/transformer.ts @@ -0,0 +1,520 @@ +/** + * @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 + */ + +import * as ts from 'typescript'; + +import * as decorator from './decorator-annotator'; +import * as es5processor from './es5processor'; +import {ModulesManifest} from './modules_manifest'; +import {SourceMapper, SourcePosition} from './source_map_utils'; +import {isTypeNodeKind} from './ts_utils'; +import * as tsickle from './tsickle'; + +export interface TransformerOptions extends es5processor.Options, tsickle.Options { + /** + * Whether to downlevel decorators + */ + transformDecorators?: boolean; + /** + * Whether to convers types to closure + */ + transformTypesToClosure?: boolean; +} + +export interface TransformerHost extends es5processor.Host, tsickle.Host { + /** + * If true, tsickle and decorator downlevel processing will be skipped for + * that file. + */ + shouldSkipTsickleProcessing(fileName: string): boolean; + /** + * Tsickle treats warnings as errors, if true, ignore warnings. This might be + * useful for e.g. third party code. + */ + shouldIgnoreWarningsForPath(filePath: string): boolean; +} + +export interface EmitResult extends ts.EmitResult { + // The manifest of JS modules output by the compiler. + modulesManifest: ModulesManifest; + /** externs.js files produced by tsickle, if any. */ + externs: {[fileName: string]: string}; +} + +export interface EmitTransformers { + beforeTsickle?: Array>; + beforeTs?: Array>; + afterTs?: Array>; +} + +export function emitWithTsickle( + program: ts.Program, host: TransformerHost, options: TransformerOptions, + tsHost: ts.CompilerHost, tsOptions: ts.CompilerOptions, targetSourceFile?: ts.SourceFile, + writeFile?: ts.WriteFileCallback, cancellationToken?: ts.CancellationToken, + emitOnlyDtsFiles?: boolean, customTransformers?: EmitTransformers): EmitResult { + let tsickleDiagnostics: ts.Diagnostic[] = []; + const typeChecker = createOriginalNodeTypeChecker(program.getTypeChecker()); + const beforeTsTransformers: Array> = []; + if (options.transformTypesToClosure) { + // Note: tsickle.annotate can also lower decorators in the same run. + beforeTsTransformers.push(createTransformer(host, (sourceFile, sourceMapper) => { + const tisckleOptions: tsickle.Options = {...options, filterTypesForExport: true}; + const {output, diagnostics} = tsickle.annotate( + typeChecker, sourceFile, host, tisckleOptions, tsHost, tsOptions, sourceMapper); + tsickleDiagnostics.push(...diagnostics); + return output; + })); + } else if (options.transformDecorators) { + beforeTsTransformers.push(createTransformer(host, (sourceFile, sourceMapper) => { + const {output, diagnostics} = + decorator.convertDecorators(typeChecker, sourceFile, sourceMapper); + tsickleDiagnostics.push(...diagnostics); + return output; + })); + } + // // For debugging: transformer that just emits the same text. + // beforeTsTransformers.push(createTransformer(host, typeChecker, (sourceFile, sourceMapper) => { + // sourceMapper.addMapping(sourceFile, {position: 0, line: 0, column: 0}, {position: 0, line: 0, + // column: 0}, sourceFile.text.length); return sourceFile.text; + // })); + const afterTsTransformers: Array> = []; + if (customTransformers) { + if (customTransformers.beforeTsickle) { + beforeTsTransformers.unshift(...customTransformers.beforeTsickle); + } + + if (customTransformers.beforeTs) { + beforeTsTransformers.push(...customTransformers.beforeTs); + } + if (customTransformers.afterTs) { + afterTsTransformers.push(...customTransformers.afterTs); + } + } + let writeFileImpl = writeFile; + const modulesManifest = new ModulesManifest(); + if (options.googmodule) { + writeFileImpl = + (fileName: string, content: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + if (!tsickle.isDtsFileName(fileName) && !fileName.endsWith('.map')) { + content = es5processor.convertCommonJsToGoogModule( + host, options, modulesManifest, fileName, content); + } + if (writeFile) { + writeFile(fileName, content, writeByteOrderMark, onError, sourceFiles); + } else { + tsHost.writeFile(fileName, content, writeByteOrderMark, onError, sourceFiles); + } + }; + } + + const {diagnostics: tsDiagnostics, emitSkipped, emittedFiles} = program.emit( + targetSourceFile, writeFileImpl, cancellationToken, emitOnlyDtsFiles, + {before: beforeTsTransformers, after: afterTsTransformers}); + + const externs: {[fileName: string]: string} = {}; + // Note: we also need to collect externs on .d.ts files, + // so we can't do this in the ts transformer pipeline. + (targetSourceFile ? [targetSourceFile] : program.getSourceFiles()).forEach(sf => { + if (host.shouldSkipTsickleProcessing(sf.fileName)) { + return; + } + const {output, diagnostics} = tsickle.writeExterns(typeChecker, sf, host, options); + if (output) { + externs[sf.fileName] = output; + } + if (diagnostics) { + tsickleDiagnostics.push(...diagnostics); + } + }); + // All diagnostics (including warnings) are treated as errors. + // If we've decided to ignore them, just discard them. + // Warnings include stuff like "don't use @type in your jsdoc"; tsickle + // warns and then fixes up the code to be Closure-compatible anyway. + tsickleDiagnostics = tsickleDiagnostics.filter( + d => d.category === ts.DiagnosticCategory.Error || + !host.shouldIgnoreWarningsForPath(d.file.fileName)); + + return { + modulesManifest, + emitSkipped, + emittedFiles, + diagnostics: [...tsDiagnostics, ...tsickleDiagnostics], + externs + }; +} + +function createTransformer( + host: TransformerHost, + operator: (sourceFile: ts.SourceFile, sourceMapper: SourceMapper) => + string): ts.TransformerFactory { + return (context: ts.TransformationContext) => (sourceFile: ts.SourceFile): ts.SourceFile => { + if (host.shouldSkipTsickleProcessing(sourceFile.fileName)) { + return sourceFile; + } + const originalNodeByGeneratedRange = new Map(); + + const addFullNodeRange = (node: ts.Node, genStartPos: number) => { + originalNodeByGeneratedRange.set( + nodeCacheKey(node.kind, genStartPos, genStartPos + (node.getEnd() - node.getStart())), + node); + node.forEachChild( + child => addFullNodeRange(child, genStartPos + (child.getStart() - node.getStart()))); + }; + + const genStartPositions = new Map(); + const sourceMapper = { + addMapping: ( + originalNode: ts.Node, original: SourcePosition, generated: SourcePosition, + length: number) => { + let originalStartPos = original.position; + let genStartPos = generated.position; + if (originalStartPos >= originalNode.getFullStart() && + originalStartPos <= originalNode.getStart()) { + // always use the node.getStart() for the index, + // as comments and whitespaces might differ between the original and transformed code. + const diffToStart = originalNode.getStart() - originalStartPos; + originalStartPos += diffToStart; + genStartPos += diffToStart; + length -= diffToStart; + genStartPositions.set(originalNode, genStartPos); + } + if (originalStartPos + length === originalNode.getEnd()) { + originalNodeByGeneratedRange.set( + nodeCacheKey( + originalNode.kind, genStartPositions.get(originalNode)!, genStartPos + length), + originalNode); + } + originalNode.forEachChild((child) => { + if (child.getStart() >= originalStartPos && child.getEnd() <= originalStartPos + length) { + addFullNodeRange(child, genStartPos + (child.getStart() - originalStartPos)); + } + }); + } + }; + + const newFile = ts.createSourceFile( + sourceFile.fileName, operator(sourceFile, sourceMapper), ts.ScriptTarget.Latest, true); + const commentSynthesizer = new CommentSynthesizer(newFile); + let synthStmts = + commentSynthesizer.synthesizeDetachedLeadingComments(newFile, newFile.statements); + synthStmts = ts.setTextRange( + ts.createNodeArray( + ([] as ts.Statement[]) + .concat( + ...synthStmts.map( + stmt => convertToSyntheticNode( + context, stmt, sourceFile, newFile, originalNodeByGeneratedRange, + commentSynthesizer)) as ts.Statement[])), + synthStmts); + synthStmts = commentSynthesizer.synthesizeDetachedTrailingComments(newFile, synthStmts); + ts.setTextRange(synthStmts, {pos: -1, end: -1}); + + // Note: Need to clone the original file (and not use `ts.updateSourceFileNode`) + // as otherwise TS fails when resolving types for decorators. + const transformedFile = getMutableClone(sourceFile); + transformedFile.statements = synthStmts; + // Need to set parents as some of TypeScript's emit logic relies on it (e.g. emitting + // decorators) + synthStmts.forEach(stmt => setParentsForSyntheticNodes(stmt, transformedFile)); + return transformedFile; + }; +} + +function createNotEmittedStatement(sourceFile: ts.SourceFile) { + const stmt = ts.createNotEmittedStatement(sourceFile); + ts.setOriginalNode(stmt, undefined); + ts.setTextRange(stmt, {pos: 0, end: 0}); + return stmt; +} + +function getMutableClone(node: T): T { + const clone = ts.getMutableClone(node); + clone.flags &= ~ts.NodeFlags.Synthesized; + return clone; +} + +class CommentSynthesizer { + private lastCommentEnd = 0; + constructor(private sourceFile: ts.SourceFile) {} + + synthesizeLeadingComments(node: ts.Node): ts.Node { + if (node.kind === ts.SyntaxKind.Block) { + const block = node as ts.Block; + const newStmts = this.synthesizeDetachedLeadingComments(block, block.statements); + if (block.statements !== newStmts) { + return ts.updateBlock(block, newStmts); + } + } + + const parent = node.parent; + const adjustedNodeFullStart = Math.max(this.lastCommentEnd, node.getFullStart()); + const sharesStartWithParent = parent && parent.getFullStart() === adjustedNodeFullStart; + if (sharesStartWithParent) { + return node; + } + const trivia = this.sourceFile.text.substring(adjustedNodeFullStart, node.getStart()); + const leadingComments = ts.getLeadingCommentRanges(trivia, 0); + if (leadingComments && leadingComments.length) { + ts.setSyntheticLeadingComments(node, convertCommentRanges(leadingComments, trivia)); + this.lastCommentEnd = node.getStart(); + } + return node; + } + + synthesizeTrailingComments(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Block) { + const block = node as ts.Block; + const newStmts = this.synthesizeDetachedTrailingComments(block, block.statements); + if (block.statements !== newStmts) { + return ts.updateBlock(block, newStmts); + } + } + + const parent = node.parent; + const sharesEndWithParent = parent && parent.getEnd() === node.getEnd(); + if (sharesEndWithParent) { + return node; + } + const trailingComments = ts.getTrailingCommentRanges(this.sourceFile.text, node.getEnd()); + if (trailingComments && trailingComments.length) { + ts.setSyntheticTrailingComments( + node, convertCommentRanges(trailingComments, this.sourceFile.text)); + this.lastCommentEnd = trailingComments[trailingComments.length - 1].end; + } + return node; + } + + synthesizeDetachedLeadingComments(node: ts.Node, statements: ts.NodeArray): + ts.NodeArray { + if (!statements.length) { + return statements; + } + const trivia = this.sourceFile.text.substring(node.getFullStart(), node.getStart()); + const detachedComments = this.getDetachedStartingComments(node.getFullStart(), trivia); + if (!detachedComments.length) { + return statements; + } + const lastComment = detachedComments[detachedComments.length - 1]; + this.lastCommentEnd = lastComment.end; + const commentStmt = createNotEmittedStatement(this.sourceFile); + ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue); + ts.setSyntheticTrailingComments(commentStmt, convertCommentRanges(detachedComments, trivia)); + return ts.setTextRange(ts.createNodeArray([commentStmt, ...statements]), statements); + } + + synthesizeDetachedTrailingComments(node: ts.Node, statements: ts.NodeArray): + ts.NodeArray { + const trailingTrivia = this.sourceFile.text.substring(statements.end, node.end); + const detachedComments = ts.getLeadingCommentRanges(trailingTrivia, 0); + if (!detachedComments || !detachedComments.length) { + return statements; + } + const lastComment = detachedComments[detachedComments.length - 1]; + this.lastCommentEnd = lastComment.end; + const commentStmt = createNotEmittedStatement(this.sourceFile); + ts.setEmitFlags(commentStmt, ts.EmitFlags.CustomPrologue); + ts.setSyntheticLeadingComments( + commentStmt, convertCommentRanges(detachedComments, trailingTrivia)); + return ts.setTextRange(ts.createNodeArray([...statements, commentStmt]), statements); + } + + // Adapted from compiler/comments.ts in TypeScript + private getDetachedStartingComments(fullStart: number, trivia: string): ts.CommentRange[] { + const leadingComments = ts.getLeadingCommentRanges(trivia, 0); + if (!leadingComments || !leadingComments.length) { + return []; + } + const detachedComments: ts.CommentRange[] = []; + let lastComment: ts.CommentRange|undefined = undefined; + + for (const comment of leadingComments) { + if (lastComment) { + const lastCommentLine = this.getLineOfPos(fullStart + lastComment.end); + const commentLine = this.getLineOfPos(fullStart + comment.pos); + + if (commentLine >= lastCommentLine + 2) { + // There was a blank line between the last comment and this comment. This + // comment is not part of the copyright comments. Return what we have so + // far. + break; + } + } + + detachedComments.push(comment); + lastComment = comment; + } + + if (detachedComments.length) { + // All comments look like they could have been part of the copyright header. Make + // sure there is at least one blank line between it and the node. If not, it's not + // a copyright header. + const lastCommentLine = + this.getLineOfPos(fullStart + detachedComments[detachedComments.length - 1].end); + const nodeLine = this.getLineOfPos(fullStart + trivia.length); + if (nodeLine >= lastCommentLine + 2) { + // Valid detachedComments + return detachedComments; + } + } + return []; + } + + getLineOfPos(pos: number): number { + return ts.getLineAndCharacterOfPosition(this.sourceFile, pos).line; + } +} + +function convertCommentRanges(parsedComments: ts.CommentRange[], text: string) { + return parsedComments.map(({kind, pos, end, hasTrailingNewLine}, commentIdx) => { + let commentText = text.substring(pos, end).trim(); + if (kind === ts.SyntaxKind.MultiLineCommentTrivia) { + commentText = commentText.replace(/(^\/\*)|(\*\/$)/g, ''); + } else if (kind === ts.SyntaxKind.SingleLineCommentTrivia) { + commentText = commentText.replace(/(^\/\/)/g, ''); + } + const comment: + ts.SynthesizedComment = {kind, text: commentText, hasTrailingNewLine, pos: -1, end: -1}; + return comment; + }); +} + +function convertToSyntheticNode( + context: ts.TransformationContext, node: ts.Node, originalSourceFile: ts.SourceFile, + sourceFile: ts.SourceFile, originalNodeByGeneratedRange: Map, + commentSynthesizer: CommentSynthesizer, mayUseOriginalNodes = true): ts.Node|ts.Node[] { + if (node.flags & ts.NodeFlags.Synthesized) { + return node; + } + const fullStart = node.getFullStart(); + const start = fullStart === -1 ? -1 : node.getStart(); + const end = node.getEnd(); + const originalNode = originalNodeByGeneratedRange.get(nodeCacheKey(node.kind, start, end)); + // Ignore types as they are not printed anyways, + // and they lead to problems with suspended lexical environments. + if (isTypeNodeKind(node.kind)) { + return originalNode || node; + } + + // For nodes that were fully mapped, use the original node instead. + if (mayUseOriginalNodes && originalNode && + sourceFile.text.substring(fullStart, end) === + originalSourceFile.text.substring(originalNode.getFullStart(), originalNode.getEnd())) { + return originalNode; + } + + // Never use original nodes for parts of a modified ExportDeclaration, + // as otherwise we get an error when TS tries to analyze it... + mayUseOriginalNodes = mayUseOriginalNodes && node.kind !== ts.SyntaxKind.ExportDeclaration; + node = commentSynthesizer.synthesizeLeadingComments(node); + node = ts.visitEachChild( + node, + child => convertToSyntheticNode( + context, child, originalSourceFile, sourceFile, originalNodeByGeneratedRange, + commentSynthesizer, mayUseOriginalNodes), + context); + node = commentSynthesizer.synthesizeTrailingComments(node); + node.flags |= ts.NodeFlags.Synthesized; + node.parent = undefined; + ts.setTextRange(node, {pos: -1, end: -1}); + // reset the pos/end of all NodeArrays + // tslint:disable-next-line:no-any + const nodeAny = node as {[prop: string]: any}; + for (const prop in nodeAny) { + if (!nodeAny.hasOwnProperty(prop)) { + continue; + } + const value = nodeAny[prop]; + if (isNodeArray(value)) { + ts.setTextRange(value, {pos: -1, end: -1}); + } + } + if (originalNode) { + ts.setOriginalNode(node, originalNode); + ts.setSourceMapRange(node, originalNode); + // Needed so that e.g. `module { ... }` prints the variable statement + // before the closure. + // tslint:disable-next-line:no-any + (node as any).symbol = (originalNode as any).symbol; + } + if (node.kind === ts.SyntaxKind.VariableStatement) { + if (node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) { + // Note: TypeScript does not emit synthetic comments on exported variable statements, + // so we have to create a fake statement to hold the comments. + // Note: We cannot use the trick via `ts.setTextRange` that we use for export / import + // declarations, as somehow TypeScript would still skip some comments. + const synthComments = ts.getSyntheticLeadingComments(node); + if (synthComments && synthComments.length) { + ts.setSyntheticLeadingComments(node, []); + const commentStmt = createNotEmittedStatement(originalSourceFile); + ts.setSyntheticLeadingComments(commentStmt, synthComments); + return [commentStmt, node]; + } + } + } + + if (node.kind === ts.SyntaxKind.ExportDeclaration || + node.kind === ts.SyntaxKind.ImportDeclaration) { + if (originalNode) { + // Note: TypeScript does not emit synthetic comments on import / export declarations. + // As tsickle never modifies comments of export / import declarations, + // we can just set the original text range and let typescript reuse the original comments. + // This is simpler than emitting a fake statement with the synthesized comments, + // as in that case we would need to calculate whether the export / import statement + // will be elided, which is far from trivial (especially for import statements). + ts.setTextRange(node, {pos: originalNode.getFullStart(), end: originalNode.getEnd()}); + + // Note: Somewhow, TypeScript does not write exports correctly if we + // have an original node for an export declaration. So we reset it to undefined. + // This still allows TypeScript to elided exports as it will actually check + // the values in the exportClause instead. + if (node.kind === ts.SyntaxKind.ExportDeclaration) { + ts.setOriginalNode(node, undefined); + } + } + } + return node; +} + +function setParentsForSyntheticNodes(node: ts.Node, parent: ts.Node|undefined) { + if (!(node.flags & ts.NodeFlags.Synthesized)) { + return; + } + node.parent = parent; + node.forEachChild(child => { + setParentsForSyntheticNodes(child, node); + }); +} + +function createOriginalNodeTypeChecker(tc: ts.TypeChecker): ts.TypeChecker { + const result = Object.create(tc); + result.getTypeAtLocation = (node: ts.Node) => tc.getTypeAtLocation(ts.getOriginalNode(node)); + result.getSymbolAtLocation = (node: ts.Node) => tc.getSymbolAtLocation(ts.getOriginalNode(node)); + result.getSignatureFromDeclaration = (declaration: ts.SignatureDeclaration) => + tc.getSignatureFromDeclaration(ts.getOriginalNode(declaration) as ts.SignatureDeclaration); + result.getSymbolsInScope = (location: ts.Node, meaning: ts.SymbolFlags) => + tc.getSymbolsInScope(ts.getOriginalNode(location), meaning); + result.getConstantValue = + (node: ts.EnumMember | ts.PropertyAccessExpression | ts.ElementAccessExpression) => + tc.getConstantValue( + ts.getOriginalNode(node) as ts.EnumMember | ts.PropertyAccessExpression | + ts.ElementAccessExpression); + result.getTypeOfSymbolAtLocation = (symbol: ts.Symbol, node: ts.Node) => + tc.getTypeOfSymbolAtLocation(symbol, ts.getOriginalNode(node)); + return result; +} + +function nodeCacheKey(kind: ts.SyntaxKind, start: number, end: number): string { + return `${kind}#${start}#${end}`; +} + +// tslint:disable-next-line:no-any +function isNodeArray(value: any): value is ts.NodeArray { + const anyValue = value; + return Array.isArray(value) && anyValue.pos !== undefined && anyValue.end !== undefined; +} diff --git a/src/ts_utils.ts b/src/ts_utils.ts new file mode 100644 index 000000000..f01aba8e0 --- /dev/null +++ b/src/ts_utils.ts @@ -0,0 +1,20 @@ +/** + * @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 + */ + +import * as ts from 'typescript'; + +// Copied from TypeScript +export function isTypeNodeKind(kind: ts.SyntaxKind) { + return (kind >= ts.SyntaxKind.FirstTypeNode && kind <= ts.SyntaxKind.LastTypeNode) || + kind === ts.SyntaxKind.AnyKeyword || kind === ts.SyntaxKind.NumberKeyword || + kind === ts.SyntaxKind.ObjectKeyword || kind === ts.SyntaxKind.BooleanKeyword || + kind === ts.SyntaxKind.StringKeyword || kind === ts.SyntaxKind.SymbolKeyword || + kind === ts.SyntaxKind.ThisKeyword || kind === ts.SyntaxKind.VoidKeyword || + kind === ts.SyntaxKind.UndefinedKeyword || kind === ts.SyntaxKind.NullKeyword || + kind === ts.SyntaxKind.NeverKeyword || kind === ts.SyntaxKind.ExpressionWithTypeArguments; +} diff --git a/src/tsickle.ts b/src/tsickle.ts index 1149ebefe..b479c7ed2 100644 --- a/src/tsickle.ts +++ b/src/tsickle.ts @@ -7,31 +7,55 @@ */ import * as path from 'path'; -import {SourceMapGenerator} from 'source-map'; import * as ts from 'typescript'; +import {DecoratorClassVisitor, visitClassContent} from './decorator-annotator'; import {hasExportingDecorator} from './decorators'; import {extractGoogNamespaceImport} from './es5processor'; import * as jsdoc from './jsdoc'; import {getIdentifierText, Rewriter, unescapeName} from './rewriter'; -import {Options} from './tsickle_compiler_host'; +import {SourceMapper} from './source_map_utils'; import * as typeTranslator from './type-translator'; import {toArray} from './util'; export {convertDecorators} from './decorator-annotator'; -export {processES5} from './es5processor'; export {FileMap, ModulesManifest} from './modules_manifest'; -export {Options, Pass, TsickleCompilerHost, TsickleHost} from './tsickle_compiler_host'; - -export interface Output { - /** The TypeScript source with Closure annotations inserted. */ - output: string; - /** Generated externs declarations, if any. */ - externs: string|null; - /** Error messages, if any. */ - diagnostics: ts.Diagnostic[]; - /** A source map mapping back into the original sources. */ - sourceMap: SourceMapGenerator; + +export interface Host { + /** + * If provided a function that logs an internal warning. + * These warnings are not actionable by an end user and should be hidden + * by default. + */ + logWarning?: (warning: ts.Diagnostic) => void; + pathToModuleName: (context: string, importPath: string) => string; +} + +export interface Options { + googmodule?: boolean; + /** + * If true, convert every type to the Closure {?} type, which means + * "don't check types". + */ + untyped?: boolean; + /** If provided, a set of paths whose types should always generate as {?}. */ + typeBlackListPaths?: Set; + /** + * Convert shorthand "/index" imports to full path (include the "/index"). + * Annotation will be slower because every import must be resolved. + */ + convertIndexImportShorthand?: boolean; + /** + * Whether to downlevel decorators as well. + */ + transformDecorators?: boolean; + /** + * Whether to filter out exports for types when expanding an `export * ...`. + * Needed e.g. for transformer mode, as + * the typechecker has no information of the new symbols, + * and therefore typescript won't automatically remove them. + */ + filterTypesForExport?: boolean; } /** @@ -82,7 +106,7 @@ export function formatDiagnostics(diags: ts.Diagnostic[]): string { } /** @return true if node has the specified modifier flag set. */ -function hasModifierFlag(node: ts.Node, flag: ts.ModifierFlags): boolean { +export function hasModifierFlag(node: ts.Node, flag: ts.ModifierFlags): boolean { return (ts.getCombinedModifierFlags(node) & flag) !== 0; } @@ -161,8 +185,10 @@ class ClosureRewriter extends Rewriter { */ symbolsToAliasedNames = new Map(); - constructor(protected program: ts.Program, file: ts.SourceFile, protected options: Options) { - super(file); + constructor( + protected typeChecker: ts.TypeChecker, protected host: Host, protected options: Options, + file: ts.SourceFile, sourceMapper?: SourceMapper) { + super(file, sourceMapper); } /** @@ -177,7 +203,7 @@ class ClosureRewriter extends Rewriter { * function statement; for overloads, name will have been merged. */ emitFunctionType(fnDecls: ts.SignatureDeclaration[], extraTags: jsdoc.Tag[] = []): string[] { - const typeChecker = this.program.getTypeChecker(); + const typeChecker = this.typeChecker; const newDoc = extraTags; const lens = fnDecls.map(fnDecl => fnDecl.parameters.length); const minArgsCount = Math.min(...lens); @@ -376,7 +402,7 @@ class ClosureRewriter extends Rewriter { // We can only @implements an interface, not a class. // But it's fine to translate TS "implements Class" into Closure // "@extends {Class}" because this is just a type hint. - const typeChecker = this.program.getTypeChecker(); + const typeChecker = this.typeChecker; let sym = typeChecker.getSymbolAtLocation(impl.expression); if (sym.flags & ts.SymbolFlags.TypeAlias) { // It's implementing a type alias. Follow the type alias back @@ -431,7 +457,7 @@ class ClosureRewriter extends Rewriter { return '?'; } - const typeChecker = this.program.getTypeChecker(); + const typeChecker = this.typeChecker; if (!type) { type = typeChecker.getTypeAtLocation(context); } @@ -440,7 +466,7 @@ class ClosureRewriter extends Rewriter { newTypeTranslator(context: ts.Node) { const translator = new typeTranslator.TypeTranslator( - this.program.getTypeChecker(), context, this.options.typeBlackListPaths, + this.typeChecker, ts.getOriginalNode(context), this.options.typeBlackListPaths, this.symbolsToAliasedNames); translator.warn = msg => this.debugWarn(context, msg); return translator; @@ -454,7 +480,7 @@ class ClosureRewriter extends Rewriter { * for tsickle to debug itself. */ debugWarn(node: ts.Node, messageText: string) { - if (!this.options.logWarning) return; + if (!this.host.logWarning) return; // Use a ts.Diagnosic so that the warning includes context and file offets. const diagnostic: ts.Diagnostic = { file: this.file, @@ -464,7 +490,7 @@ class ClosureRewriter extends Rewriter { category: ts.DiagnosticCategory.Warning, code: 0, }; - this.options.logWarning(diagnostic); + this.host.logWarning(diagnostic); } } @@ -479,40 +505,21 @@ const FILEOVERVIEW_COMMENTS: ReadonlySet = /** Annotator translates a .ts to a .ts with Closure annotations. */ class Annotator extends ClosureRewriter { - /** - * Generated externs, if any. Any "declare" blocks encountered in the source - * are forwarded to the ExternsWriter to be translated into externs. - */ - private externsWriter: ExternsWriter; - /** Exported symbol names that have been generated by expanding an "export * from ...". */ private generatedExports = new Set(); - - private typeChecker: ts.TypeChecker; + /** ComposableDecoratorRewriter when using tsickle as a TS transformer */ + private currentDecoratorConverter: DecoratorClassVisitor|undefined; constructor( - program: ts.Program, file: ts.SourceFile, options: Options, - private pathToModuleName: (context: string, importPath: string) => string, - private host?: ts.ModuleResolutionHost, private tsOpts?: ts.CompilerOptions) { - super(program, file, options); - this.externsWriter = new ExternsWriter(program, file, options); - this.typeChecker = program.getTypeChecker(); + typeChecker: ts.TypeChecker, host: Host, options: Options, file: ts.SourceFile, + sourceMapper?: SourceMapper, private tsHost?: ts.ModuleResolutionHost, + private tsOpts?: ts.CompilerOptions) { + super(typeChecker, host, options, file, sourceMapper); } - annotate(): Output { + annotate(): {output: string, diagnostics: ts.Diagnostic[]} { this.visit(this.file); - - const annotated = this.getOutput(); - - const externs = this.externsWriter.getOutput(); - const externsSource = externs.output.length > 0 ? externs.output : null; - - return { - output: annotated.output, - externs: externsSource, - diagnostics: externs.diagnostics.concat(annotated.diagnostics), - sourceMap: annotated.sourceMap, - }; + return this.getOutput(); } getExportDeclarationNames(node: ts.Node): ts.Identifier[] { @@ -582,15 +589,16 @@ class Annotator extends ClosureRewriter { */ maybeProcess(node: ts.Node): boolean { if (hasModifierFlag(node, ts.ModifierFlags.Ambient) || isDtsFileName(this.file.fileName)) { - this.externsWriter.visit(node); // An ambient declaration declares types for TypeScript's benefit, so we want to skip Tsickle // conversion of its contents. - this.writeRange(node.getFullStart(), node.getEnd()); + this.writeRange(node, node.getFullStart(), node.getEnd()); // ... but it might need to be exported for downstream importing code. this.maybeEmitAmbientDeclarationExport(node); return true; } - + if (this.currentDecoratorConverter) { + this.currentDecoratorConverter.beforeProcessNode(node); + } switch (node.kind) { case ts.SyntaxKind.SourceFile: this.handleSourceFile(node as ts.SourceFile); @@ -599,14 +607,16 @@ class Annotator extends ClosureRewriter { return this.emitImportDeclaration(node as ts.ImportDeclaration); case ts.SyntaxKind.ExportDeclaration: const exportDecl = node as ts.ExportDeclaration; - this.writeRange(node.getFullStart(), node.getStart()); + this.writeRange(node, node.getFullStart(), node.getStart()); this.emit('export'); let exportedSymbols: NamedSymbol[] = []; if (!exportDecl.exportClause && exportDecl.moduleSpecifier) { // It's an "export * from ..." statement. // Rewrite it to re-export each exported symbol directly. exportedSymbols = this.expandSymbolsFromExportStar(exportDecl); - this.emit(` {${exportedSymbols.map(e => unescapeName(e.name)).join(',')}}`); + const exportSymbolsToEmit = + exportedSymbols.filter(s => this.shouldEmitExportSymbol(s.sym)); + this.emit(` {${exportSymbolsToEmit.map(e => unescapeName(e.name)).join(',')}}`); } else { if (exportDecl.exportClause) { exportedSymbols = this.getNamedSymbols(exportDecl.exportClause.elements); @@ -615,11 +625,14 @@ class Annotator extends ClosureRewriter { } if (exportDecl.moduleSpecifier) { this.emit(` from '${this.resolveModuleSpecifier(exportDecl.moduleSpecifier)}';`); - this.forwardDeclare(exportDecl.moduleSpecifier, exportedSymbols); } else { // export {...}; this.emit(';'); } + this.writeRange(node, node.getEnd(), node.getEnd()); + if (exportDecl.moduleSpecifier) { + this.forwardDeclare(exportDecl.moduleSpecifier, exportedSymbols); + } if (exportedSymbols.length) { this.emitTypeDefExports(exportedSymbols); } @@ -627,7 +640,7 @@ class Annotator extends ClosureRewriter { case ts.SyntaxKind.InterfaceDeclaration: this.emitInterface(node as ts.InterfaceDeclaration); // Emit the TS interface verbatim, with no tsickle processing of properties. - this.writeRange(node.getFullStart(), node.getEnd()); + this.writeRange(node, node.getFullStart(), node.getEnd()); return true; case ts.SyntaxKind.VariableDeclaration: const varDecl = node as ts.VariableDeclaration; @@ -662,12 +675,12 @@ class Annotator extends ClosureRewriter { let offset = ctor.getStart(); if (ctor.parameters.length) { for (const param of ctor.parameters) { - this.writeRange(offset, param.getFullStart()); + this.writeRange(node, offset, param.getFullStart()); this.visit(param); offset = param.getEnd(); } } - this.writeRange(offset, node.getEnd()); + this.writeRange(node, offset, node.getEnd()); return true; case ts.SyntaxKind.ArrowFunction: // It's difficult to annotate arrow functions due to a bug in @@ -689,7 +702,7 @@ class Annotator extends ClosureRewriter { } this.emitFunctionType([fnDecl], tags); - this.writeRange(fnDecl.getStart(), fnDecl.body.getFullStart()); + this.writeRange(fnDecl, fnDecl.getStart(), fnDecl.body.getFullStart()); this.visit(fnDecl.body); return true; case ts.SyntaxKind.TypeAliasDeclaration: @@ -740,7 +753,7 @@ class Annotator extends ClosureRewriter { if (docTags.length > 0 && node.getFirstToken()) { this.emit('\n'); this.emit(jsdoc.toString(docTags)); - this.writeRange(node.getFirstToken().getStart(), node.getEnd()); + this.writeRange(node, node.getFirstToken().getStart(), node.getEnd()); return true; } break; @@ -801,21 +814,43 @@ class Annotator extends ClosureRewriter { ` has a string index type but is accessed using dotted access. ` + `Quoting the access.`); this.writeNode(pae.expression); - this.emit(`['${getIdentifierText(pae.name)}']`); + this.emit('["'); + this.writeNode(pae.name); + this.emit('"]'); return true; + case ts.SyntaxKind.Decorator: + return !!this.currentDecoratorConverter && + this.currentDecoratorConverter.maybeProcessDecorator(node); default: break; } return false; } + private shouldEmitExportSymbol(sym: ts.Symbol): boolean { + if (!this.options.filterTypesForExport) { + return true; + } + if (sym.flags & ts.SymbolFlags.Alias) { + sym = this.typeChecker.getAliasedSymbol(sym); + } + if ((sym.flags & ts.SymbolFlags.Value) === 0) { + // Note: We create explicit reexports via closure at another place in tsickle. + return false; + } + if (sym.flags & ts.SymbolFlags.ConstEnum) { + return false; + } + return true; + } + private handleSourceFile(sf: ts.SourceFile) { this.emitSuppressChecktypes(sf); for (const stmt of sf.statements) { this.visit(stmt); } if (sf.statements.length) { - this.writeRange(sf.statements[sf.statements.length - 1].getEnd(), sf.getEnd()); + this.writeRange(sf, sf.statements[sf.statements.length - 1].getEnd(), sf.getEnd()); } } @@ -849,7 +884,7 @@ class Annotator extends ClosureRewriter { return; } const comment = comments[fileoverviewIdx]; - this.writeRange(0, comment.pos); + this.writeRange(sf, 0, comment.pos); this.skipUpToOffset = comment.end; const parsed = jsdoc.parse(sf.getFullText().substring(comment.pos, comment.end)); @@ -951,8 +986,16 @@ class Annotator extends ClosureRewriter { for (const exp of exports) { if (exp.sym.flags & ts.SymbolFlags.Alias) exp.sym = this.typeChecker.getAliasedSymbol(exp.sym); - const isTypeAlias = (exp.sym.flags & ts.SymbolFlags.TypeAlias) !== 0 && + let isTypeAlias = (exp.sym.flags & ts.SymbolFlags.TypeAlias) !== 0 && (exp.sym.flags & ts.SymbolFlags.Value) === 0; + if (this.options.filterTypesForExport) { + // If we are in the transformer pipeline, + // we don't know that tsickle converted interfaces into functions, + // so we have to manually reexport them as well. + isTypeAlias = isTypeAlias || + (exp.sym.flags & ts.SymbolFlags.Interface) !== 0 && + (exp.sym.flags & ts.SymbolFlags.Value) === 0; + } if (!isTypeAlias) continue; const typeName = this.symbolsToAliasedNames.get(exp.sym) || exp.sym.name; this.emit(`\n/** @typedef {${typeName}} */\nexports.${exp.name}; // re-export typedef`); @@ -970,11 +1013,11 @@ class Annotator extends ClosureRewriter { } let moduleId = (moduleSpecifier as ts.StringLiteral).text; if (this.options.convertIndexImportShorthand) { - if (!this.tsOpts || !this.host) { + if (!this.tsOpts || !this.tsHost) { throw new Error( 'option convertIndexImportShorthand requires that annotate be called with a TypeScript host and options.'); } - const resolved = ts.resolveModuleName(moduleId, this.file.fileName, this.tsOpts, this.host); + const resolved = ts.resolveModuleName(moduleId, this.file.fileName, this.tsOpts, this.tsHost); if (resolved && resolved.resolvedModule) { const requestedModule = moduleId.replace(extension, ''); const resolvedModule = resolved.resolvedModule.resolvedFileName.replace(extension, ''); @@ -997,13 +1040,14 @@ class Annotator extends ClosureRewriter { * @return true if the decl was handled, false to allow default processing. */ private emitImportDeclaration(decl: ts.ImportDeclaration): boolean { - this.writeRange(decl.getFullStart(), decl.getStart()); + this.writeRange(decl, decl.getFullStart(), decl.getStart()); this.emit('import'); const importPath = this.resolveModuleSpecifier(decl.moduleSpecifier); const importClause = decl.importClause; if (!importClause) { // import './foo'; this.emit(`'${importPath}';`); + this.writeRange(decl, decl.getEnd(), decl.getEnd()); return true; } else if ( importClause.name || @@ -1011,6 +1055,7 @@ class Annotator extends ClosureRewriter { importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) { this.visit(importClause); this.emit(` from '${importPath}';`); + this.writeRange(decl, decl.getEnd(), decl.getEnd()); // importClause.name implies // import a from ...; @@ -1049,6 +1094,7 @@ class Annotator extends ClosureRewriter { // import * as foo from ...; this.visit(importClause); this.emit(` from '${importPath}';`); + this.writeRange(decl, decl.getEnd(), decl.getEnd()); return true; } else { this.errorUnimplementedKind(decl, 'unexpected kind of import'); @@ -1080,7 +1126,7 @@ class Annotator extends ClosureRewriter { const nsImport = extractGoogNamespaceImport(importPath); const forwardDeclarePrefix = `tsickle_forward_declare_${++this.forwardDeclareCounter}`; const moduleNamespace = - nsImport !== null ? nsImport : this.pathToModuleName(this.file.fileName, importPath); + nsImport !== null ? nsImport : this.host.pathToModuleName(this.file.fileName, importPath); const exports = this.typeChecker.getExportsOfModule(this.typeChecker.getSymbolAtLocation(specifier)); // In TypeScript, importing a module for use in a type annotation does not cause a runtime load. @@ -1088,7 +1134,7 @@ class Annotator extends ClosureRewriter { // here would cause a change in load order, which is observable (and can lead to errors). // Instead, goog.forwardDeclare types, which allows using them in type annotations without // causing a load. See below for the exception to the rule. - this.emit(`\nconst ${forwardDeclarePrefix} = goog.forwardDeclare('${moduleNamespace}');`); + this.emit(`\nconst ${forwardDeclarePrefix} = goog.forwardDeclare("${moduleNamespace}");`); const hasValues = exports.some(e => (e.flags & ts.SymbolFlags.Value) !== 0); if (!hasValues) { // Closure Compiler's toolchain will drop files that are never goog.require'd *before* type @@ -1101,7 +1147,7 @@ class Annotator extends ClosureRewriter { // This is a heuristic - if the module exports some values, but those are never imported, // the file will still end up not being imported. Hopefully modules that export values are // imported for their value in some place. - this.emit(`\ngoog.require('${moduleNamespace}'); // force type-only module to be loaded`); + this.emit(`\ngoog.require("${moduleNamespace}"); // force type-only module to be loaded`); } for (const exp of exportedSymbols) { if (exp.sym.flags & ts.SymbolFlags.Alias) @@ -1114,6 +1160,12 @@ class Annotator extends ClosureRewriter { } private visitClassDeclaration(classDecl: ts.ClassDeclaration) { + this.writeRange(classDecl, classDecl.getFullStart(), classDecl.getFullStart()); + const oldDecoratorConverter = this.currentDecoratorConverter; + if (this.options.transformDecorators) { + this.currentDecoratorConverter = new DecoratorClassVisitor(this.typeChecker, this, classDecl); + } + const docTags = this.getJSDoc(classDecl) || []; if (hasModifierFlag(classDecl, ts.ModifierFlags.Abstract)) { docTags.push({tagName: 'abstract'}); @@ -1126,19 +1178,11 @@ class Annotator extends ClosureRewriter { this.emit('\n'); if (docTags.length > 0) this.emit(jsdoc.toString(docTags)); - if (classDecl.members.length > 0) { - // We must visit all members individually, to strip out any - // /** @export */ annotations that show up in the constructor - // and to annotate methods. - this.writeRange(classDecl.getStart(), classDecl.members[0].getFullStart()); - for (const member of classDecl.members) { - this.visit(member); - } - } else { - this.writeRange(classDecl.getStart(), classDecl.getLastToken().getFullStart()); - } - this.writeNode(classDecl.getLastToken()); + visitClassContent(classDecl, this, this.currentDecoratorConverter); + this.writeRange(classDecl, classDecl.getEnd(), classDecl.getEnd()); this.emitTypeAnnotationsHelper(classDecl); + + this.currentDecoratorConverter = oldDecoratorConverter; return true; } @@ -1219,6 +1263,9 @@ class Annotator extends ClosureRewriter { const className = getIdentifierText(classDecl.name); this.emit(`\n\nfunction ${className}_tsickle_Closure_declarations() {\n`); + if (this.currentDecoratorConverter) { + this.currentDecoratorConverter.emitMetadataTypeAnnotationsHelpers(); + } staticProps.forEach(p => this.visitProperty([className], p)); const memberNamespace = [className, 'prototype']; nonStaticProps.forEach((p) => this.visitProperty(memberNamespace, p)); @@ -1350,11 +1397,12 @@ class Annotator extends ClosureRewriter { // both a typedef and an indexable object if we export it. this.emit('\n'); const name = node.name.getText(); - const isExported = hasModifierFlag(node, ts.ModifierFlags.Export); - if (isExported) this.emit('export '); this.emit(`type ${name} = number;\n`); - if (isExported) this.emit('export '); this.emit(`let ${name}: any = {};\n`); + const isExported = hasModifierFlag(node, ts.ModifierFlags.Export); + if (isExported) { + this.emit(`export {${name}};\n`); + } // Emit foo.BAR = 0; lines. for (const member of toArray(members.keys())) { @@ -1380,6 +1428,18 @@ class Annotator extends ClosureRewriter { /** ExternsWriter generates Closure externs from TypeScript source. */ class ExternsWriter extends ClosureRewriter { + process(): {output: string, diagnostics: ts.Diagnostic[]} { + this.findExternRoots().forEach(node => this.visit(node)); + return this.getOutput(); + } + + private findExternRoots(): ts.Node[] { + if (isDtsFileName(this.file.fileName)) { + return [this.file]; + } + return this.file.statements.filter(stmt => hasModifierFlag(stmt, ts.ModifierFlags.Ambient)); + } + /** visit is the main entry point. It generates externs from a ts.Node. */ public visit(node: ts.Node, namespace: string[] = []) { switch (node.kind) { @@ -1454,11 +1514,11 @@ class ExternsWriter extends ClosureRewriter { break; } // Gather up all overloads of this function. - const sym = this.program.getTypeChecker().getSymbolAtLocation(name); + const sym = this.typeChecker.getSymbolAtLocation(name); const decls = sym.declarations!.filter(d => d.kind === ts.SyntaxKind.FunctionDeclaration) as ts.FunctionDeclaration[]; // Only emit the first declaration of each overloaded function. - if (fnDecl !== decls[0]) break; + if (ts.getOriginalNode(fnDecl) !== decls[0]) break; const params = this.emitFunctionType(decls); this.writeExternsFunction(name.getText(), params, namespace); break; @@ -1488,10 +1548,10 @@ class ExternsWriter extends ClosureRewriter { */ private isFirstDeclaration(decl: ts.DeclarationStatement): boolean { if (!decl.name) return true; - const typeChecker = this.program.getTypeChecker(); + const typeChecker = this.typeChecker; const sym = typeChecker.getSymbolAtLocation(decl.name); if (!sym.declarations || sym.declarations.length < 2) return true; - return decl === sym.declarations[0]; + return ts.getOriginalNode(decl) === sym.declarations[0]; } private writeExternsType(decl: ts.InterfaceDeclaration|ts.ClassDeclaration, namespace: string[]) { @@ -1654,9 +1714,26 @@ class ExternsWriter extends ClosureRewriter { } export function annotate( - program: ts.Program, file: ts.SourceFile, - pathToModuleName: (context: string, importPath: string) => string, options: Options = {}, - host?: ts.ModuleResolutionHost, tsOpts?: ts.CompilerOptions): Output { + typeChecker: ts.TypeChecker, file: ts.SourceFile, host: Host, options: Options = {}, + tsHost?: ts.ModuleResolutionHost, tsOpts?: ts.CompilerOptions, + sourceMapper?: SourceMapper): {output: string, diagnostics: ts.Diagnostic[]} { + typeTranslator.assertTypeChecked(file); + return new Annotator(typeChecker, host, options, file, sourceMapper, tsHost, tsOpts).annotate(); +} + +export function writeExterns( + typeChecker: ts.TypeChecker, file: ts.SourceFile, host: Host, + options: Options = {}): {output: string, diagnostics: ts.Diagnostic[]} { typeTranslator.assertTypeChecked(file); - return new Annotator(program, file, options, pathToModuleName, host, tsOpts).annotate(); + return new ExternsWriter(typeChecker, host, options, file).process(); +} + +/** Concatenate all generated externs definitions together into a string. */ +export function getGeneratedExterns(externs: {[fileName: string]: string}): string { + let allExterns = EXTERNS_HEADER; + for (const fileName of Object.keys(externs)) { + allExterns += `// externs from ${fileName}:\n`; + allExterns += externs[fileName]; + } + return allExterns; } diff --git a/src/tsickle_compiler_host.ts b/src/tsickle_compiler_host.ts index 28c323174..e0dda860c 100644 --- a/src/tsickle_compiler_host.ts +++ b/src/tsickle_compiler_host.ts @@ -11,7 +11,7 @@ import {SourceMapGenerator} from 'source-map'; import * as ts from 'typescript'; import {convertDecorators} from './decorator-annotator'; -import {processES5} from './es5processor'; +import * as es5processor from './es5processor'; import {ModulesManifest} from './modules_manifest'; import * as sourceMapUtils from './source_map_utils'; import * as tsickle from './tsickle'; @@ -30,56 +30,27 @@ export enum Pass { CLOSURIZE } -export interface Options { - googmodule?: boolean; - es5Mode?: boolean; - prelude?: string; - /** - * If true, convert every type to the Closure {?} type, which means - * "don't check types". - */ - untyped?: boolean; - /** - * If provided a function that logs an internal warning. - * These warnings are not actionable by an end user and should be hidden - * by default. - */ - logWarning?: (warning: ts.Diagnostic) => void; - /** If provided, a set of paths whose types should always generate as {?}. */ - typeBlackListPaths?: Set; - /** - * Convert shorthand "/index" imports to full path (include the "/index"). - * Annotation will be slower because every import must be resolved. - */ - convertIndexImportShorthand?: boolean; +export interface Options extends es5processor.Options, tsickle.Options { + // This method here for backwards compatibility. Don't use it anymore, + // but rather use the corresponding method in the TsickleHost. + logWarning?: TsickleHost['logWarning']; } /** * Provides hooks to customize TsickleCompilerHost's behavior for different * compilation environments. */ -export interface TsickleHost { +export interface TsickleHost extends es5processor.Host, tsickle.Host { /** * If true, tsickle and decorator downlevel processing will be skipped for * that file. */ shouldSkipTsickleProcessing(fileName: string): boolean; - /** - * Takes a context (the current file) and the path of the file to import - * and generates a googmodule module name - */ - pathToModuleName(context: string, importPath: string): string; /** * Tsickle treats warnings as errors, if true, ignore warnings. This might be * useful for e.g. third party code. */ shouldIgnoreWarningsForPath(filePath: string): boolean; - /** - * If we do googmodule processing, we polyfill module.id, since that's - * part of ES6 modules. This function determines what the module.id will be - * for each file. - */ - fileNameToModuleId(fileName: string): string; } /** @@ -107,6 +78,9 @@ export class TsickleCompilerHost implements ts.CompilerHost { constructor( private delegate: ts.CompilerHost, private tscOptions: ts.CompilerOptions, private options: Options, private environment: TsickleHost) { + if (options.logWarning && !environment.logWarning) { + environment.logWarning = options.logWarning; + } // ts.CompilerHost includes a bunch of optional methods. If they're // present on the delegate host, we want to delegate them. if (this.delegate.getCancellationToken) { @@ -279,19 +253,8 @@ export class TsickleCompilerHost implements ts.CompilerHost { } convertCommonJsToGoogModule(fileName: string, content: string): string { - const moduleId = this.environment.fileNameToModuleId(fileName); - - const {output, referencedModules} = processES5( - fileName, moduleId, content, this.environment.pathToModuleName.bind(this.environment), - this.options.es5Mode, this.options.prelude); - - const moduleName = this.environment.pathToModuleName('', fileName); - this.modulesManifest.addModule(fileName, moduleName); - for (const referenced of referencedModules) { - this.modulesManifest.addReferencedModule(fileName, referenced); - } - - return output; + return es5processor.convertCommonJsToGoogModule( + this.environment, this.options, this.modulesManifest, fileName, content); } private downlevelDecorators( @@ -301,7 +264,8 @@ export class TsickleCompilerHost implements ts.CompilerHost { this.getSourceMapKeyForSourceFile(sourceFile), new SourceMapGenerator()); if (this.environment.shouldSkipTsickleProcessing(fileName)) return sourceFile; let fileContent = sourceFile.text; - const converted = convertDecorators(program.getTypeChecker(), sourceFile); + const sourceMapper = new sourceMapUtils.DefaultSourceMapper(sourceFile.fileName); + const converted = convertDecorators(program.getTypeChecker(), sourceFile, sourceMapper); if (converted.diagnostics) { this.diagnostics.push(...converted.diagnostics); } @@ -311,7 +275,7 @@ export class TsickleCompilerHost implements ts.CompilerHost { } fileContent = converted.output; this.decoratorDownlevelSourceMaps.set( - this.getSourceMapKeyForSourceFile(sourceFile), converted.sourceMap); + this.getSourceMapKeyForSourceFile(sourceFile), sourceMapper.sourceMap); return ts.createSourceFile(fileName, fileContent, languageVersion, true); } @@ -325,13 +289,16 @@ export class TsickleCompilerHost implements ts.CompilerHost { // this means we don't process e.g. lib.d.ts. if (isDefinitions && this.environment.shouldSkipTsickleProcessing(fileName)) return sourceFile; + const sourceMapper = new sourceMapUtils.DefaultSourceMapper(sourceFile.fileName); const annotated = tsickle.annotate( - program, sourceFile, this.environment.pathToModuleName.bind(this.environment), this.options, - this.delegate, this.tscOptions); - const {output, externs, sourceMap} = annotated; - let {diagnostics} = annotated; - if (externs) { - this.externs[fileName] = externs; + program.getTypeChecker(), sourceFile, this.environment, this.options, this.delegate, + this.tscOptions, sourceMapper); + const externs = + tsickle.writeExterns(program.getTypeChecker(), sourceFile, this.environment, this.options); + let diagnostics = externs.diagnostics.concat(annotated.diagnostics); + + if (externs.output.length > 0) { + this.externs[fileName] = externs.output; } if (this.environment.shouldIgnoreWarningsForPath(sourceFile.fileName)) { // All diagnostics (including warnings) are treated as errors. @@ -341,18 +308,14 @@ export class TsickleCompilerHost implements ts.CompilerHost { diagnostics = diagnostics.filter(d => d.category === ts.DiagnosticCategory.Error); } this.diagnostics = diagnostics; - this.tsickleSourceMaps.set(this.getSourceMapKeyForSourceFile(sourceFile), sourceMap); - return ts.createSourceFile(fileName, output, languageVersion, true); + this.tsickleSourceMaps.set( + this.getSourceMapKeyForSourceFile(sourceFile), sourceMapper.sourceMap); + return ts.createSourceFile(fileName, annotated.output, languageVersion, true); } /** Concatenate all generated externs definitions together into a string. */ getGeneratedExterns(): string { - let allExterns = tsickle.EXTERNS_HEADER; - for (const fileName of Object.keys(this.externs)) { - allExterns += `// externs from ${fileName}:\n`; - allExterns += this.externs[fileName]; - } - return allExterns; + return tsickle.getGeneratedExterns(this.externs); } // Delegate everything else to the original compiler host. diff --git a/src/type-translator.ts b/src/type-translator.ts index 36e6e5a14..28f9a371f 100644 --- a/src/type-translator.ts +++ b/src/type-translator.ts @@ -11,7 +11,7 @@ import * as ts from 'typescript'; import {toArray} from './util'; export function assertTypeChecked(sourceFile: ts.SourceFile) { - if (!('resolvedModules' in sourceFile)) { + if (!('resolvedModules' in ts.getOriginalNode(sourceFile))) { throw new Error('must provide typechecked program'); } } diff --git a/test/decorator-annotator_test.ts b/test/decorator-annotator_test.ts index 6e6b275de..3487e211a 100644 --- a/test/decorator-annotator_test.ts +++ b/test/decorator-annotator_test.ts @@ -13,6 +13,7 @@ import {SourceMapConsumer} from 'source-map'; import * as ts from 'typescript'; import {convertDecorators} from '../src/decorator-annotator'; +import {DefaultSourceMapper} from '../src/source_map_utils'; import * as tsickle from '../src/tsickle'; import * as testSupport from './test_support'; @@ -36,11 +37,12 @@ describe( 'decorator-annotator', () => { function translate(sourceText: string, allowErrors = false) { const program = testSupport.createProgram(sources(sourceText)); - const {output, diagnostics, sourceMap} = - convertDecorators(program.getTypeChecker(), program.getSourceFile(testCaseFileName)); + const sourceMapper = new DefaultSourceMapper(testCaseFileName); + const {output, diagnostics} = convertDecorators( + program.getTypeChecker(), program.getSourceFile(testCaseFileName), sourceMapper); if (!allowErrors) expect(diagnostics).to.be.empty; verifyCompiles(output); - return {output, diagnostics, sourceMap}; + return {output, diagnostics, sourceMap: sourceMapper.sourceMap}; } function expectUnchanged(sourceText: string) { @@ -296,8 +298,7 @@ static decorators: {type: Function, args?: any[]}[] = [ /** @nocollapse */ static ctorParameters: () => ({type: any, decorators?: {type: Function, args?: any[]}[]}|null)[] = () => [ {type: BarService, }, -null, -]; +null,]; }`); }); @@ -319,8 +320,7 @@ class Foo { /** @nocollapse */ static ctorParameters: () => ({type: any, decorators?: {type: Function, args?: any[]}[]}|null)[] = () => [ {type: bar.BarService, decorators: [{ type: Inject, args: [param, ] }, ]}, -null, -null, +null, null, {type: bar.BarService, }, ]; }`); @@ -403,7 +403,7 @@ class Foo { class Foo { \n bar() {} static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { -'bar': [{ type: Test1, args: ['somename', ] },], +"bar": [{ type: Test1, args: ['somename', ] },], }; }`); }); @@ -425,8 +425,8 @@ class ClassWithDecorators { \n set c(value) {} static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { -'a': [{ type: PropDecorator, args: ["p1", ] },{ type: PropDecorator, args: ["p2", ] },], -'c': [{ type: PropDecorator, args: ["p3", ] },], +"a": [{ type: PropDecorator, args: ["p1", ] },{ type: PropDecorator, args: ["p2", ] },], +"c": [{ type: PropDecorator, args: ["p3", ] },], }; }`); }); @@ -456,7 +456,7 @@ class Foo { missingSemi = () => {} other: number; static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { -'other': [{ type: PropDecorator },], +"other": [{ type: PropDecorator },], }; }`); diff --git a/test/e2e_source_map_test.ts b/test/e2e_source_map_test.ts index cd85ec5f0..9c62b96cf 100644 --- a/test/e2e_source_map_test.ts +++ b/test/e2e_source_map_test.ts @@ -16,8 +16,9 @@ import * as ts from 'typescript'; import * as cliSupport from '../src/cli_support'; import {convertDecorators} from '../src/decorator-annotator'; import {toClosureJS} from '../src/main'; -import {getInlineSourceMapCount, setInlineSourceMap,} from '../src/source_map_utils'; +import {DefaultSourceMapper, getInlineSourceMapCount, setInlineSourceMap} from '../src/source_map_utils'; import * as tsickle from '../src/tsickle'; +import * as tsickleCompilerHost from '../src/tsickle_compiler_host'; import {toArray} from '../src/util'; import * as testSupport from './test_support'; @@ -236,8 +237,9 @@ describe('source maps', () => { const closurizeSources = decoratorDownlevelAndAddInlineSourceMaps(decoratorDownlevelSources); - const {compiledJS, sourceMap} = - compile(closurizeSources, {inlineSourceMap: true, tsicklePasses: [tsickle.Pass.CLOSURIZE]}); + const {compiledJS, sourceMap} = compile( + closurizeSources, + {inlineSourceMap: true, tsicklePasses: [tsickleCompilerHost.Pass.CLOSURIZE]}); expect(getInlineSourceMapCount(compiledJS)).to.equal(1); const {line, column} = getLineAndColumn(compiledJS, 'methodName'); @@ -289,7 +291,7 @@ describe('source maps', () => { let z : string = x + y;`); const {compiledJS, sourceMap} = - compile(closurizeSources, {tsicklePasses: [tsickle.Pass.CLOSURIZE]}); + compile(closurizeSources, {tsicklePasses: [tsickleCompilerHost.Pass.CLOSURIZE]}); { const {line, column} = getLineAndColumn(compiledJS, 'methodName'); @@ -310,9 +312,10 @@ function decoratorDownlevelAndAddInlineSourceMaps(sources: Map): const transformedSources = new Map(); const program = testSupport.createProgram(sources); for (const fileName of toArray(sources.keys())) { - const {output, sourceMap: preexistingSourceMap} = - convertDecorators(program.getTypeChecker(), program.getSourceFile(fileName)); - transformedSources.set(fileName, setInlineSourceMap(output, preexistingSourceMap.toString())); + const sourceMapper = new DefaultSourceMapper(fileName); + const {output} = + convertDecorators(program.getTypeChecker(), program.getSourceFile(fileName), sourceMapper); + transformedSources.set(fileName, setInlineSourceMap(output, sourceMapper.sourceMap.toString())); } return transformedSources; } @@ -331,7 +334,7 @@ interface CompilerOptions { outFile: string; filesNotToProcess: Set; inlineSourceMap: boolean; - tsicklePasses: tsickle.Pass[]; + tsicklePasses: tsickleCompilerHost.Pass[]; generateDTS: boolean; } @@ -339,7 +342,7 @@ const DEFAULT_COMPILER_OPTIONS = { outFile: 'output.js', filesNotToProcess: new Set(), inlineSourceMap: false, - tsicklePasses: [tsickle.Pass.DECORATOR_DOWNLEVEL, tsickle.Pass.CLOSURIZE], + tsicklePasses: [tsickleCompilerHost.Pass.DECORATOR_DOWNLEVEL, tsickleCompilerHost.Pass.CLOSURIZE], generateDTS: false, }; @@ -372,7 +375,7 @@ function compile(sources: Map, partialOptions = {} as Partial fileNames.indexOf(fileName) === -1 || options.filesNotToProcess.has(fileName), pathToModuleName: cliSupport.pathToModuleName, diff --git a/test/es5processor_test.ts b/test/es5processor_test.ts index 653180709..c1d3e8d58 100644 --- a/test/es5processor_test.ts +++ b/test/es5processor_test.ts @@ -12,11 +12,17 @@ import * as cliSupport from '../src/cli_support'; import * as es5processor from '../src/es5processor'; describe('convertCommonJsToGoogModule', () => { + function processES5(fileName: string, content: string, isES5 = true, prelude = '') { + const options: es5processor.Options = {es5Mode: isES5, prelude}; + const host: es5processor.Host = { + fileNameToModuleId: (fn) => fn, + pathToModuleName: cliSupport.pathToModuleName + }; + return es5processor.processES5(host, options, fileName, content); + } + function expectCommonJs(fileName: string, content: string, isES5 = true, prelude = '') { - return expect( - es5processor - .processES5(fileName, fileName, content, cliSupport.pathToModuleName, isES5, prelude) - .output); + return expect(processES5(fileName, content, isES5, prelude).output); } it('adds a goog.module call', () => { @@ -172,14 +178,13 @@ foo_1.A, foo_2.B, foo_2 , foo_3.default; }); it('gathers referenced modules', () => { - const {referencedModules} = - es5processor.processES5('a/b', 'a/b', ` + const {referencedModules} = processES5('a/b', ` require('../foo/bare_require'); var googRequire = require('goog:foo.bar'); var es6RelativeRequire = require('./relative'); var es6NonRelativeRequire = require('non/relative'); __export(require('./export_star'); -`, cliSupport.pathToModuleName); +`); return expect(referencedModules).to.deep.equal([ 'foo.bare_require', diff --git a/test/source_map_test.ts b/test/source_map_test.ts index 3800c3812..dfdf47135 100644 --- a/test/source_map_test.ts +++ b/test/source_map_test.ts @@ -7,11 +7,15 @@ */ import {expect} from 'chai'; +import * as path from 'path'; import {SourceMapConsumer} from 'source-map'; +import * as ts from 'typescript'; -import {annotate} from '../src/tsickle'; +import {DefaultSourceMapper, sourceMapTextToConsumer} from '../src/source_map_utils'; +import * as transformer from '../src/transformer'; +import * as tsickle from '../src/tsickle'; -import {createProgram} from './test_support'; +import * as testSupport from './test_support'; describe('source maps', () => { it('generates a source map', () => { @@ -19,9 +23,14 @@ describe('source maps', () => { sources.set('input.ts', ` class X { field: number; } class Y { field2: string; }`); - const program = createProgram(sources); - const annotated = annotate(program, program.getSourceFile('input.ts'), () => 'input'); - const rawMap = annotated.sourceMap.toJSON(); + const tsHost = ts.createCompilerHost(testSupport.compilerOptions); + const program = testSupport.createProgram(sources, tsHost); + const sourceMapper = new DefaultSourceMapper('input.ts'); + const tsickleHost: tsickle.Host = {pathToModuleName: () => 'input'}; + const annotated = tsickle.annotate( + program.getTypeChecker(), program.getSourceFile('input.ts'), tsickleHost, {}, tsHost, + testSupport.compilerOptions, sourceMapper); + const rawMap = sourceMapper.sourceMap.toJSON(); const consumer = new SourceMapConsumer(rawMap); const lines = annotated.output.split('\n'); // Uncomment to debug contents: @@ -34,4 +43,55 @@ describe('source maps', () => { expect(consumer.originalPositionFor({line: secondClassLine, column: 20}).line) .to.equal(3, 'second class definition'); }); + + it('generates a source map with transformers', () => { + const sources = new Map(); + sources.set('input.ts', ` + class X { field: number; } + class Y { field2: string; }`); + const tsCompilerOptions: ts.CompilerOptions = { + ...testSupport.compilerOptions, + sourceMap: true, + inlineSourceMap: false + }; + const tsHost = ts.createCompilerHost(tsCompilerOptions); + const program = testSupport.createProgram(sources, tsHost, tsCompilerOptions); + const transformerHost: transformer.TransformerHost = { + shouldSkipTsickleProcessing: (fileName) => !sources.has(fileName), + shouldIgnoreWarningsForPath: () => false, + pathToModuleName: (context, importPath) => { + importPath = importPath.replace(/(\.d)?\.[tj]s$/, ''); + if (importPath[0] === '.') importPath = path.join(path.dirname(context), importPath); + return importPath.replace(/\/|\\/g, '.'); + }, + fileNameToModuleId: (fileName) => fileName.replace(/^\.\//, ''), + }; + const transfromerOptions: transformer.TransformerOptions = { + es5Mode: true, + prelude: '', + googmodule: true, + convertIndexImportShorthand: true, + transformDecorators: true, + transformTypesToClosure: true, + }; + const emittedFiles: {[fileName: string]: string} = {}; + const {diagnostics, externs} = transformer.emitWithTsickle( + program, transformerHost, transfromerOptions, tsHost, tsCompilerOptions, undefined, + (fileName: string, data: string) => { + emittedFiles[fileName] = data; + }); + + const consumer = sourceMapTextToConsumer(emittedFiles['./input.js.map']); + const lines = emittedFiles['./input.js'].split('\n'); + // Uncomment to debug contents: + // lines.forEach((v, i) => console.log(i + 1, v)); + // Find class X and class Y in the output to make the test robust against code changes. + const firstClassLine = lines.findIndex(l => l.indexOf('class X') !== -1) + 1; + const secondClassLine = lines.findIndex(l => l.indexOf('class Y') !== -1) + 1; + expect(consumer.originalPositionFor({line: firstClassLine, column: 20}).line) + .to.equal(2, 'first class definition'); + expect(consumer.originalPositionFor({line: secondClassLine, column: 20}).line) + .to.equal(3, 'second class definition'); + + }); }); diff --git a/test/test_support.ts b/test/test_support.ts index d1a16ee73..9a90e67ac 100644 --- a/test/test_support.ts +++ b/test/test_support.ts @@ -12,6 +12,7 @@ import * as path from 'path'; import * as ts from 'typescript'; import * as cliSupport from '../src/cli_support'; +import * as es5processor from '../src/es5processor'; import * as tsickle from '../src/tsickle'; import {toArray} from '../src/util'; @@ -46,8 +47,12 @@ const {cachedLibPath, cachedLib} = (() => { })(); /** Creates a ts.Program from a set of input files. */ -export function createProgram(sources: Map): ts.Program { - const host = ts.createCompilerHost(compilerOptions); +export function createProgram( + sources: Map, host?: ts.CompilerHost, + tsCompilerOptions: ts.CompilerOptions = compilerOptions): ts.Program { + if (!host) { + host = ts.createCompilerHost(tsCompilerOptions); + } host.getSourceFile = (fileName: string, languageVersion: ts.ScriptTarget, onError?: (msg: string) => void): ts.SourceFile => { @@ -63,17 +68,22 @@ export function createProgram(sources: Map): ts.Program { throw new Error('unexpected file read of ' + fileName + ' not in ' + toArray(sources.keys())); }; - return ts.createProgram(toArray(sources.keys()), compilerOptions, host); + return ts.createProgram(toArray(sources.keys()), tsCompilerOptions, host); } /** Emits transpiled output with tsickle postprocessing. Throws an exception on errors. */ -export function emit(program: ts.Program): {[fileName: string]: string} { +export function emit( + program: ts.Program, + transformers: Array> = []): {[fileName: string]: string} { const transformed: {[fileName: string]: string} = {}; const {diagnostics} = program.emit(undefined, (fileName: string, data: string) => { - const moduleId = fileName.replace(/^\.\//, ''); - transformed[fileName] = - tsickle.processES5(fileName, moduleId, data, cliSupport.pathToModuleName).output; - }); + const options: es5processor.Options = {es5Mode: true, prelude: ''}; + const host: es5processor.Host = { + fileNameToModuleId: (fn) => fn.replace(/^\.\//, ''), + pathToModuleName: cliSupport.pathToModuleName + }; + transformed[fileName] = es5processor.processES5(host, options, fileName, data).output; + }, undefined, undefined, {before: transformers}); if (diagnostics.length > 0) { throw new Error(tsickle.formatDiagnostics(diagnostics)); } diff --git a/test/tsickle_test.ts b/test/tsickle_test.ts index bf222e3e3..bf5247891 100644 --- a/test/tsickle_test.ts +++ b/test/tsickle_test.ts @@ -133,30 +133,37 @@ testFn('golden tests', () => { const tsickleSources = new Map(); for (const tsPath of toArray(tsSources.keys())) { const warnings: ts.Diagnostic[] = []; - options.logWarning = (diag: ts.Diagnostic) => { - warnings.push(diag); - }; // Run TypeScript through tsickle and compare against goldens. - const {output, externs, diagnostics} = tsickle.annotate( - program, program.getSourceFile(tsPath), - (context, importPath) => { - importPath = importPath.replace(/(\.d)?\.[tj]s$/, ''); - if (importPath[0] === '.') importPath = path.join(path.dirname(context), importPath); - return importPath.replace(/\/|\\/g, '.'); - }, - options, { - fileExists: ts.sys.fileExists, - readFile: ts.sys.readFile, - }, + const tsHost = { + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, + }; + const tsickleHost: tsickle.Host = { + logWarning: (diag: ts.Diagnostic) => { + warnings.push(diag); + }, + pathToModuleName: (context, importPath) => { + importPath = importPath.replace(/(\.d)?\.[tj]s$/, ''); + if (importPath[0] === '.') importPath = path.join(path.dirname(context), importPath); + return importPath.replace(/\/|\\/g, '.'); + } + }; + const sourceFile = program.getSourceFile(tsPath); + const annotated = tsickle.annotate( + program.getTypeChecker(), sourceFile, tsickleHost, options, tsHost, testSupport.compilerOptions); - if (externs && !test.name.endsWith('.no_externs')) { + const externs = + tsickle.writeExterns(program.getTypeChecker(), sourceFile, tsickleHost, options); + const diagnostics = externs.diagnostics.concat(annotated.diagnostics); + + if (externs.output && !test.name.endsWith('.no_externs')) { if (!allExterns) allExterns = tsickle.EXTERNS_HEADER; - allExterns += externs; + allExterns += externs.output; } // If there were any diagnostics, convert them into strings for // the golden output. - let fileOutput = output; + let fileOutput = annotated.output; diagnostics.push(...warnings); if (diagnostics.length > 0) { // Munge the filenames in the diagnostics so that they don't include @@ -165,12 +172,12 @@ testFn('golden tests', () => { const fileName = diag.file.fileName; diag.file.fileName = fileName.substr(fileName.indexOf('test_files')); } - fileOutput = tsickle.formatDiagnostics(diagnostics) + '\n====\n' + output; + fileOutput = tsickle.formatDiagnostics(diagnostics) + '\n====\n' + annotated.output; } const tsicklePath = tsPath.replace(/((\.d)?\.tsx?)$/, '.tsickle$1'); expect(tsicklePath).to.not.equal(tsPath); compareAgainstGolden(fileOutput, tsicklePath); - tsickleSources.set(tsPath, output); + tsickleSources.set(tsPath, annotated.output); } compareAgainstGolden(allExterns, test.externsPath); diff --git a/test/tsickle_transformer_test.ts b/test/tsickle_transformer_test.ts new file mode 100644 index 000000000..0dd4b818b --- /dev/null +++ b/test/tsickle_transformer_test.ts @@ -0,0 +1,218 @@ +/** + * @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 + */ + +import {expect} from 'chai'; +import * as diff from 'diff'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; + +import * as transformer from '../src/transformer'; +import * as tsickle from '../src/tsickle'; +import {toArray} from '../src/util'; + +import * as testSupport from './test_support'; + +const TEST_FILTER: RegExp|null = + process.env.TEST_FILTER ? new RegExp(process.env.TEST_FILTER) : null; + +// If true, update all the golden .transformer.patch files to be whatever tsickle +// produces from the .ts source. Do not change this code but run as: +// UPDATE_TRANSFORMER_GOLDENS=y gulp test +const UPDATE_GOLDENS = !!process.env.UPDATE_TRANSFORMER_GOLDENS; + +function calcPatchPath(path: string): string { + return `${path}.transform.patch`; +} + +function readGolden(path: string): string|null { + let golden: string|null = null; + try { + golden = fs.readFileSync(path, 'utf-8'); + } catch (e) { + if (e.code === 'ENOENT') { + return null; + } else { + throw e; + } + } + + const patchPath = calcPatchPath(path); + if (fs.existsSync(patchPath)) { + // Note: the typings for `diff.applyPath` are wrong in that the function + // can also return `false`. + const patchedGolden = + diff.applyPatch(golden, fs.readFileSync(patchPath, 'utf-8')) as string | false; + if (patchedGolden === false) { + return golden; + } + return patchedGolden; + } + return golden; +} + +/** + * compareAgainstGoldens compares a test output against the content in a golden + * path, updating the content of the golden when UPDATE_GOLDENS is true. + * + * @param output The expected output, where the empty string indicates + * the file is expected to exist and be empty, while null indicates + * the file is expected to not exist. (This subtlety is used for + * externs files, where the majority of tests are not expected to + * produce one.) + */ +function compareAgainstGolden(output: string|null, path: string) { + let golden = readGolden(path); + + // Make sure we have proper line endings when testing on Windows. + if (golden != null) golden = golden.replace(/\r\n/g, '\n'); + if (output != null) output = output.replace(/\r\n/g, '\n'); + + const patchPath = calcPatchPath(path); + if (UPDATE_GOLDENS && output !== golden) { + console.log(`Updating golden path file for ${path} with ${patchPath}`); + if (output !== null) { + const patchOutput = + diff.createPatch(path, golden || '', output, 'golden', 'tsickle with transformer')!; + fs.writeFileSync(patchPath, patchOutput, 'utf-8'); + } else { + // The desired golden state is for there to be no output file. + // Ensure no file exists. + try { + fs.unlinkSync(patchPath); + } catch (e) { + // ignore. + } + } + } else { + expect(output).to.equal(golden, `${path} with ${patchPath}`); + } +} + +const DIAGONSTIC_FILE_REGEX = /(test_files.*?):\s/; + +function compareAgainstGoldenDiagnostics(diagnostics: ts.Diagnostic[], path: string) { + // Munge the filenames in the diagnostics so that they don't include + // the tsickle checkout path. + for (const diag of diagnostics) { + const fileName = diag.file.fileName; + diag.file.fileName = fileName.substr(fileName.indexOf('test_files')); + } + const tsicklePath = path.replace(/((\.d)?\.tsx?)$/, '.tsickle$1'); + expect(tsicklePath).to.not.equal(path); + const golden = readGolden(tsicklePath) || ''; + const goldenFormattedDiagnostics = + sortDiagnostics(golden.substring(0, golden.indexOf('\n====\n'))); + const outputFormattedDiagnostics = sortDiagnostics( + tsickle.formatDiagnostics(diagnostics.filter(diag => diag.file.fileName === path))); + + expect(outputFormattedDiagnostics).to.equal(goldenFormattedDiagnostics, ''); +} + +function sortDiagnostics(diagnostics: string): string { + const lines = diagnostics.split('\n'); + return lines + .sort((l1, l2) => { + const m1 = DIAGONSTIC_FILE_REGEX.exec(l1) || [l1]; + const m2 = DIAGONSTIC_FILE_REGEX.exec(l2) || [l2]; + return m1[0].localeCompare(m2[0]); + }) + .join('\n'); +} + +// Only run golden tests if we filter for a specific one. +const testFn = TEST_FILTER ? describe.only : describe; + +testFn('golden transformer tests', () => { + testSupport.goldenTests().forEach((test) => { + if (TEST_FILTER && !TEST_FILTER.exec(test.name)) { + it.skip(test.name); + return; + } + let emitDeclarations = true; + const transfromerOptions: transformer.TransformerOptions = { + // See test_files/jsdoc_types/nevertyped.ts. + es5Mode: true, + prelude: '', + googmodule: true, + typeBlackListPaths: new Set(['test_files/jsdoc_types/nevertyped.ts']), + convertIndexImportShorthand: true, + transformDecorators: true, + transformTypesToClosure: true, + }; + if (/\.untyped\b/.test(test.name)) { + transfromerOptions.untyped = true; + } + if (test.name === 'fields') { + emitDeclarations = false; + } + it(test.name, () => { + // Read all the inputs into a map, and create a ts.Program from them. + const tsSources = new Map(); + for (const tsFile of test.tsFiles) { + const tsPath = path.join(test.path, tsFile); + let tsSource = fs.readFileSync(tsPath, 'utf-8'); + tsSource = tsSource.replace(/\r\n/g, '\n'); + tsSources.set(tsPath, tsSource); + } + const tsCompilerOptions: ts.CompilerOptions = { + ...testSupport.compilerOptions, + // Test that creating declarations does not throw + declaration: emitDeclarations + }; + const tsHost = ts.createCompilerHost(tsCompilerOptions); + const program = testSupport.createProgram(tsSources, tsHost, tsCompilerOptions); + { + const diagnostics = ts.getPreEmitDiagnostics(program); + if (diagnostics.length) { + throw new Error(tsickle.formatDiagnostics(diagnostics)); + } + } + const allDiagnostics: ts.Diagnostic[] = []; + const transformerHost: transformer.TransformerHost = { + logWarning: (diag: ts.Diagnostic) => { + allDiagnostics.push(diag); + }, + shouldSkipTsickleProcessing: (fileName) => !tsSources.has(fileName), + shouldIgnoreWarningsForPath: () => false, + pathToModuleName: (context, importPath) => { + importPath = importPath.replace(/(\.d)?\.[tj]s$/, ''); + if (importPath[0] === '.') importPath = path.join(path.dirname(context), importPath); + return importPath.replace(/\/|\\/g, '.'); + }, + fileNameToModuleId: (fileName) => fileName.replace(/^\.\//, ''), + }; + const jsSources: {[fileName: string]: string} = {}; + const {diagnostics, externs} = transformer.emitWithTsickle( + program, transformerHost, transfromerOptions, tsHost, tsCompilerOptions, undefined, + (fileName: string, data: string) => { + if (!fileName.endsWith('.d.ts')) { + // Don't check .d.ts files, we are only interested to test + // that we don't throw when we generate them. + jsSources[fileName] = data; + } + }); + allDiagnostics.push(...diagnostics); + let allExterns: string|null = null; + if (!test.name.endsWith('.no_externs')) { + for (const tsPath of toArray(tsSources.keys())) { + if (externs[tsPath]) { + if (!allExterns) allExterns = tsickle.EXTERNS_HEADER; + allExterns += externs[tsPath]; + } + } + } + compareAgainstGolden(allExterns, test.externsPath); + Object.keys(jsSources).forEach(jsPath => { + compareAgainstGolden(jsSources[jsPath], jsPath); + }); + Array.from(tsSources.keys()) + .forEach(tsPath => compareAgainstGoldenDiagnostics(allDiagnostics, tsPath)); + }); + }); +}); diff --git a/test_files/clutz.no_externs/strip_clutz_type.js b/test_files/clutz.no_externs/strip_clutz_type.js index 78345564e..1f2f5488f 100644 --- a/test_files/clutz.no_externs/strip_clutz_type.js +++ b/test_files/clutz.no_externs/strip_clutz_type.js @@ -4,9 +4,9 @@ goog.module('test_files.clutz.no_externs.strip_clutz_type');var module = module */ var goog_some_name_space_1 = goog.require('some.name.space'); -const tsickle_forward_declare_1 = goog.forwardDeclare('some.name.space'); -const tsickle_forward_declare_2 = goog.forwardDeclare('some.other'); -goog.require('some.other'); // force type-only module to be loaded +const tsickle_forward_declare_1 = goog.forwardDeclare("some.name.space"); +const tsickle_forward_declare_2 = goog.forwardDeclare("some.other"); +goog.require("some.other"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1.ClutzedClass} */ clutzedClass = new goog_some_name_space_1.ClutzedClass(); console.log(clutzedClass); let /** @type {!some.other.ClutzedInterface} */ typeAliased = clutzedClass.field; diff --git a/test_files/clutz.no_externs/strip_clutz_type.tsickle.ts b/test_files/clutz.no_externs/strip_clutz_type.tsickle.ts index 8fc183180..4cd48a775 100644 --- a/test_files/clutz.no_externs/strip_clutz_type.tsickle.ts +++ b/test_files/clutz.no_externs/strip_clutz_type.tsickle.ts @@ -4,10 +4,10 @@ */ import {ClutzedClass, clutzedFn} from 'goog:some.name.space'; -const tsickle_forward_declare_1 = goog.forwardDeclare('some.name.space'); +const tsickle_forward_declare_1 = goog.forwardDeclare("some.name.space"); import {TypeAlias} from 'goog:some.other'; -const tsickle_forward_declare_2 = goog.forwardDeclare('some.other'); -goog.require('some.other'); // force type-only module to be loaded +const tsickle_forward_declare_2 = goog.forwardDeclare("some.other"); +goog.require("some.other"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1.ClutzedClass} */ clutzedClass: ClutzedClass = new ClutzedClass(); console.log(clutzedClass); diff --git a/test_files/decorator/decorator.decorated.ts b/test_files/decorator/decorator.decorated.ts index 2406cd032..87d67d7be 100644 --- a/test_files/decorator/decorator.decorated.ts +++ b/test_files/decorator/decorator.decorated.ts @@ -1,3 +1,5 @@ +import {AClass, AType, AClassWithGenerics} from './external'; + function decorator(a: Object, b: string) {} /** @Annotation */ @@ -15,19 +17,30 @@ function classAnnotation(t: any) { return t; } class DecoratorTest { + constructor(a: any[], n: number, b: boolean, promise: Promise, arr: Array, aClass: AClass, aClassWithGenerics: AClassWithGenerics, aType: AType) {} + @decorator private x: number; private y: number; + + @decorator + private z: AClass; static decorators: {type: Function, args?: any[]}[] = [ { type: classAnnotation }, ]; /** @nocollapse */ static ctorParameters: () => ({type: any, decorators?: {type: Function, args?: any[]}[]}|null)[] = () => [ -]; +{type: Array, }, +null, null, +{type: Promise, }, +{type: Array, }, +{type: AClass, }, +{type: AClassWithGenerics, }, +null,]; static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { -'y': [{ type: annotationDecorator },], +"y": [{ type: annotationDecorator },], }; } diff --git a/test_files/decorator/decorator.js b/test_files/decorator/decorator.js index f850586f6..3d7fd0ce9 100644 --- a/test_files/decorator/decorator.js +++ b/test_files/decorator/decorator.js @@ -2,6 +2,9 @@ goog.module('test_files.decorator.decorator');var module = module || {id: 'test_ * @fileoverview added by tsickle * @suppress {checkTypes} checked by tsc */ + +var external_1 = goog.require('test_files.decorator.external'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.decorator.external"); /** * @param {!Object} a * @param {string} b @@ -27,21 +30,42 @@ function classDecorator(t) { return t; } */ function classAnnotation(t) { return t; } class DecoratorTest { + /** + * @param {!Array} a + * @param {number} n + * @param {boolean} b + * @param {!Promise} promise + * @param {!Array} arr + * @param {!tsickle_forward_declare_1.AClass} aClass + * @param {!tsickle_forward_declare_1.AClassWithGenerics} aClassWithGenerics + * @param {!tsickle_forward_declare_1.AType} aType + */ + constructor(a, n, b, promise, arr, aClass, aClassWithGenerics, aType) { } } DecoratorTest.decorators = [ { type: classAnnotation }, ]; -/** - * @nocollapse - */ -DecoratorTest.ctorParameters = () => []; +/** @nocollapse */ +DecoratorTest.ctorParameters = () => [ + { type: Array, }, + null, null, + { type: Promise, }, + { type: Array, }, + { type: external_1.AClass, }, + { type: external_1.AClassWithGenerics, }, + null, +]; DecoratorTest.propDecorators = { - 'y': [{ type: annotationDecorator },], + "y": [{ type: annotationDecorator },], }; __decorate([ decorator, __metadata("design:type", Number) ], DecoratorTest.prototype, "x", void 0); +__decorate([ + decorator, + __metadata("design:type", external_1.AClass) +], DecoratorTest.prototype, "z", void 0); function DecoratorTest_tsickle_Closure_declarations() { /** @type {!Array<{type: !Function, args: (undefined|!Array)}>} */ DecoratorTest.decorators; @@ -56,6 +80,8 @@ function DecoratorTest_tsickle_Closure_declarations() { DecoratorTest.prototype.x; /** @type {number} */ DecoratorTest.prototype.y; + /** @type {!tsickle_forward_declare_1.AClass} */ + DecoratorTest.prototype.z; } let DecoratedClass = class DecoratedClass { }; diff --git a/test_files/decorator/decorator.js.transform.patch b/test_files/decorator/decorator.js.transform.patch new file mode 100644 index 000000000..a0256ebba --- /dev/null +++ b/test_files/decorator/decorator.js.transform.patch @@ -0,0 +1,14 @@ +Index: ./test_files/decorator/decorator.js +=================================================================== +--- ./test_files/decorator/decorator.js golden ++++ ./test_files/decorator/decorator.js tsickle with transformer +@@ -44,9 +44,8 @@ + } + DecoratorTest.decorators = [ + { type: classAnnotation }, + ]; +-/** @nocollapse */ + DecoratorTest.ctorParameters = () => [ + { type: Array, }, + null, null, + { type: Promise, }, diff --git a/test_files/decorator/decorator.ts b/test_files/decorator/decorator.ts index 55cb70729..e98b7dca8 100644 --- a/test_files/decorator/decorator.ts +++ b/test_files/decorator/decorator.ts @@ -1,3 +1,5 @@ +import {AClass, AType, AClassWithGenerics} from './external'; + function decorator(a: Object, b: string) {} /** @Annotation */ @@ -15,11 +17,16 @@ function classAnnotation(t: any) { return t; } @classAnnotation class DecoratorTest { + constructor(a: any[], n: number, b: boolean, promise: Promise, arr: Array, aClass: AClass, aClassWithGenerics: AClassWithGenerics, aType: AType) {} + @decorator private x: number; @annotationDecorator private y: number; + + @decorator + private z: AClass; } @classDecorator diff --git a/test_files/decorator/decorator.tsickle.ts b/test_files/decorator/decorator.tsickle.ts index eb0a22809..0c9f268ac 100644 --- a/test_files/decorator/decorator.tsickle.ts +++ b/test_files/decorator/decorator.tsickle.ts @@ -3,7 +3,8 @@ * @suppress {checkTypes} checked by tsc */ - +import {AClass, AType, AClassWithGenerics} from './external'; +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.decorator.external"); /** * @param {!Object} a * @param {string} b @@ -31,19 +32,38 @@ type classAnnotation = {}; */ function classAnnotation(t: any) { return t; } class DecoratorTest { +/** + * @param {!Array} a + * @param {number} n + * @param {boolean} b + * @param {!Promise} promise + * @param {!Array} arr + * @param {!tsickle_forward_declare_1.AClass} aClass + * @param {!tsickle_forward_declare_1.AClassWithGenerics} aClassWithGenerics + * @param {!tsickle_forward_declare_1.AType} aType + */ +constructor(a: any[], n: number, b: boolean, promise: Promise, arr: Array, aClass: AClass, aClassWithGenerics: AClassWithGenerics, aType: AType) {} + @decorator private x: number; private y: number; + + @decorator +private z: AClass; static decorators: {type: Function, args?: any[]}[] = [ { type: classAnnotation }, ]; -/** - * @nocollapse - */ +/** @nocollapse */ static ctorParameters: () => ({type: any, decorators?: {type: Function, args?: any[]}[]}|null)[] = () => [ -]; +{type: Array, }, +null, null, +{type: Promise, }, +{type: Array, }, +{type: AClass, }, +{type: AClassWithGenerics, }, +null,]; static propDecorators: {[key: string]: {type: Function, args?: any[]}[]} = { -'y': [{ type: annotationDecorator },], +"y": [{ type: annotationDecorator },], }; } @@ -61,6 +81,8 @@ DecoratorTest.propDecorators; DecoratorTest.prototype.x; /** @type {number} */ DecoratorTest.prototype.y; +/** @type {!tsickle_forward_declare_1.AClass} */ +DecoratorTest.prototype.z; } @classDecorator diff --git a/test_files/decorator/external.js b/test_files/decorator/external.js new file mode 100644 index 000000000..fbbc6bd21 --- /dev/null +++ b/test_files/decorator/external.js @@ -0,0 +1,19 @@ +goog.module('test_files.decorator.external');var module = module || {id: 'test_files/decorator/external.js'};/** + * @fileoverview added by tsickle + * @suppress {checkTypes} checked by tsc + */ + +class AClass { +} +exports.AClass = AClass; +/** + * @template T + */ +class AClassWithGenerics { +} +exports.AClassWithGenerics = AClassWithGenerics; +/** + * @record + */ +function AType() { } +exports.AType = AType; diff --git a/test_files/decorator/external.ts b/test_files/decorator/external.ts new file mode 100644 index 000000000..d951a07ba --- /dev/null +++ b/test_files/decorator/external.ts @@ -0,0 +1,5 @@ +export class AClass {} + +export class AClassWithGenerics {} + +export interface AType {} \ No newline at end of file diff --git a/test_files/decorator/external.tsickle.ts b/test_files/decorator/external.tsickle.ts new file mode 100644 index 000000000..f1bd37e85 --- /dev/null +++ b/test_files/decorator/external.tsickle.ts @@ -0,0 +1,18 @@ +/** + * @fileoverview added by tsickle + * @suppress {checkTypes} checked by tsc + */ + + +export class AClass {} +/** + * @template T + */ +export class AClassWithGenerics {} +/** + * @record + */ +export function AType() {} + + +export interface AType {} \ No newline at end of file diff --git a/test_files/enum.untyped/enum.untyped.js b/test_files/enum.untyped/enum.untyped.js index fd86c1475..13db2422a 100644 --- a/test_files/enum.untyped/enum.untyped.js +++ b/test_files/enum.untyped/enum.untyped.js @@ -8,8 +8,9 @@ EnumUntypedTest1.XYZ = 0; EnumUntypedTest1.PI = 3.14159; EnumUntypedTest1[EnumUntypedTest1.XYZ] = "XYZ"; EnumUntypedTest1[EnumUntypedTest1.PI] = "PI"; -exports.EnumUntypedTest2 = {}; -exports.EnumUntypedTest2.XYZ = 0; -exports.EnumUntypedTest2.PI = 3.14159; -exports.EnumUntypedTest2[exports.EnumUntypedTest2.XYZ] = "XYZ"; -exports.EnumUntypedTest2[exports.EnumUntypedTest2.PI] = "PI"; +let EnumUntypedTest2 = {}; +exports.EnumUntypedTest2 = EnumUntypedTest2; +EnumUntypedTest2.XYZ = 0; +EnumUntypedTest2.PI = 3.14159; +EnumUntypedTest2[EnumUntypedTest2.XYZ] = "XYZ"; +EnumUntypedTest2[EnumUntypedTest2.PI] = "PI"; diff --git a/test_files/enum.untyped/enum.untyped.tsickle.ts b/test_files/enum.untyped/enum.untyped.tsickle.ts index 1e1540d0a..112d3db3c 100644 --- a/test_files/enum.untyped/enum.untyped.tsickle.ts +++ b/test_files/enum.untyped/enum.untyped.tsickle.ts @@ -11,8 +11,9 @@ EnumUntypedTest1.PI = 3.14159; EnumUntypedTest1[EnumUntypedTest1.XYZ] = "XYZ"; EnumUntypedTest1[EnumUntypedTest1.PI] = "PI"; -export type EnumUntypedTest2 = number; -export let EnumUntypedTest2: any = {}; +type EnumUntypedTest2 = number; +let EnumUntypedTest2: any = {}; +export {EnumUntypedTest2}; EnumUntypedTest2.XYZ = 0; EnumUntypedTest2.PI = 3.14159; EnumUntypedTest2[EnumUntypedTest2.XYZ] = "XYZ"; diff --git a/test_files/enum/enum.js b/test_files/enum/enum.js index f022a3fa9..fd00525ad 100644 --- a/test_files/enum/enum.js +++ b/test_files/enum/enum.js @@ -27,13 +27,14 @@ function enumTestFunction(val) { } enumTestFunction(enumTestValue); let /** @type {number} */ enumTestLookup = EnumTest1["XYZ"]; let /** @type {?} */ enumTestLookup2 = EnumTest1["xyz".toUpperCase()]; -exports.EnumTest2 = {}; +let EnumTest2 = {}; +exports.EnumTest2 = EnumTest2; /** @type {number} */ -exports.EnumTest2.XYZ = 0; +EnumTest2.XYZ = 0; /** @type {number} */ -exports.EnumTest2.PI = 3.14159; -exports.EnumTest2[exports.EnumTest2.XYZ] = "XYZ"; -exports.EnumTest2[exports.EnumTest2.PI] = "PI"; +EnumTest2.PI = 3.14159; +EnumTest2[EnumTest2.XYZ] = "XYZ"; +EnumTest2[EnumTest2.PI] = "PI"; let ComponentIndex = {}; /** @type {number} */ ComponentIndex.Scheme = 1; diff --git a/test_files/enum/enum.js.transform.patch b/test_files/enum/enum.js.transform.patch new file mode 100644 index 000000000..e942bbef2 --- /dev/null +++ b/test_files/enum/enum.js.transform.patch @@ -0,0 +1,17 @@ +Index: ./test_files/enum/enum.js +=================================================================== +--- ./test_files/enum/enum.js golden ++++ ./test_files/enum/enum.js tsickle with transformer +@@ -16,10 +16,10 @@ + // number. Verify that the resulting TypeScript still allows you to + // index into the enum with all the various ways allowed of enums. + let /** @type {number} */ enumTestValue = EnumTest1.XYZ; + let /** @type {number} */ enumTestValue2 = EnumTest1['XYZ']; +-let /** @type {string} */ enumNumIndex = EnumTest1[((null))]; +-let /** @type {number} */ enumStrIndex = EnumTest1[((null))]; ++let /** @type {string} */ enumNumIndex = EnumTest1[/** @type {number} */ ((null))]; ++let /** @type {number} */ enumStrIndex = EnumTest1[/** @type {string} */ ((null))]; + /** + * @param {number} val + * @return {void} + */ diff --git a/test_files/enum/enum.tsickle.ts b/test_files/enum/enum.tsickle.ts index 36c61b503..6078994a4 100644 --- a/test_files/enum/enum.tsickle.ts +++ b/test_files/enum/enum.tsickle.ts @@ -35,8 +35,9 @@ enumTestFunction(enumTestValue); let /** @type {number} */ enumTestLookup = EnumTest1["XYZ"]; let /** @type {?} */ enumTestLookup2 = EnumTest1["xyz".toUpperCase()]; -export type EnumTest2 = number; -export let EnumTest2: any = {}; +type EnumTest2 = number; +let EnumTest2: any = {}; +export {EnumTest2}; /** @type {number} */ EnumTest2.XYZ = 0; /** @type {number} */ diff --git a/test_files/export/export.js b/test_files/export/export.js index 14e72dcb6..e8bd62d5e 100644 --- a/test_files/export/export.js +++ b/test_files/export/export.js @@ -9,25 +9,28 @@ exports.Bar = export_helper_1.Bar; exports.export5 = export_helper_1.export5; exports.export4 = export_helper_1.export4; exports.Interface = export_helper_1.Interface; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.export.export_helper'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper"); /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.RenamedTypeDef; // re-export typedef /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.TypeDef; // re-export typedef -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.export.export_helper_2'); +/** @typedef {tsickle_forward_declare_1.DeclaredType} */ +exports.DeclaredType; // re-export typedef +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); // These conflict with an export discovered via the above exports, // so the above export's versions should not show up. exports.export1 = 'wins'; var export_helper_2 = export_helper_1; exports.export3 = export_helper_2.export4; -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.export.export_helper'); +exports.RenamedInterface = export_helper_2.Interface; +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.export.export_helper"); // This local should be fine to export. exports.exportLocal = 3; // The existence of a local should not prevent "export2" from making // it to the exports list. export2 should only show up once in the // above two "export *" lines, though. let /** @type {number} */ export2 = 3; -const tsickle_forward_declare_4 = goog.forwardDeclare('test_files.export.export_helper'); +const tsickle_forward_declare_4 = goog.forwardDeclare("test_files.export.export_helper"); var type_and_value_1 = goog.require('test_files.export.type_and_value'); exports.TypeAndValue = type_and_value_1.TypeAndValue; -const tsickle_forward_declare_5 = goog.forwardDeclare('test_files.export.type_and_value'); +const tsickle_forward_declare_5 = goog.forwardDeclare("test_files.export.type_and_value"); diff --git a/test_files/export/export.js.transform.patch b/test_files/export/export.js.transform.patch new file mode 100644 index 000000000..64db526e6 --- /dev/null +++ b/test_files/export/export.js.transform.patch @@ -0,0 +1,40 @@ +Index: ./test_files/export/export.js +=================================================================== +--- ./test_files/export/export.js golden ++++ ./test_files/export/export.js tsickle with transformer +@@ -4,27 +4,32 @@ + */ + + var export_helper_1 = goog.require('test_files.export.export_helper'); + exports.export2 = export_helper_1.export2; +-exports.Bar = export_helper_1.Bar; + exports.export5 = export_helper_1.export5; + exports.export4 = export_helper_1.export4; +-exports.Interface = export_helper_1.Interface; + const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper"); ++/** @typedef {tsickle_forward_declare_1.Bar} */ ++exports.Bar; // re-export typedef + /** @typedef {tsickle_forward_declare_1.TypeDef} */ + exports.RenamedTypeDef; // re-export typedef + /** @typedef {tsickle_forward_declare_1.TypeDef} */ + exports.TypeDef; // re-export typedef ++/** @typedef {tsickle_forward_declare_1.Interface} */ ++exports.Interface; // re-export typedef + /** @typedef {tsickle_forward_declare_1.DeclaredType} */ + exports.DeclaredType; // re-export typedef ++/** @typedef {tsickle_forward_declare_1.DeclaredInterface} */ ++exports.DeclaredInterface; // re-export typedef + const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); + // These conflict with an export discovered via the above exports, + // so the above export's versions should not show up. + exports.export1 = 'wins'; + var export_helper_2 = export_helper_1; + exports.export3 = export_helper_2.export4; +-exports.RenamedInterface = export_helper_2.Interface; + const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.export.export_helper"); ++/** @typedef {tsickle_forward_declare_3.Interface} */ ++exports.RenamedInterface; // re-export typedef + // This local should be fine to export. + exports.exportLocal = 3; + // The existence of a local should not prevent "export2" from making + // it to the exports list. export2 should only show up once in the diff --git a/test_files/export/export.ts b/test_files/export/export.ts index d67dbf531..c2a9bb6d2 100644 --- a/test_files/export/export.ts +++ b/test_files/export/export.ts @@ -4,7 +4,7 @@ export * from './export_helper_2'; // These conflict with an export discovered via the above exports, // so the above export's versions should not show up. export var export1: string = 'wins'; -export {export4 as export3} from './export_helper'; +export {export4 as export3, Interface as RenamedInterface} from './export_helper'; // This local should be fine to export. export var exportLocal = 3; diff --git a/test_files/export/export.tsickle.ts b/test_files/export/export.tsickle.ts index 94b311cb3..613358944 100644 --- a/test_files/export/export.tsickle.ts +++ b/test_files/export/export.tsickle.ts @@ -3,20 +3,22 @@ * @suppress {checkTypes} checked by tsc */ -export {export2,Bar,export5,RenamedTypeDef,export4,TypeDef,Interface} from './export_helper'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.export.export_helper'); +export {export2,Bar,export5,RenamedTypeDef,export4,TypeDef,Interface,ConstEnum,DeclaredType,DeclaredInterface} from './export_helper'; +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper"); /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.RenamedTypeDef; // re-export typedef /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.TypeDef; // re-export typedef +/** @typedef {tsickle_forward_declare_1.DeclaredType} */ +exports.DeclaredType; // re-export typedef export {} from './export_helper_2'; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.export.export_helper_2'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); // These conflict with an export discovered via the above exports, // so the above export's versions should not show up. export var /** @type {string} */ export1: string = 'wins'; -export {export4 as export3} from './export_helper'; -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.export.export_helper'); +export {export4 as export3, Interface as RenamedInterface} from './export_helper'; +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.export.export_helper"); // This local should be fine to export. export var /** @type {number} */ exportLocal = 3; @@ -28,7 +30,7 @@ let /** @type {number} */ export2 = 3; // This is just an import, so export5 should still be included. import {export5} from './export_helper'; -const tsickle_forward_declare_4 = goog.forwardDeclare('test_files.export.export_helper'); +const tsickle_forward_declare_4 = goog.forwardDeclare("test_files.export.export_helper"); export {TypeAndValue} from './type_and_value'; -const tsickle_forward_declare_5 = goog.forwardDeclare('test_files.export.type_and_value'); +const tsickle_forward_declare_5 = goog.forwardDeclare("test_files.export.type_and_value"); diff --git a/test_files/export/export_helper.js b/test_files/export/export_helper.js index e59195ffc..9874aa554 100644 --- a/test_files/export/export_helper.js +++ b/test_files/export/export_helper.js @@ -8,9 +8,11 @@ goog.module('test_files.export.export_helper');var module = module || {id: 'test var export_helper_2_1 = goog.require('test_files.export.export_helper_2'); exports.export4 = export_helper_2_1.export4; exports.Interface = export_helper_2_1.Interface; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.export.export_helper_2'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper_2"); /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.TypeDef; // re-export typedef +/** @typedef {tsickle_forward_declare_1.DeclaredType} */ +exports.DeclaredType; // re-export typedef exports.export1 = 3; exports.export2 = 3; /** @@ -21,6 +23,6 @@ exports.Bar = Bar; /** @type {number} */ Bar.prototype.barField; exports.export5 = 3; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.export.export_helper_2'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); /** @typedef {tsickle_forward_declare_2.TypeDef} */ exports.RenamedTypeDef; // re-export typedef diff --git a/test_files/export/export_helper.js.transform.patch b/test_files/export/export_helper.js.transform.patch new file mode 100644 index 000000000..e3d7cf892 --- /dev/null +++ b/test_files/export/export_helper.js.transform.patch @@ -0,0 +1,29 @@ +Index: ./test_files/export/export_helper.js +=================================================================== +--- ./test_files/export/export_helper.js golden ++++ ./test_files/export/export_helper.js tsickle with transformer +@@ -6,14 +6,17 @@ + // This file isn't itself a test case, but it is imported by the + // export.in.ts test case. + var export_helper_2_1 = goog.require('test_files.export.export_helper_2'); + exports.export4 = export_helper_2_1.export4; +-exports.Interface = export_helper_2_1.Interface; + const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper_2"); + /** @typedef {tsickle_forward_declare_1.TypeDef} */ + exports.TypeDef; // re-export typedef ++/** @typedef {tsickle_forward_declare_1.Interface} */ ++exports.Interface; // re-export typedef + /** @typedef {tsickle_forward_declare_1.DeclaredType} */ + exports.DeclaredType; // re-export typedef ++/** @typedef {tsickle_forward_declare_1.DeclaredInterface} */ ++exports.DeclaredInterface; // re-export typedef + exports.export1 = 3; + exports.export2 = 3; + /** + * @record +@@ -25,4 +28,5 @@ + exports.export5 = 3; + const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); + /** @typedef {tsickle_forward_declare_2.TypeDef} */ + exports.RenamedTypeDef; // re-export typedef ++// re-export typedef diff --git a/test_files/export/export_helper.tsickle.ts b/test_files/export/export_helper.tsickle.ts index 7b3a73503..708410307 100644 --- a/test_files/export/export_helper.tsickle.ts +++ b/test_files/export/export_helper.tsickle.ts @@ -5,10 +5,12 @@ // This file isn't itself a test case, but it is imported by the // export.in.ts test case. -export {export4,TypeDef,Interface} from './export_helper_2'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.export.export_helper_2'); +export {export4,TypeDef,Interface,ConstEnum,DeclaredType,DeclaredInterface} from './export_helper_2'; +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.export.export_helper_2"); /** @typedef {tsickle_forward_declare_1.TypeDef} */ exports.TypeDef; // re-export typedef +/** @typedef {tsickle_forward_declare_1.DeclaredType} */ +exports.DeclaredType; // re-export typedef export let /** @type {number} */ export1 = 3; export let /** @type {number} */ export2 = 3; /** @@ -25,6 +27,6 @@ export var /** @type {!Bar} */ export3: Bar; export let /** @type {number} */ export5 = 3; export {TypeDef as RenamedTypeDef} from './export_helper_2'; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.export.export_helper_2'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.export.export_helper_2"); /** @typedef {tsickle_forward_declare_2.TypeDef} */ exports.RenamedTypeDef; // re-export typedef diff --git a/test_files/export/export_helper_2.js b/test_files/export/export_helper_2.js index d7bd0dece..4cf9ebf56 100644 --- a/test_files/export/export_helper_2.js +++ b/test_files/export/export_helper_2.js @@ -17,3 +17,7 @@ function Interface() { } exports.Interface = Interface; /** @type {string} */ Interface.prototype.x; +/** @typedef {DeclaredType} */ +exports.DeclaredType; +/** @typedef {DeclaredInterface} */ +exports.DeclaredInterface; diff --git a/test_files/export/export_helper_2.ts b/test_files/export/export_helper_2.ts index c3d59dca0..eb338f3b1 100644 --- a/test_files/export/export_helper_2.ts +++ b/test_files/export/export_helper_2.ts @@ -6,3 +6,15 @@ export let export4 = 3; export type TypeDef = string|number; export interface Interface { x: string; } + +export const enum ConstEnum { + AValue = 1 +} + +export declare type DeclaredType = { + a: number +} + +export declare interface DeclaredInterface { + a: number; +} diff --git a/test_files/export/export_helper_2.tsickle.ts b/test_files/export/export_helper_2.tsickle.ts index ed043ba5e..417f3440f 100644 --- a/test_files/export/export_helper_2.tsickle.ts +++ b/test_files/export/export_helper_2.tsickle.ts @@ -21,3 +21,21 @@ export function Interface() {} Interface.prototype.x; export interface Interface { x: string; } + +export const enum ConstEnum { + AValue = 1 +} + +export declare type DeclaredType = { + a: number +} +/** @typedef {DeclaredType} */ +exports.DeclaredType; + + +export declare interface DeclaredInterface { + a: number; +} +/** @typedef {DeclaredInterface} */ +exports.DeclaredInterface; + diff --git a/test_files/export/externs.js b/test_files/export/externs.js new file mode 100644 index 000000000..5ced7ac10 --- /dev/null +++ b/test_files/export/externs.js @@ -0,0 +1,13 @@ +/** + * @externs + * @suppress {duplicate} + */ +// NOTE: generated by tsickle, do not edit. + +/** @typedef {{a: number}} */ +var DeclaredType; + +/** @record @struct */ +function DeclaredInterface() {} + /** @type {number} */ +DeclaredInterface.prototype.a; diff --git a/test_files/import_from_goog/import_from_goog.js b/test_files/import_from_goog/import_from_goog.js index 0f04b3405..df376edf3 100644 --- a/test_files/import_from_goog/import_from_goog.js +++ b/test_files/import_from_goog/import_from_goog.js @@ -3,9 +3,9 @@ goog.module('test_files.import_from_goog.import_from_goog');var module = module * @suppress {checkTypes} checked by tsc */ -const tsickle_forward_declare_1 = goog.forwardDeclare('closure.Module'); -goog.require('closure.Module'); // force type-only module to be loaded -const tsickle_forward_declare_2 = goog.forwardDeclare('closure.OtherModule'); -goog.require('closure.OtherModule'); // force type-only module to be loaded +const tsickle_forward_declare_1 = goog.forwardDeclare("closure.Module"); +goog.require("closure.Module"); // force type-only module to be loaded +const tsickle_forward_declare_2 = goog.forwardDeclare("closure.OtherModule"); +goog.require("closure.OtherModule"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1} */ x; let /** @type {!tsickle_forward_declare_2.SymA} */ y; diff --git a/test_files/import_from_goog/import_from_goog.tsickle.ts b/test_files/import_from_goog/import_from_goog.tsickle.ts index aae744d04..5dffdfa98 100644 --- a/test_files/import_from_goog/import_from_goog.tsickle.ts +++ b/test_files/import_from_goog/import_from_goog.tsickle.ts @@ -4,12 +4,12 @@ */ import LocalName from 'goog:closure.Module'; -const tsickle_forward_declare_1 = goog.forwardDeclare('closure.Module'); -goog.require('closure.Module'); // force type-only module to be loaded +const tsickle_forward_declare_1 = goog.forwardDeclare("closure.Module"); +goog.require("closure.Module"); // force type-only module to be loaded // tslint:disable-next-line:no-unused-variable import {SymA, SymB} from 'goog:closure.OtherModule'; -const tsickle_forward_declare_2 = goog.forwardDeclare('closure.OtherModule'); -goog.require('closure.OtherModule'); // force type-only module to be loaded +const tsickle_forward_declare_2 = goog.forwardDeclare("closure.OtherModule"); +goog.require("closure.OtherModule"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1} */ x: LocalName; let /** @type {!tsickle_forward_declare_2.SymA} */ y: SymA; diff --git a/test_files/import_only_types/import_only_types.js b/test_files/import_only_types/import_only_types.js index 7d4846b38..8b0a8d236 100644 --- a/test_files/import_only_types/import_only_types.js +++ b/test_files/import_only_types/import_only_types.js @@ -3,7 +3,7 @@ goog.module('test_files.import_only_types.import_only_types');var module = modul * @suppress {checkTypes} checked by tsc */ -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.import_only_types.types_only'); -goog.require('test_files.import_only_types.types_only'); // force type-only module to be loaded +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.import_only_types.types_only"); +goog.require("test_files.import_only_types.types_only"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1.Foo} */ x = { x: 'x' }; console.log(x); diff --git a/test_files/import_only_types/import_only_types.tsickle.ts b/test_files/import_only_types/import_only_types.tsickle.ts index 958c86ab4..447252487 100644 --- a/test_files/import_only_types/import_only_types.tsickle.ts +++ b/test_files/import_only_types/import_only_types.tsickle.ts @@ -4,8 +4,8 @@ */ import {Foo} from './types_only'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.import_only_types.types_only'); -goog.require('test_files.import_only_types.types_only'); // force type-only module to be loaded +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.import_only_types.types_only"); +goog.require("test_files.import_only_types.types_only"); // force type-only module to be loaded let /** @type {!tsickle_forward_declare_1.Foo} */ x: Foo = {x: 'x'}; console.log(x); diff --git a/test_files/index_import/has_index/relative.js b/test_files/index_import/has_index/relative.js index c5c7c6c13..815823154 100644 --- a/test_files/index_import/has_index/relative.js +++ b/test_files/index_import/has_index/relative.js @@ -3,5 +3,5 @@ goog.module('test_files.index_import.has_index.relative');var module = module || * @suppress {checkTypes} checked by tsc */ -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.index_import.has_index.index'); -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.index_import.has_index.index"); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.index_import.has_index.index"); diff --git a/test_files/index_import/has_index/relative.tsickle.ts b/test_files/index_import/has_index/relative.tsickle.ts index ef1468cd7..300f1bfa4 100644 --- a/test_files/index_import/has_index/relative.tsickle.ts +++ b/test_files/index_import/has_index/relative.tsickle.ts @@ -4,7 +4,7 @@ */ import {a as a2} from './index'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.index_import.has_index.index"); import {a} from './index'; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.index_import.has_index.index"); diff --git a/test_files/index_import/user.js b/test_files/index_import/user.js index 408076a9d..981316b40 100644 --- a/test_files/index_import/user.js +++ b/test_files/index_import/user.js @@ -3,13 +3,13 @@ goog.module('test_files.index_import.user');var module = module || {id: 'test_fi * @suppress {checkTypes} checked by tsc */ -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.index_import.has_index.index"); var index_1 = goog.require('test_files.index_import.has_index.index'); exports.a = index_1.a; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.index_import.has_index.index"); var index_2 = index_1; exports.a = index_2.a; -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.index_import.has_index.index'); -const tsickle_forward_declare_4 = goog.forwardDeclare('test_files.index_import.has_index.index'); -const tsickle_forward_declare_5 = goog.forwardDeclare('test_files.index_import.has_index.index'); -const tsickle_forward_declare_6 = goog.forwardDeclare('test_files.index_import.lib'); +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.index_import.has_index.index"); +const tsickle_forward_declare_4 = goog.forwardDeclare("test_files.index_import.has_index.index"); +const tsickle_forward_declare_5 = goog.forwardDeclare("test_files.index_import.has_index.index"); +const tsickle_forward_declare_6 = goog.forwardDeclare("test_files.index_import.lib"); diff --git a/test_files/index_import/user.tsickle.ts b/test_files/index_import/user.tsickle.ts index 427d6f045..de0babcc7 100644 --- a/test_files/index_import/user.tsickle.ts +++ b/test_files/index_import/user.tsickle.ts @@ -5,15 +5,15 @@ /// import {a} from './has_index/index'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.index_import.has_index.index"); export {a} from './has_index/index'; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.index_import.has_index.index"); export {a} from './has_index/index'; -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.index_import.has_index.index"); import {a as a2} from './has_index/index'; -const tsickle_forward_declare_4 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_4 = goog.forwardDeclare("test_files.index_import.has_index.index"); import {a as a3} from './has_index/index.js'; -const tsickle_forward_declare_5 = goog.forwardDeclare('test_files.index_import.has_index.index'); +const tsickle_forward_declare_5 = goog.forwardDeclare("test_files.index_import.has_index.index"); import {b} from './lib'; -const tsickle_forward_declare_6 = goog.forwardDeclare('test_files.index_import.lib'); +const tsickle_forward_declare_6 = goog.forwardDeclare("test_files.index_import.lib"); import * as foo from 'foo'; diff --git a/test_files/interface/implement_import.js b/test_files/interface/implement_import.js index 75063a02c..487b509df 100644 --- a/test_files/interface/implement_import.js +++ b/test_files/interface/implement_import.js @@ -4,7 +4,7 @@ goog.module('test_files.interface.implement_import');var module = module || {id: */ var interface_1 = goog.require('test_files.interface.interface'); -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.interface.interface'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.interface.interface"); /** * @implements {tsickle_forward_declare_1.Point} */ diff --git a/test_files/interface/implement_import.tsickle.ts b/test_files/interface/implement_import.tsickle.ts index ea1b7eba6..ca5f0685c 100644 --- a/test_files/interface/implement_import.tsickle.ts +++ b/test_files/interface/implement_import.tsickle.ts @@ -4,7 +4,7 @@ */ import {Point, User} from './interface'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.interface.interface'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.interface.interface"); /** * @implements {tsickle_forward_declare_1.Point} */ diff --git a/test_files/jsdoc/jsdoc.js.transform.patch b/test_files/jsdoc/jsdoc.js.transform.patch new file mode 100644 index 000000000..40530f411 --- /dev/null +++ b/test_files/jsdoc/jsdoc.js.transform.patch @@ -0,0 +1,16 @@ +Index: ./test_files/jsdoc/jsdoc.js +=================================================================== +--- ./test_files/jsdoc/jsdoc.js golden ++++ ./test_files/jsdoc/jsdoc.js tsickle with transformer +@@ -32,11 +32,8 @@ + /** @const {string} */ + this.badConstThing = 'a'; + } + } +-/** +- * \@internal +- */ + JSDocTest.X = []; + function JSDocTest_tsickle_Closure_declarations() { + /** + * \@internal diff --git a/test_files/jsdoc_types/jsdoc_types.js b/test_files/jsdoc_types/jsdoc_types.js index 33dd28584..673ddd3d1 100644 --- a/test_files/jsdoc_types/jsdoc_types.js +++ b/test_files/jsdoc_types/jsdoc_types.js @@ -9,11 +9,11 @@ goog.module('test_files.jsdoc_types.jsdoc_types');var module = module || {id: 't */ var module1 = goog.require('test_files.jsdoc_types.module1'); var module2_1 = goog.require('test_files.jsdoc_types.module2'); -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.jsdoc_types.module2'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.jsdoc_types.module2"); var default_1 = goog.require('test_files.jsdoc_types.default'); -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.jsdoc_types.default'); -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.jsdoc_types.nevertyped'); -goog.require('test_files.jsdoc_types.nevertyped'); // force type-only module to be loaded +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.jsdoc_types.default"); +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.jsdoc_types.nevertyped"); +goog.require("test_files.jsdoc_types.nevertyped"); // force type-only module to be loaded // Check that imported types get the proper names in JSDoc. let /** @type {!module1.Class} */ useNamespacedClass = new module1.Class(); let /** @type {!module1.Class} */ useNamespacedClassAsType; diff --git a/test_files/jsdoc_types/jsdoc_types.tsickle.ts b/test_files/jsdoc_types/jsdoc_types.tsickle.ts index 557c90e23..a554dc03f 100644 --- a/test_files/jsdoc_types/jsdoc_types.tsickle.ts +++ b/test_files/jsdoc_types/jsdoc_types.tsickle.ts @@ -10,12 +10,12 @@ import * as module1 from './module1'; import {ClassOne, value, ClassOne as RenamedClassOne, ClassTwo as RenamedClassTwo, Interface, ClassWithParams} from './module2'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.jsdoc_types.module2'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.jsdoc_types.module2"); import DefaultClass from './default'; -const tsickle_forward_declare_2 = goog.forwardDeclare('test_files.jsdoc_types.default'); +const tsickle_forward_declare_2 = goog.forwardDeclare("test_files.jsdoc_types.default"); import {NeverTyped} from './nevertyped'; -const tsickle_forward_declare_3 = goog.forwardDeclare('test_files.jsdoc_types.nevertyped'); -goog.require('test_files.jsdoc_types.nevertyped'); // force type-only module to be loaded +const tsickle_forward_declare_3 = goog.forwardDeclare("test_files.jsdoc_types.nevertyped"); +goog.require("test_files.jsdoc_types.nevertyped"); // force type-only module to be loaded // Check that imported types get the proper names in JSDoc. let /** @type {!module1.Class} */ useNamespacedClass = new module1.Class(); diff --git a/test_files/module/.DS_Store b/test_files/module/.DS_Store new file mode 100644 index 000000000..5008ddfcf Binary files /dev/null and b/test_files/module/.DS_Store differ diff --git a/test_files/module/module.js b/test_files/module/module.js new file mode 100644 index 000000000..e23c8ec3d --- /dev/null +++ b/test_files/module/module.js @@ -0,0 +1,8 @@ +goog.module('test_files.module.module');var module = module || {id: 'test_files/module/module.js'};/** + * @fileoverview added by tsickle + * @suppress {checkTypes} checked by tsc + */ +var Reflect; +(function (Reflect) { + const /** @type {number} */ x = 1; +})(Reflect || (Reflect = {})); diff --git a/test_files/module/module.ts b/test_files/module/module.ts new file mode 100644 index 000000000..56678bce5 --- /dev/null +++ b/test_files/module/module.ts @@ -0,0 +1,3 @@ +module Reflect { + const x = 1; +} diff --git a/test_files/module/module.tsickle.ts b/test_files/module/module.tsickle.ts new file mode 100644 index 000000000..117d456c7 --- /dev/null +++ b/test_files/module/module.tsickle.ts @@ -0,0 +1,8 @@ +/** + * @fileoverview added by tsickle + * @suppress {checkTypes} checked by tsc + */ + +module Reflect { + const /** @type {number} */ x = 1; +} diff --git a/test_files/quote_props/quote.js b/test_files/quote_props/quote.js index faf1a9a19..5247b8707 100644 --- a/test_files/quote_props/quote.js +++ b/test_files/quote_props/quote.js @@ -8,8 +8,8 @@ goog.module('test_files.quote_props.quote');var module = module || {id: 'test_fi */ function Quoted() { } let /** @type {!Quoted} */ quoted = {}; -console.log(quoted['hello']); -quoted['hello'] = 1; +console.log(quoted["hello"]); +quoted["hello"] = 1; quoted['hello'] = 1; /** * @record diff --git a/test_files/quote_props/quote.tsickle.ts b/test_files/quote_props/quote.tsickle.ts index de7585d8d..1a804ca57 100644 --- a/test_files/quote_props/quote.tsickle.ts +++ b/test_files/quote_props/quote.tsickle.ts @@ -23,8 +23,8 @@ interface Quoted { } let /** @type {!Quoted} */ quoted: Quoted = {}; -console.log(quoted['hello']); -quoted['hello'] = 1; +console.log(quoted["hello"]); +quoted["hello"] = 1; quoted['hello'] = 1; /** * @record diff --git a/test_files/underscore/underscore.js b/test_files/underscore/underscore.js index d5b95c18c..417e609b6 100644 --- a/test_files/underscore/underscore.js +++ b/test_files/underscore/underscore.js @@ -7,7 +7,7 @@ goog.module('test_files.underscore.underscore');var module = module || {id: 'tes // See getIdentifierText() in tsickle.ts. var export_underscore_1 = goog.require('test_files.underscore.export_underscore'); exports.__test = export_underscore_1.__test; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.underscore.export_underscore'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.underscore.export_underscore"); let /** @type {number} */ __foo = 3; exports.__bar = __foo; class __Class { diff --git a/test_files/underscore/underscore.tsickle.ts b/test_files/underscore/underscore.tsickle.ts index 6f09a2e97..545ccfa34 100644 --- a/test_files/underscore/underscore.tsickle.ts +++ b/test_files/underscore/underscore.tsickle.ts @@ -7,7 +7,7 @@ // See getIdentifierText() in tsickle.ts. export {__test} from './export_underscore'; -const tsickle_forward_declare_1 = goog.forwardDeclare('test_files.underscore.export_underscore'); +const tsickle_forward_declare_1 = goog.forwardDeclare("test_files.underscore.export_underscore"); let /** @type {number} */ __foo = 3; export {__foo as __bar}; diff --git a/test_files/use_closure_externs/use_closure_externs.js.transform.patch b/test_files/use_closure_externs/use_closure_externs.js.transform.patch new file mode 100644 index 000000000..3a94c892c --- /dev/null +++ b/test_files/use_closure_externs/use_closure_externs.js.transform.patch @@ -0,0 +1,12 @@ +Index: ./test_files/use_closure_externs/use_closure_externs.js +=================================================================== +--- ./test_files/use_closure_externs/use_closure_externs.js golden ++++ ./test_files/use_closure_externs/use_closure_externs.js tsickle with transformer +@@ -5,6 +5,6 @@ + */ + console.log('work around TS dropping consecutive comments'); + let /** @type {!NodeListOf} */ x = document.getElementsByTagName('p'); + console.log(x); +-const /** @type {(null|!RegExpExecArray)} */ res = ((/asd/.exec('asd asd'))); ++const /** @type {(null|!RegExpExecArray)} */ res = /** @type {!RegExpExecArray} */ ((/asd/.exec('asd asd'))); + console.log(res);