diff --git a/demo/package.json b/demo/package.json index 0bd9d3e46..3c13d5def 100644 --- a/demo/package.json +++ b/demo/package.json @@ -8,7 +8,7 @@ "dependencies": { "minimist": "^1.2.3", "tsickle": "file:../", - "typescript": "5.2.2" + "typescript": "5.3.2" }, "devDependencies": { "@types/minimist": "1.2.0", diff --git a/package.json b/package.json index b2f9930b5..3bbb1a402 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "out/src/*" ], "peerDependencies": { - "typescript": "~5.1.5" + "typescript": "~5.3.2" }, "devDependencies": { "@types/diff-match-patch": "^1.0.32", @@ -28,7 +28,7 @@ "source-map-support": "^0.5.19", "tslib": "^2.2.0", "tslint": "^6.1.3", - "typescript": "5.2.2" + "typescript": "5.3.2" }, "scripts": { "build": "tsc", diff --git a/src/clutz.ts b/src/clutz.ts index 2e1a818e8..855abab58 100644 --- a/src/clutz.ts +++ b/src/clutz.ts @@ -21,6 +21,11 @@ import * as googmodule from './googmodule'; import * as path from './path'; import {isDeclaredInClutzDts} from './type_translator'; +interface ClutzHost { + /** See compiler_host.ts */ + rootDirsRelative(fileName: string): string; +} + /** * Constructs a ts.CustomTransformerFactory that postprocesses the .d.ts * that are generated by ordinary TypeScript compilations to add some @@ -28,8 +33,7 @@ import {isDeclaredInClutzDts} from './type_translator'; */ export function makeDeclarationTransformerFactory( typeChecker: ts.TypeChecker, - googmoduleHost: googmodule.GoogModuleProcessorHost): - ts.CustomTransformerFactory { + host: ClutzHost&googmodule.GoogModuleProcessorHost): ts.CustomTransformerFactory { return (context: ts.TransformationContext): ts.CustomTransformer => { return { transformBundle(): ts.Bundle { @@ -49,8 +53,7 @@ export function makeDeclarationTransformerFactory( // import 'path/to/the/js_file'; // so to for that import to resolve, you need to first import the clutz // d.ts that defines that declared module. - const imports = - gatherNecessaryClutzImports(googmoduleHost, typeChecker, file); + const imports = gatherNecessaryClutzImports(host, typeChecker, file); let importStmts: ts.Statement[]|undefined; if (imports.length > 0) { importStmts = imports.map(fileName => { @@ -66,22 +69,56 @@ export function makeDeclarationTransformerFactory( // Construct `declare global {}` in the Clutz namespace for symbols // Clutz might use. const globalBlock = generateClutzAliases( - file, googmoduleHost.pathToModuleName('', file.fileName), - typeChecker, options); + file, host.pathToModuleName('', file.fileName), typeChecker, + options); // Only need to transform file if we needed one of the above additions. if (!importStmts && !globalBlock) return file; - return ts.factory.updateSourceFile(file, [ - ...(importStmts ?? []), - ...file.statements, - ...(globalBlock ? [globalBlock] : []), - ]); + return ts.factory.updateSourceFile( + file, + ts.setTextRange( + ts.factory.createNodeArray([ + ...(importStmts ?? []), + ...file.statements, + ...(globalBlock ? [globalBlock] : []), + ]), + file.statements), + file.isDeclarationFile, + file.referencedFiles.map( + f => fixRelativeReference(f, file, options, host)), + // /// directives are ignored under bazel. + /*typeReferences=*/[]); } }; }; } +/** + * Fixes a relative reference from an output file with respect to multiple + * rootDirs. See https://github.com/Microsoft/TypeScript/issues/8245 for + * details. + */ +function fixRelativeReference( + reference: ts.FileReference, origin: ts.SourceFile, + options: ts.CompilerOptions, host: ClutzHost): ts.FileReference { + if (!options.outDir || !options.rootDir) { + return reference; + } + const originDir = path.dirname(origin.fileName); + // Where TypeScript expects the output to be. + const expectedOutDir = + path.join(options.outDir, path.relative(options.rootDir, originDir)); + const referencedFile = path.join(expectedOutDir, reference.fileName); + // Where the output is actually emitted. + const actualOutDir = + path.join(options.outDir, host.rootDirsRelative(originDir)); + const fixedReference = path.relative(actualOutDir, referencedFile); + + reference.fileName = fixedReference; + return reference; +} + /** Compares two strings and returns a number suitable for use in sort(). */ function stringCompare(a: string, b: string): number { if (a < b) return -1; diff --git a/src/decorators.ts b/src/decorators.ts index fb8523ad1..57e6e6903 100644 --- a/src/decorators.ts +++ b/src/decorators.ts @@ -98,7 +98,7 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: return (context: ts.TransformationContext) => { const result: ts.Transformer = (sourceFile: ts.SourceFile) => { let nodeNeedingGoogReflect: undefined|ts.Node = undefined; - const visitor: ts.Visitor = (node) => { + const visitor = (node: ts.Node) => { const replacementNode = rewriteDecorator(node); if (replacementNode) { nodeNeedingGoogReflect = node; @@ -107,9 +107,7 @@ export function transformDecoratorsOutputForClosurePropertyRenaming(diagnostics: return ts.visitEachChild(node, visitor, context); }; let updatedSourceFile = - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion - ts.visitNode(sourceFile, visitor, ts.isSourceFile)!; + ts.visitNode(sourceFile, visitor, ts.isSourceFile); if (nodeNeedingGoogReflect !== undefined) { const statements = [...updatedSourceFile.statements]; const googModuleIndex = statements.findIndex(isGoogModuleStatement); diff --git a/src/enum_transformer.ts b/src/enum_transformer.ts index 812b4fff0..4b7869bdd 100644 --- a/src/enum_transformer.ts +++ b/src/enum_transformer.ts @@ -19,6 +19,7 @@ * type resolve ("@type {Foo}"). */ +import {TsickleHost} from 'tsickle'; import * as ts from 'typescript'; import * as jsdoc from './jsdoc'; @@ -95,7 +96,7 @@ export function getEnumType(typeChecker: ts.TypeChecker, enumDecl: ts.EnumDeclar /** * Transformer factory for the enum transformer. See fileoverview for details. */ -export function enumTransformer(typeChecker: ts.TypeChecker): +export function enumTransformer(host: TsickleHost, typeChecker: ts.TypeChecker): (context: ts.TransformationContext) => ts.Transformer { return (context: ts.TransformationContext) => { function visitor(node: T): T|ts.Node[] { @@ -180,7 +181,11 @@ export function enumTransformer(typeChecker: ts.TypeChecker): /* modifiers */ undefined, ts.factory.createVariableDeclarationList( [varDecl], - /* create a const var */ ts.NodeFlags.Const)), + /* When using unoptimized namespaces, create a var + declaration, otherwise create a const var. See b/157460535 */ + host.useDeclarationMergingTransformation ? + ts.NodeFlags.Const : + undefined)), node), node); diff --git a/src/externs.ts b/src/externs.ts index a30f6caf0..cae60d17d 100644 --- a/src/externs.ts +++ b/src/externs.ts @@ -295,24 +295,13 @@ export function generateExterns( * interface Foo { x: number; } * interface Foo { y: number; } * we only want to emit the "\@record" for Foo on the first one. - * - * The exception are variable declarations, which - in externs - do not assign a value: - * /.. \@type {...} ./ - * var someVariable; - * /.. \@type {...} ./ - * someNamespace.someVariable; - * If a later declaration wants to add additional properties on someVariable, tsickle must still - * emit an assignment into the object, as it's otherwise absent. */ function isFirstValueDeclaration(decl: ts.DeclarationStatement): boolean { if (!decl.name) return true; const sym = typeChecker.getSymbolAtLocation(decl.name)!; if (!sym.declarations || sym.declarations.length < 2) return true; const earlierDecls = sym.declarations.slice(0, sym.declarations.indexOf(decl)); - // Either there are no earlier declarations, or all of them are variables (see above). tsickle - // emits a value for all other declaration kinds (function for functions, classes, interfaces, - // {} object for namespaces). - return earlierDecls.length === 0 || earlierDecls.every(ts.isVariableDeclaration); + return earlierDecls.length === 0 || earlierDecls.every(d => ts.isVariableDeclaration(d) && d.getSourceFile() !== decl.getSourceFile()); } /** Writes the actual variable statement of a Closure variable declaration. */ diff --git a/src/googmodule.ts b/src/googmodule.ts index bf6e6d91b..fce705bec 100644 --- a/src/googmodule.ts +++ b/src/googmodule.ts @@ -29,11 +29,9 @@ export interface GoogModuleProcessorHost { * Takes the import URL of an ES6 import and returns the googmodule module * name for the imported module, iff the module is an original closure * JavaScript file. - * - * Warning: If this function is present, GoogModule won't produce diagnostics - * for multiple provides. */ - jsPathToModuleName?(importPath: string): string|undefined; + jsPathToModuleName? + (importPath: string): {name: string, multipleProvides: boolean}|undefined; /** * Takes the import URL of an ES6 import and returns the property name that * should be stripped from the usage. @@ -89,7 +87,8 @@ export function jsPathToNamespace( host: GoogModuleProcessorHost, context: ts.Node, diagnostics: ts.Diagnostic[], importPath: string, getModuleSymbol: () => ts.Symbol | undefined): string|undefined { - const namespace = localJsPathToNamespace(host, importPath); + const namespace = + localJsPathToNamespace(host, context, diagnostics, importPath); if (namespace) return namespace; const moduleSymbol = getModuleSymbol(); @@ -105,7 +104,8 @@ export function jsPathToNamespace( * Forwards to `jsPathToModuleName` on the host if present. */ export function localJsPathToNamespace( - host: GoogModuleProcessorHost, importPath: string): string|undefined { + host: GoogModuleProcessorHost, context: ts.Node|undefined, + diagnostics: ts.Diagnostic[], importPath: string): string|undefined { if (importPath.match(/^goog:/)) { // This is a namespace import, of the form "goog:foo.bar". // Fix it to just "foo.bar". @@ -113,7 +113,12 @@ export function localJsPathToNamespace( } if (host.jsPathToModuleName) { - return host.jsPathToModuleName(importPath); + const module = host.jsPathToModuleName(importPath); + if (!module) return undefined; + if (module.multipleProvides) { + reportMultipleProvidesError(context, diagnostics, importPath); + } + return module.name; } return undefined; @@ -394,10 +399,7 @@ function getGoogNamespaceFromClutzComments( findLocalInDeclarations(moduleSymbol, '__clutz_multiple_provides'); if (hasMultipleProvides) { // Report an error... - reportDiagnostic( - tsickleDiagnostics, context, - `referenced JavaScript module ${ - tsImport} provides multiple namespaces and cannot be imported by path.`); + reportMultipleProvidesError(context, tsickleDiagnostics, tsImport); // ... but continue producing an emit that effectively references the first // provided symbol (to continue finding any additional errors). } @@ -411,6 +413,15 @@ function getGoogNamespaceFromClutzComments( return actualNamespace; } +function reportMultipleProvidesError( + context: ts.Node|undefined, diagnostics: ts.Diagnostic[], + importPath: string) { + reportDiagnostic( + diagnostics, context, + `referenced JavaScript module ${ + importPath} provides multiple namespaces and cannot be imported by path.`); +} + /** * Converts a TS/ES module './import/path' into a goog.module compatible * namespace, handling regular imports and `goog:` namespace imports. @@ -446,27 +457,6 @@ function rewriteModuleExportsAssignment(expr: ts.ExpressionStatement) { expr); } -/** - * Checks whether expr is of the form `exports.abc = identifier` and if so, - * returns the string abc, otherwise returns null. - */ -function isExportsAssignment(expr: ts.Expression): string|null { - // Verify this looks something like `exports.abc = ...`. - if (!ts.isBinaryExpression(expr)) return null; - if (expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null; - - // Verify the left side of the expression is an access on `exports`. - if (!ts.isPropertyAccessExpression(expr.left)) return null; - if (!ts.isIdentifier(expr.left.expression)) return null; - if (expr.left.expression.escapedText !== 'exports') return null; - - // Check whether right side of assignment is an identifier. - if (!ts.isIdentifier(expr.right)) return null; - - // Return the property name as string. - return expr.left.name.escapedText.toString(); -} - /** * Convert a series of comma-separated expressions * x = foo, y(), z.bar(); @@ -976,13 +966,19 @@ export function commonJsToGoogmoduleTransformer( // onSubstituteNode callback. ts.setEmitFlags(arg.right.expression, ts.EmitFlags.NoSubstitution); - // Namespaces can merge with classes and functions. TypeScript emits - // separate exports assignments for those. Don't emit extra ones here. + // Namespaces can merge with classes and functions and TypeScript emits + // separate exports assignments for those already. No need to add an + // extra one. + // The same is true for enums, but only if they have been transformed + // to closure enums. const notAlreadyExported = matchingExports.filter( decl => !ts.isClassDeclaration( decl.declarationSymbol.valueDeclaration) && !ts.isFunctionDeclaration( - decl.declarationSymbol.valueDeclaration)); + decl.declarationSymbol.valueDeclaration) && + !(host.transformTypesToClosure && + ts.isEnumDeclaration( + decl.declarationSymbol.valueDeclaration))); const exportNames = notAlreadyExported.map(decl => decl.exportName); return { @@ -1147,7 +1143,6 @@ export function commonJsToGoogmoduleTransformer( return exportStmt; } - const exportsSeen = new Set(); const seenNamespaceOrEnumExports = new Set(); /** @@ -1245,53 +1240,27 @@ export function commonJsToGoogmoduleTransformer( return; } - // Avoid EXPORT_REPEATED_ERROR from JSCompiler. Occurs for: - // class Foo {} - // namespace Foo { ... } - // export {Foo}; - // TypeScript emits 2 separate exports assignments. One after the - // class and one after the namespace. - // TODO(b/277272562): TypeScript 5.1 changes how exports assignments - // are emitted, making this no longer an issue. On the other hand - // this is unsafe. We really need to keep the _last_ (not the first) - // export assignment in the general case. Remove this check after - // the 5.1 upgrade. - const exportName = isExportsAssignment(exprStmt.expression); - if (exportName) { - if (exportsSeen.has(exportName)) { - stmts.push(createNotEmittedStatementWithComments(sf, exprStmt)); - return; - } - exportsSeen.add(exportName); - } - - // TODO(b/277272562): This code works in 5.1. But breaks in 5.0, - // which emits separate exports assignments for namespaces and enums - // and this code would emit duplicate exports assignments. Run this - // unconditionally after 5.1 has been released. - if ((ts.versionMajorMinor as string) !== '5.0') { - // Check for inline exports assignments as they are emitted for - // exported namespaces and enums, e.g.: - // (function (Foo) { - // })(Foo || (exports.Foo = exports.Bar = Foo = {})); - // and moves the exports assignments to a separate statement. - const exportInIifeArguments = - maybeRewriteExportsAssignmentInIifeArguments(exprStmt); - if (exportInIifeArguments) { - stmts.push(exportInIifeArguments.statement); - for (const newExport of exportInIifeArguments.exports) { - const exportName = newExport.expression.left.name.text; - // Namespaces produce multiple exports assignments when - // they're re-opened in the same file. Only emit the first one - // here. This is fine because the namespace object itself - // cannot be re-assigned later. - if (!seenNamespaceOrEnumExports.has(exportName)) { - stmts.push(newExport); - seenNamespaceOrEnumExports.add(exportName); - } + // Check for inline exports assignments as they are emitted for + // exported namespaces and enums, e.g.: + // (function (Foo) { + // })(Foo || (exports.Foo = exports.Bar = Foo = {})); + // and moves the exports assignments to a separate statement. + const exportInIifeArguments = + maybeRewriteExportsAssignmentInIifeArguments(exprStmt); + if (exportInIifeArguments) { + stmts.push(exportInIifeArguments.statement); + for (const newExport of exportInIifeArguments.exports) { + const exportName = newExport.expression.left.name.text; + // Namespaces produce multiple exports assignments when + // they're re-opened in the same file. Only emit the first one + // here. This is fine because the namespace object itself + // cannot be re-assigned later. + if (!seenNamespaceOrEnumExports.has(exportName)) { + stmts.push(newExport); + seenNamespaceOrEnumExports.add(exportName); } - return; } + return; } // Delay `exports.X = X` assignments for decorated classes. @@ -1473,7 +1442,7 @@ export function commonJsToGoogmoduleTransformer( 'requireDynamic', createSingleQuoteStringLiteral(imp)); } - const visitForDynamicImport: ts.Visitor = (node) => { + const visitForDynamicImport = (node: ts.Node) => { const replacementNode = rewriteDynamicRequire(node); if (replacementNode) { return replacementNode; @@ -1482,9 +1451,7 @@ export function commonJsToGoogmoduleTransformer( }; if (host.transformDynamicImport === 'closure') { - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion - sf = ts.visitNode(sf, visitForDynamicImport, ts.isSourceFile)!; + sf = ts.visitNode(sf, visitForDynamicImport, ts.isSourceFile); } // Convert each top level statement to goog.module. diff --git a/src/jsdoc_transformer.ts b/src/jsdoc_transformer.ts index 6e3e5119b..5775b2b9a 100644 --- a/src/jsdoc_transformer.ts +++ b/src/jsdoc_transformer.ts @@ -827,8 +827,6 @@ export function jsdocTransformer( const updatedBinding = renameArrayBindings(decl.name, aliases); if (updatedBinding && aliases.length > 0) { const declVisited = - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion ts.visitNode(decl, visitor, ts.isVariableDeclaration)!; const newDecl = ts.factory.updateVariableDeclaration( declVisited, updatedBinding, declVisited.exclamationToken, @@ -846,10 +844,9 @@ export function jsdocTransformer( continue; } } - const newDecl = - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion - ts.visitNode(decl, visitor, ts.isVariableDeclaration)!; + const newDecl = ts.setEmitFlags( + ts.visitNode(decl, visitor, ts.isVariableDeclaration)!, + ts.EmitFlags.NoComments); const newStmt = ts.factory.createVariableStatement( varStmt.modifiers, ts.factory.createVariableDeclarationList([newDecl], flags)); @@ -1322,8 +1319,6 @@ export function jsdocTransformer( e, e.dotDotDotToken, ts.visitNode(e.propertyName, visitor, ts.isPropertyName), updatedBindingName, - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion ts.visitNode(e.initializer, visitor) as ts.Expression)); } return ts.factory.updateArrayBindingPattern(node, updatedElements); @@ -1406,22 +1401,15 @@ export function jsdocTransformer( if (ts.isBlock(node.statement)) { updatedStatement = ts.factory.updateBlock(node.statement, [ ...aliasDecls, - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion ...ts.visitNode(node.statement, visitor, ts.isBlock)!.statements ]); } else { updatedStatement = ts.factory.createBlock([ - ...aliasDecls, - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion - ts.visitNode(node.statement, visitor) as ts.Statement + ...aliasDecls, ts.visitNode(node.statement, visitor) as ts.Statement ]); } return ts.factory.updateForOfStatement( node, node.awaitModifier, updatedInitializer, - // TODO: go/ts50upgrade - Remove after upgrade. - // tslint:disable-next-line:no-unnecessary-type-assertion ts.visitNode(node.expression, visitor) as ts.Expression, updatedStatement); } diff --git a/src/ns_transformer.ts b/src/ns_transformer.ts index 67300c942..129c15a3d 100644 --- a/src/ns_transformer.ts +++ b/src/ns_transformer.ts @@ -72,8 +72,8 @@ export function namespaceTransformer( // transformation fails. function transformNamespace( ns: ts.ModuleDeclaration, - mergedDecl: ts.ClassDeclaration| - ts.InterfaceDeclaration): ts.Statement[] { + mergedDecl: ts.ClassDeclaration|ts.InterfaceDeclaration| + ts.EnumDeclaration): ts.Statement[] { if (!ns.body || !ts.isModuleBlock(ns.body)) { if (ts.isModuleDeclaration(ns)) { error( @@ -83,10 +83,15 @@ export function namespaceTransformer( return [ns]; } const nsName = getIdentifierText(ns.name as ts.Identifier); + const mergingWithEnum = ts.isEnumDeclaration(mergedDecl); const transformedNsStmts: ts.Statement[] = []; for (const stmt of ns.body.statements) { if (ts.isEmptyStatement(stmt)) continue; if (ts.isClassDeclaration(stmt)) { + if (mergingWithEnum) { + errorNotAllowed(stmt, 'class'); + continue; + } transformInnerDeclaration( stmt, (classDecl, notExported, hoistedIdent) => { return ts.factory.updateClassDeclaration( @@ -95,12 +100,20 @@ export function namespaceTransformer( classDecl.members); }); } else if (ts.isEnumDeclaration(stmt)) { + if (mergingWithEnum) { + errorNotAllowed(stmt, 'enum'); + continue; + } transformInnerDeclaration( stmt, (enumDecl, notExported, hoistedIdent) => { return ts.factory.updateEnumDeclaration( enumDecl, notExported, hoistedIdent, enumDecl.members); }); } else if (ts.isInterfaceDeclaration(stmt)) { + if (mergingWithEnum) { + errorNotAllowed(stmt, 'interface'); + continue; + } transformInnerDeclaration( stmt, (interfDecl, notExported, hoistedIdent) => { return ts.factory.updateInterfaceDeclaration( @@ -109,6 +122,10 @@ export function namespaceTransformer( interfDecl.members); }); } else if (ts.isTypeAliasDeclaration(stmt)) { + if (mergingWithEnum) { + errorNotAllowed(stmt, 'type alias'); + continue; + } transformTypeAliasDeclaration(stmt); } else if (ts.isVariableStatement(stmt)) { if ((ts.getCombinedNodeFlags(stmt.declarationList) & @@ -116,13 +133,28 @@ export function namespaceTransformer( error( stmt, 'non-const values are not supported. (go/ts-merged-namespaces)'); + continue; } if (!ts.isInterfaceDeclaration(mergedDecl)) { error( stmt, 'const declaration only allowed when merging with an interface (go/ts-merged-namespaces)'); + continue; } transformConstDeclaration(stmt); + } else if (ts.isFunctionDeclaration(stmt)) { + if (!ts.isEnumDeclaration(mergedDecl)) { + error( + stmt, + 'function declaration only allowed when merging with an enum (go/ts-merged-namespaces)'); + } + transformInnerDeclaration( + stmt, (funcDecl, notExported, hoistedIdent) => { + return ts.factory.updateFunctionDeclaration( + funcDecl, notExported, funcDecl.asteriskToken, + hoistedIdent, funcDecl.typeParameters, + funcDecl.parameters, funcDecl.type, funcDecl.body); + }); } else { error( stmt, @@ -145,6 +177,12 @@ export function namespaceTransformer( // Local functions follow. + function errorNotAllowed(stmt: ts.Statement, declKind: string) { + error( + stmt, + `${declKind} cannot be merged with enum declaration. (go/ts-merged-namespaces)`); + } + type DeclarationStatement = ts.Declaration&ts.DeclarationStatement; function transformConstDeclaration(varDecl: ts.VariableStatement) { @@ -365,12 +403,13 @@ export function namespaceTransformer( } if (!ts.isInterfaceDeclaration(mergedDecl) && - !ts.isClassDeclaration(mergedDecl)) { - // The previous declaration is not a class or interface. + !ts.isClassDeclaration(mergedDecl) && + !ts.isEnumDeclaration(mergedDecl)) { + // The previous declaration is not a class, enum, or interface. transformedStmts.push(ns); // Nothing to do here. error( ns.name, - 'merged declaration must be local class or interface. (go/ts-merged-namespaces)'); + 'merged declaration must be local class, enum, or interface. (go/ts-merged-namespaces)'); return; } diff --git a/src/summary.ts b/src/summary.ts index ffbff10fb..9f2623345 100644 --- a/src/summary.ts +++ b/src/summary.ts @@ -54,6 +54,7 @@ export class FileSummary { modName: string|undefined; autochunk = false; enhanceable = false; + legacyNamespace = false; moduleType = ModuleType.UNKNOWN; private stringify(symbol: Symbol): string { diff --git a/src/transformer_util.ts b/src/transformer_util.ts index 6c60ef0dd..8489f8fe8 100644 --- a/src/transformer_util.ts +++ b/src/transformer_util.ts @@ -152,7 +152,7 @@ export function updateSourceFileNode( } sf = ts.factory.updateSourceFile( sf, - statements, + ts.setTextRange(statements, sf.statements), sf.isDeclarationFile, sf.referencedFiles, sf.typeReferenceDirectives, @@ -227,28 +227,30 @@ export function reportDebugWarning( * @param textRange pass to overrride the text range from the node with a more specific range. */ export function reportDiagnostic( - diagnostics: ts.Diagnostic[], node: ts.Node, messageText: string, textRange?: ts.TextRange, - category = ts.DiagnosticCategory.Error) { + diagnostics: ts.Diagnostic[], node: ts.Node|undefined, messageText: string, + textRange?: ts.TextRange, category = ts.DiagnosticCategory.Error) { diagnostics.push(createDiagnostic(node, messageText, textRange, category)); } function createDiagnostic( - node: ts.Node, messageText: string, textRange: ts.TextRange|undefined, + node: ts.Node|undefined, messageText: string, + textRange: ts.TextRange|undefined, category: ts.DiagnosticCategory): ts.Diagnostic { - let start, length: number; + let start: number|undefined; + let length: number|undefined; // getStart on a synthesized node can crash (due to not finding an associated // source file). Make sure to use the original node. node = ts.getOriginalNode(node); if (textRange) { start = textRange.pos; length = textRange.end - textRange.pos; - } else { + } else if (node) { // Only use getStart if node has a valid pos, as it might be synthesized. start = node.pos >= 0 ? node.getStart() : 0; length = node.end - node.pos; } return { - file: node.getSourceFile(), + file: node?.getSourceFile(), start, length, messageText, @@ -431,4 +433,4 @@ export function getPreviousDeclaration( } } return null; -} \ No newline at end of file +} diff --git a/src/ts_migration_exports_shim.ts b/src/ts_migration_exports_shim.ts index ac715764b..0136ea45e 100644 --- a/src/ts_migration_exports_shim.ts +++ b/src/ts_migration_exports_shim.ts @@ -457,6 +457,9 @@ class Generator { fileSummary.addStrongRequire({type: Type.CLOSURE, name: 'goog'}); fileSummary.addStrongRequire( {type: Type.CLOSURE, name: this.srcIds.googModuleId}); + if (maybeDeclareLegacyNameCall) { + fileSummary.legacyNamespace = true; + } fileSummary.autochunk = isAutoChunk; fileSummary.moduleType = ModuleType.GOOG_MODULE; diff --git a/src/tsickle.ts b/src/tsickle.ts index 3b10f073a..ac0db0b80 100644 --- a/src/tsickle.ts +++ b/src/tsickle.ts @@ -8,6 +8,7 @@ import * as ts from 'typescript'; +import * as path from './path'; import {AnnotatorHost} from './annotator_host'; import {assertAbsolute} from './cli_support'; import * as clutz from './clutz'; @@ -23,7 +24,6 @@ import {namespaceTransformer} from './ns_transformer'; import {FileSummary, SummaryGenerationProcessorHost} from './summary'; import {isDtsFileName} from './transformer_util'; import * as tsmes from './ts_migration_exports_shim'; -import {makeTsickleDeclarationMarkerTransformerFactory} from './tsickle_declaration_marker'; // Exported for users as a default impl of pathToModuleName. export {pathToModuleName} from './cli_support'; @@ -154,6 +154,25 @@ export interface EmitTransformers { } +function writeWithTsickleHeader( + writeFile: ts.WriteFileCallback, rootDir: string) { + return (fileName: string, content: string, writeByteOrderMark: boolean, + onError: ((message: string) => void)|undefined, + sourceFiles: readonly ts.SourceFile[]|undefined, + data: ts.WriteFileCallbackData|undefined) => { + if (fileName.endsWith('.d.ts')) { + // Add tsickle header. + const sources = + sourceFiles?.map(sf => path.relative(rootDir, sf.fileName)); + content = `//!! generated by tsickle from ${ + sources?.join(' ') || '???'}\n${content}`; + } + + writeFile( + fileName, content, writeByteOrderMark, onError, sourceFiles, data); + }; +} + /** * @deprecated Exposed for backward compat with Angular. Use emit() instead. */ @@ -222,7 +241,7 @@ export function emit( } tsickleSourceTransformers.push( jsdocTransformer(host, tsOptions, typeChecker, tsickleDiagnostics)); - tsickleSourceTransformers.push(enumTransformer(typeChecker)); + tsickleSourceTransformers.push(enumTransformer(host, typeChecker)); } if (host.transformDecorators) { tsickleSourceTransformers.push( @@ -254,14 +273,9 @@ export function emit( clutz.makeDeclarationTransformerFactory(typeChecker, host)); } - // Adds a marker to the top of tsickle-generated .d.ts files, should always go - // last - tsTransformers.afterDeclarations!.push( - makeTsickleDeclarationMarkerTransformerFactory(tsOptions)); - const {diagnostics: tsDiagnostics, emitSkipped, emittedFiles} = program.emit( - targetSourceFile, writeFile, cancellationToken, emitOnlyDtsFiles, - tsTransformers); + targetSourceFile, writeWithTsickleHeader(writeFile, tsOptions.rootDir), + cancellationToken, emitOnlyDtsFiles, tsTransformers); const externs: {[fileName: string]: {output: string, moduleNamespace: string}} = {}; diff --git a/src/tsickle_declaration_marker.ts b/src/tsickle_declaration_marker.ts deleted file mode 100644 index 76af06e42..000000000 --- a/src/tsickle_declaration_marker.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @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 path from './path'; -import {createNotEmittedStatement, updateSourceFileNode} from './transformer_util'; - -/** Marks tsickle generated .d.ts's with a comment we can find later. */ -export function makeTsickleDeclarationMarkerTransformerFactory( - options: ts.CompilerOptions): ts.CustomTransformerFactory { - return (context: ts.TransformationContext): ts.CustomTransformer => { - return { - transformBundle(): ts.Bundle { - // The TS API wants declaration transfomers to be able to handle Bundle, - // but we don't support them within tsickle. - throw new Error('did not expect to transform a bundle'); - }, - transformSourceFile(sf: ts.SourceFile): ts.SourceFile { - if (!options.rootDir) return sf; - let syntheticFirstStatement = createNotEmittedStatement(sf); - syntheticFirstStatement = ts.addSyntheticTrailingComment( - syntheticFirstStatement, ts.SyntaxKind.SingleLineCommentTrivia, - `!! generated by tsickle from ${ - path.relative(options.rootDir, sf.fileName)}`, - /*hasTrailingNewLine=*/ true); - return updateSourceFileNode(sf, ts.factory.createNodeArray([ - syntheticFirstStatement, ...sf.statements - ])); - } - }; - }; -} diff --git a/src/type_translator.ts b/src/type_translator.ts index 324ec77e8..1464a7cb4 100644 --- a/src/type_translator.ts +++ b/src/type_translator.ts @@ -860,6 +860,20 @@ export class TypeTranslator { if (sigs.length === 1) { return this.signatureToClosure(sigs[0]); } + // Function has multiple declaration. Let's see if we can find a single + // declaration with an implementation. In this case all the other + // declarations are overloads and the implementation must have a + // signature that matches all of them. + const declWithBody = type.symbol.declarations?.filter( + (d): d is ts.FunctionLikeDeclaration => + isFunctionLikeDeclaration(d) && d.body != null); + if (declWithBody?.length === 1) { + const sig = + this.typeChecker.getSignatureFromDeclaration(declWithBody[0]); + if (sig) { + return this.signatureToClosure(sig); + } + } this.warn('unhandled anonymous type with multiple call signatures'); return '?'; } @@ -1173,3 +1187,11 @@ export function restParameterType( } return typeArgs[0]; } + +function isFunctionLikeDeclaration(node: ts.Node): + node is ts.FunctionLikeDeclaration { + return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) || ts.isFunctionExpression(node) || + ts.isArrowFunction(node); +} diff --git a/test/googmodule_test.ts b/test/googmodule_test.ts index a258a7bcf..4d804cbb5 100644 --- a/test/googmodule_test.ts +++ b/test/googmodule_test.ts @@ -13,6 +13,7 @@ import * as googmodule from '../src/googmodule'; import {ModulesManifest} from '../src/modules_manifest'; import * as testSupport from './test_support'; +import {outdent} from './test_support'; interface ResolvedNamespace { name: string; @@ -42,8 +43,14 @@ function processES5( transformDynamicImport: 'closure', }; if (pathToNamespaceMap) { - host.jsPathToModuleName = (importPath: string) => - pathToNamespaceMap.get(importPath)?.name; + host.jsPathToModuleName = (importPath: string) => { + const module = pathToNamespaceMap.get(importPath); + if (!module) return undefined; + return { + name: module.name, + multipleProvides: false, + }; + }; host.jsPathToStripProperty = (importPath: string) => pathToNamespaceMap.get(importPath)?.stripProperty; } @@ -71,21 +78,6 @@ function processES5( return {output, manifest, rootDir}; } -/** - * Remove the first line (if empty) and unindents the all other lines by the - * amount of leading whitespace in the second line. - */ -function outdent(str: string) { - const lines = str.split('\n'); - if (lines.length < 2) return str; - if (lines.shift() !== '') return str; - const indent = lines[0].match(/^ */)![0].length; - for (let i = 0; i < lines.length; i++) { - lines[i] = lines[i].substring(indent); - } - return lines.join('\n'); -} - describe('convertCommonJsToGoogModule', () => { beforeEach(() => { testSupport.addDiffMatchers(); diff --git a/test/test_support.ts b/test/test_support.ts index c270ac144..6f0f2b826 100644 --- a/test/test_support.ts +++ b/test/test_support.ts @@ -453,3 +453,18 @@ export function pathToModuleName( if (fileName === tslibPath()) return 'tslib'; return cliSupport.pathToModuleName(rootModulePath, context, fileName); } + +/** + * Remove the first line (if empty) and unindents the all other lines by the + * amount of leading whitespace in the second line. + */ +export function outdent(str: string) { + const lines = str.split('\n'); + if (lines.length < 2) return str; + if (lines.shift() !== '') return str; + const indent = lines[0].match(/^ */)![0].length; + for (let i = 0; i < lines.length; i++) { + lines[i] = lines[i].substring(indent); + } + return lines.join('\n'); +} diff --git a/test/tsickle_test.ts b/test/tsickle_test.ts index 7fbb9592a..11d59e3e7 100644 --- a/test/tsickle_test.ts +++ b/test/tsickle_test.ts @@ -13,14 +13,14 @@ import {assertAbsolute} from '../src/cli_support'; import * as tsickle from '../src/tsickle'; import * as testSupport from './test_support'; +import {outdent} from './test_support'; describe('emitWithTsickle', () => { function emitWithTsickle( tsSources: {[fileName: string]: string}, tsConfigOverride: Partial = {}, tsickleHostOverride: Partial = {}, - customTransformers?: tsickle.EmitTransformers): - {[fileName: string]: string} { + customTransformers?: tsickle.EmitTransformers) { const tsCompilerOptions: ts.CompilerOptions = { ...testSupport.compilerOptions, target: ts.ScriptTarget.ES5, @@ -55,13 +55,13 @@ describe('emitWithTsickle', () => { return importPath.replace(/\/|\\/g, '.'); }, fileNameToModuleId: (fileName) => fileName.replace(/^\.\//, ''), - ...tsickleHostOverride, options: tsCompilerOptions, rootDirsRelative: testSupport.relativeToTsickleRoot, - transformDynamicImport: 'closure' + transformDynamicImport: 'closure', + ...tsickleHostOverride, }; const jsSources: {[fileName: string]: string} = {}; - tsickle.emit( + const {diagnostics} = tsickle.emit( program, tsickleHost, (fileName: string, data: string) => { jsSources[path.relative(tsCompilerOptions.rootDir!, fileName)] = data; @@ -69,7 +69,7 @@ describe('emitWithTsickle', () => { /* sourceFile */ undefined, /* cancellationToken */ undefined, /* emitOnlyDtsFiles */ undefined, customTransformers); - return jsSources; + return {jsSources, diagnostics}; } @@ -91,7 +91,7 @@ describe('emitWithTsickle', () => { const tsSources = { 'a.ts': `export const x = 1;`, }; - const jsSources = emitWithTsickle( + const {jsSources} = emitWithTsickle( tsSources, undefined, { shouldSkipTsickleProcessing: () => true, }, @@ -106,12 +106,10 @@ describe('emitWithTsickle', () => { 'b.ts': `export * from './a';`, }; - const jsSources = emitWithTsickle( - tsSources, { - preserveConstEnums: true, - module: ts.ModuleKind.ES2015, - }, - {googmodule: false}); + const {jsSources} = emitWithTsickle(tsSources, { + preserveConstEnums: true, + module: ts.ModuleKind.ES2015, + }); expect(jsSources['b.js']).toContain(`export { Foo } from './a';`); }); @@ -121,16 +119,62 @@ describe('emitWithTsickle', () => { 'a.ts': `export function f() : typeof f { return f; }`, }; - const jsSources = emitWithTsickle(tsSources, { + const {jsSources} = emitWithTsickle(tsSources, { module: ts.ModuleKind.ES2015, }); - expect(jsSources['a.js']).toContain(` -/** - * @return {function(): ?} - */ -export function f() { return f; } -`); + expect(jsSources['a.js']).toContain(outdent(` + /** + * @return {function(): ?} + */ + export function f() { return f; } + `)); + }); + + it('reports multi-provides error with jsPathToModuleName impl', () => { + const tsSources = { + 'a.ts': `import {} from 'google3/multi/provide';`, + 'clutz.d.ts': `declare module 'google3/multi/provide' { export {}; }`, + }; + const {diagnostics} = + emitWithTsickle( + tsSources, /* tsConfigOverride= */ undefined, + /* tsickleHostOverride= */ { + jsPathToModuleName(importPath: string) { + if (importPath === 'google3/multi/provide') { + return { + name: 'multi.provide', + multipleProvides: true, + }; + } + return undefined; + } + }); + expect(testSupport.formatDiagnostics(diagnostics)) + .toContain( + 'referenced JavaScript module google3/multi/provide provides multiple namespaces and cannot be imported by path'); + }); + + it('allows side-effect import of multi-provides module', () => { + const tsSources = { + 'a.ts': `import 'google3/multi/provide';`, + 'clutz.d.ts': `declare module 'google3/multi/provide' { export {}; }`, + }; + const {jsSources} = emitWithTsickle( + tsSources, /* tsConfigOverride= */ undefined, + /* tsickleHostOverride= */ { + googmodule: true, + jsPathToModuleName(importPath: string) { + if (importPath === 'google3/multi/provide') { + return { + name: 'multi.provide', + multipleProvides: true, + }; + } + return undefined; + }, + }); + expect(jsSources['a.js']).toContain(`goog.require('multi.provide');`); }); describe('regressions', () => { @@ -140,16 +184,15 @@ export function f() { return f; } 'a.ts': `export const x = 1;`, 'b.ts': `export * from './a';\n`, }; - const jsSources = emitWithTsickle( - tsSources, { - declaration: true, - module: ts.ModuleKind.ES2015, - }, - {googmodule: false}); - - expect(jsSources['b.d.ts']) - .toEqual(`//!! generated by tsickle from b.ts -export * from './a';\n`); + const {jsSources} = emitWithTsickle(tsSources, { + declaration: true, + module: ts.ModuleKind.ES2015, + }); + + expect(jsSources['b.d.ts']).toEqual(outdent(` + //!! generated by tsickle from b.ts + export * from './a'; + `)); }); }); }); diff --git a/test_files/augment/externs.js b/test_files/augment/externs.js index 18fb334ad..077c7e11f 100644 --- a/test_files/augment/externs.js +++ b/test_files/augment/externs.js @@ -8,8 +8,6 @@ var test_files$augment$angular$index_ = {}; /** @type {!test_files$augment$angular$index_.angular.IAngularStatic} */ test_files$augment$angular$index_.angular; -/** @const */ -test_files$augment$angular$index_.angular = {}; /** * @record * @struct diff --git a/test_files/clutz_imports.declaration.no_externs/clutz2_output_demo8.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz2_output_demo8.d.ts deleted file mode 100644 index 3874d54c0..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz2_output_demo8.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -//!! generated by clutz2 -/** - * @fileoverview This file contains the Clutz2 output for a simple goog.provide. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz { - namespace demo8 { - export class C { - private noStructuralTyping_demo8$C: any; - } - } // namespace demo8 -} // ಠ_ಠ.clutz - -declare module 'goog:demo8' { - import demo8 = ಠ_ಠ.clutz.demo8; - export default demo8; -} diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo1.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo1.d.ts deleted file mode 100644 index 923e8b723..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo1.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.module. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz.module$exports$demo1 { - class C { - private noStructuralTyping_module$exports$demo1_C: any; - foo(): void; - } -} -declare module 'goog:demo1' { -import demo1 = ಠ_ಠ.clutz.module$exports$demo1; - export = demo1; -} diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo2.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo2.d.ts deleted file mode 100644 index 52c32f839..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo2.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.provide. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz.demo2 { - class C { - private noStructuralTyping_demo2_C: any; - bar(): void; - } -} -declare module 'goog:demo2' { -import demo2 = ಠ_ಠ.clutz.demo2; - export = demo2; -} diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo3.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo3.d.ts deleted file mode 100644 index 5c68d9376..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo3.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.module. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz { - class module$exports$demo3 { - private noStructuralTyping_module$exports$demo3: any; - bar(): void; - } -} -declare module 'goog:demo3' { -import demo3 = ಠ_ಠ.clutz.module$exports$demo3; - export default demo3; -} diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo4.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo4.d.ts deleted file mode 100644 index 4758a64fd..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo4.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.provide. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz.demo4 { - function f(): void; -} -declare module 'goog:demo4' { -import demo4 = ಠ_ಠ.clutz.demo4; - export = demo4; -} diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo5.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo5.d.ts deleted file mode 100644 index 9cb9ad9ae..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo5.d.ts +++ /dev/null @@ -1,17 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.module. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz.module$exports$demo5 { - class C { - private noStructuralTyping_module$exports$demo5_C : any; - f ( ) : void ; - } -} -declare module 'goog:demo5' { - import demo5 = ಠ_ಠ.clutz.module$exports$demo5; - export = demo5; -} - diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo6.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo6.d.ts deleted file mode 100644 index 96b67ac90..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo6.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for a simple goog.module, - * with a generic class. - * It was manually created and is a support file for the actual test. - */ - -declare namespace ಠ_ಠ.clutz.module$exports$demo6 { - class C < T = any > { - private noStructuralTyping_module$exports$demo6_C : [ T ]; - foo ( ) : void ; - } -} -declare module 'goog:demo6' { - import demo6 = ಠ_ಠ.clutz.module$exports$demo6; - export = demo6; -} - diff --git a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo7.d.ts b/test_files/clutz_imports.declaration.no_externs/clutz_output_demo7.d.ts deleted file mode 100644 index 5333dad3a..000000000 --- a/test_files/clutz_imports.declaration.no_externs/clutz_output_demo7.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -//!! generated by clutz. -/** - * @fileoverview This file contains the Clutz output for an externs file. - * It was manually created and is a support file for the actual test. - */ - -declare namespace demo7 { - class C { - foo(): void; - } -} diff --git a/test_files/clutz_imports.declaration.no_externs/user_code.d.ts b/test_files/clutz_imports.declaration.no_externs/user_code.d.ts deleted file mode 100644 index 0a3d3ba90..000000000 --- a/test_files/clutz_imports.declaration.no_externs/user_code.d.ts +++ /dev/null @@ -1,53 +0,0 @@ -// test_files/clutz_imports.declaration.no_externs/user_code.ts(39,1): warning TS0: anonymous type has no symbol -//!! generated by tsickle from test_files/clutz_imports.declaration.no_externs/user_code.ts -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo1"; -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo2"; -import "test_files/clutz_imports.declaration.no_externs/clutz2_output_demo8"; -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo4"; -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo6"; -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo5"; -import "test_files/clutz_imports.declaration.no_externs/clutz_output_demo7"; -/** - * @fileoverview This file simulates a TypeScript file that interacts with Clutz - * types. The expected output is that the generated .d.ts file has explicit - * "import" statements that refer directly to the paths that define some of - * the Clutz symbols (either goog: or look of disapproval) referenced in the - * public API of this file. - */ -import * as demo1 from 'goog:demo1'; -/** - * demo1 is exposed in the public API via an import, so we expect the output - * d.ts to have an import of the module underlying goog:demo1. - */ -export declare function f1(c: demo1.C): void; -/** - * demo2 is exposed in the public API via a direct reference to the look of - * disapproval namespace, so we expect the output d.ts to have an import of the - * module underlying goog:demo2. - * - * demo8 is the same, but the d.ts file is generated by Clutz2. - */ -export declare function f2(c: ಠ_ಠ.clutz.demo2.C, c2: ಠ_ಠ.clutz.demo8.C): void; -/** - * demo4 verifies that the Clutz type via 'typeof' still produces an import - * statement in the output. (It differs from the above in that a typeof node - * in the TS AST contains the reference to a Clutz symbol as a value, not a - * type.) - */ -export type f4 = typeof ಠ_ಠ.clutz.demo4; -export declare function f5(): ಠ_ಠ.clutz.module$exports$demo6.C<ಠ_ಠ.clutz.module$exports$demo5.C> | undefined; -/** - * demo7 contains typings generated from externs. - * - * Even though we don't reference the internal Clutz namespace here, we expect - * the output d.ts to have an import to the demo7 file. - */ -export declare function f6(c: demo7.C): void; -declare global { - namespace ಠ_ಠ.clutz { - export { f1 as module$contents$test_files$clutz_imports$declaration$no_externs$user_code_f1, f2 as module$contents$test_files$clutz_imports$declaration$no_externs$user_code_f2, f4 as module$contents$test_files$clutz_imports$declaration$no_externs$user_code_f4, f5 as module$contents$test_files$clutz_imports$declaration$no_externs$user_code_f5, f6 as module$contents$test_files$clutz_imports$declaration$no_externs$user_code_f6 }; - export namespace module$exports$test_files$clutz_imports$declaration$no_externs$user_code { - export { f1, f2, f4, f5, f6 }; - } - } -} diff --git a/test_files/clutz_imports.declaration.no_externs/user_code.ts b/test_files/clutz_imports.declaration.no_externs/user_code.ts deleted file mode 100644 index 9c2741eea..000000000 --- a/test_files/clutz_imports.declaration.no_externs/user_code.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @fileoverview This file simulates a TypeScript file that interacts with Clutz - * types. The expected output is that the generated .d.ts file has explicit - * "import" statements that refer directly to the paths that define some of - * the Clutz symbols (either goog: or look of disapproval) referenced in the - * public API of this file. - */ - -import * as demo1 from 'goog:demo1'; -import demo3 from 'goog:demo3'; - -/** - * demo1 is exposed in the public API via an import, so we expect the output - * d.ts to have an import of the module underlying goog:demo1. - */ -export function f1(c: demo1.C) {} - -/** - * demo2 is exposed in the public API via a direct reference to the look of - * disapproval namespace, so we expect the output d.ts to have an import of the - * module underlying goog:demo2. - * - * demo8 is the same, but the d.ts file is generated by Clutz2. - */ -export function f2(c: ಠ_ಠ.clutz.demo2.C, c2: ಠ_ಠ.clutz.demo8.C) {} - -/** - * demo3 is used by this module, but not exported, so we don't expect an import - * of the underlying module in the output d.ts. - */ -function f3(c: demo3) {} - -/** - * demo4 verifies that the Clutz type via 'typeof' still produces an import - * statement in the output. (It differs from the above in that a typeof node - * in the TS AST contains the reference to a Clutz symbol as a value, not a - * type.) - */ -export type f4 = typeof ಠ_ಠ.clutz.demo4; - -/** - * This next example verifies that references generated by TS are still handled. - * The internal function here references a Clutz type, which normally would - * stay internal-only and not affect the d.ts. But then we export a function - * that uses inference to refer to this type. - * - * This is a special case because the Clutz type appears in the d.ts but it - * is generated by a codepath in the TS compiler that causes the type to have no - * symbol present in the TypeChecker. - * - * This also uses a generic, to cover one additional case. - * We expect both demo5 and demo6 to show up in the public API of the d.ts. - */ -function internal(): - ಠ_ಠ.clutz.module$exports$demo6.C<ಠ_ಠ.clutz.module$exports$demo5.C>| - undefined { - return undefined; -} -export function f5() { - return internal(); -} - -/** - * demo7 contains typings generated from externs. - * - * Even though we don't reference the internal Clutz namespace here, we expect - * the output d.ts to have an import to the demo7 file. - */ -export function f6(c: demo7.C) {} diff --git a/test_files/comments/trailing_no_semicolon.js b/test_files/comments/trailing_no_semicolon.js new file mode 100644 index 000000000..f16924b09 --- /dev/null +++ b/test_files/comments/trailing_no_semicolon.js @@ -0,0 +1,22 @@ +/** + * + * @fileoverview Tests that the JSDoc comment of `other` is only emitted once. + * Without the trailing semicolon after `noExplicitSemicolon` TypeScript seems + * to duplicate the trailing comment as soon as a custom transformer modifies + * the variable statement. + * + * Generated from: test_files/comments/trailing_no_semicolon.ts + */ +goog.module('test_files.comments.trailing_no_semicolon'); +var module = module || { id: 'test_files/comments/trailing_no_semicolon.ts' }; +goog.require('tslib'); +/** @type {number} */ +const noExplicitSemicolon = 0; +/** + * This is a comment with a JSDoc tag + * JSCompiler doesn't recognize + * + * \@foobar + * @type {number} + */ +exports.other = 1; diff --git a/test_files/comments/trailing_no_semicolon.ts b/test_files/comments/trailing_no_semicolon.ts new file mode 100644 index 000000000..b68b9844a --- /dev/null +++ b/test_files/comments/trailing_no_semicolon.ts @@ -0,0 +1,17 @@ +/** + * @fileoverview Tests that the JSDoc comment of `other` is only emitted once. + * Without the trailing semicolon after `noExplicitSemicolon` TypeScript seems + * to duplicate the trailing comment as soon as a custom transformer modifies + * the variable statement. + */ + + +const noExplicitSemicolon = 0 + +/** + * This is a comment with a JSDoc tag + * JSCompiler doesn't recognize + * + * @foobar + */ +export const other = 1; diff --git a/test_files/decl_merge/outer_enum.js b/test_files/decl_merge/outer_enum.js new file mode 100644 index 000000000..9f3827e1a --- /dev/null +++ b/test_files/decl_merge/outer_enum.js @@ -0,0 +1,31 @@ +/** + * + * @fileoverview Ensure that a function declared in a declaration + * merging namespace is generated as a property of the merged outer enum. + * + * Generated from: test_files/decl_merge/outer_enum.ts + * @suppress {uselessCode,checkTypes} + * + */ +goog.module('test_files.decl_merge.outer_enum'); +var module = module || { id: 'test_files/decl_merge/outer_enum.ts' }; +goog.require('tslib'); +/** @enum {number} */ +const E = { + a: 42, + b: 43, +}; +exports.E = E; +E[E.a] = 'a'; +E[E.b] = 'b'; +/** + * @param {string} s + * @return {!E} + */ +function E$fromString(s) { + return s === 'a' ? E.a : E.b; +} +/** @const */ +E.fromString = E$fromString; +/** @type {!E} */ +const e = E.fromString('a'); diff --git a/test_files/decl_merge/outer_enum.ts b/test_files/decl_merge/outer_enum.ts new file mode 100644 index 000000000..b89996cc3 --- /dev/null +++ b/test_files/decl_merge/outer_enum.ts @@ -0,0 +1,20 @@ +/** + * @fileoverview Ensure that a function declared in a declaration + * merging namespace is generated as a property of the merged outer enum. + * + * @suppress {uselessCode,checkTypes} + */ + +export enum E { + a = 42, + b +} + +// tslint:disable-next-line:no-namespace +export namespace E { + export function fromString(s: string) { + return s === 'a' ? E.a : E.b; + }; +} + +const e = E.fromString('a'); diff --git a/test_files/decl_merge/rejected_ns.js b/test_files/decl_merge/rejected_ns.js index 54aa1d565..2fbd3bfa2 100644 --- a/test_files/decl_merge/rejected_ns.js +++ b/test_files/decl_merge/rejected_ns.js @@ -1,12 +1,13 @@ -// test_files/decl_merge/rejected_ns.ts(34,1): warning TS0: type/symbol conflict for Inbetween, using {?} for now +// test_files/decl_merge/rejected_ns.ts(32,1): warning TS0: type/symbol conflict for Inbetween, using {?} for now // test_files/decl_merge/rejected_ns.ts(9,11): error TS0: transformation of plain namespace not supported. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(13,11): error TS0: merged declaration must be local class or interface. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(21,11): error TS0: merged declaration must be local class or interface. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(26,3): error TS0: const declaration only allowed when merging with an interface (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(38,3): error TS0: non-const values are not supported. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(40,9): error TS0: 'K' must be exported. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(42,16): error TS0: Destructuring declarations are not supported. (go/ts-merged-namespaces) -// test_files/decl_merge/rejected_ns.ts(47,11): error TS0: nested namespaces are not supported. (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(13,11): error TS0: merged declaration must be local class, enum, or interface. (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(19,3): error TS0: const declaration only allowed when merging with an interface (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(24,3): error TS0: function declaration only allowed when merging with an enum (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(36,3): error TS0: non-const values are not supported. (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(38,9): error TS0: 'K' must be exported. (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(40,16): error TS0: Destructuring declarations are not supported. (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(44,3): error TS0: function declaration only allowed when merging with an enum (go/ts-merged-namespaces) +// test_files/decl_merge/rejected_ns.ts(48,11): error TS0: nested namespaces are not supported. (go/ts-merged-namespaces) /** * * @fileoverview Test namespace transformations that are not supported @@ -24,21 +25,21 @@ goog.require('tslib'); * @return {void} */ function funcToBeMerged() { } -/** @enum {number} */ -const Colors = { - red: 0, - green: 1, - blue: 2, -}; -Colors[Colors.red] = 'red'; -Colors[Colors.green] = 'green'; -Colors[Colors.blue] = 'blue'; // Adding const values is only allowed on interfaces. class Cabbage { } (function (Cabbage) { Cabbage.C = 0; })(Cabbage || (Cabbage = {})); +// Adding functions is only allowed on enums. +(function (Cabbage) { + /** + * @return {void} + */ + function foo() { } + Cabbage.foo = foo; + ; +})(Cabbage || (Cabbage = {})); /** @type {{a: number, b: string}} */ const o = { a: 0, @@ -60,6 +61,13 @@ var Inbetween; // Destructuring declarations are not allowed. Inbetween.a = o.a, Inbetween.b = o.b; })(Inbetween || (Inbetween = {})); +(function (Inbetween) { + /** + * @return {void} + */ + function foo() { } + Inbetween.foo = foo; +})(Inbetween || (Inbetween = {})); // Nested namespaces are not supported. class A { } diff --git a/test_files/decl_merge/rejected_ns.ts b/test_files/decl_merge/rejected_ns.ts index 2e414d803..fdb0df004 100644 --- a/test_files/decl_merge/rejected_ns.ts +++ b/test_files/decl_merge/rejected_ns.ts @@ -12,13 +12,6 @@ namespace notMerging {} function funcToBeMerged() {} namespace funcToBeMerged {} -// Declaration merging with enums is not supported. -enum Colors { - red, - green, - blue -} -namespace Colors {} // Adding const values is only allowed on interfaces. class Cabbage {} @@ -26,6 +19,11 @@ namespace Cabbage { export const C = 0; } +// Adding functions is only allowed on enums. +namespace Cabbage { + export function foo() {}; +} + const o = { a: 0, b: '' @@ -42,6 +40,9 @@ namespace Inbetween { export const {a, b} = o; } +namespace Inbetween { + export function foo() {} +} // Nested namespaces are not supported. class A {} namespace A.B {} diff --git a/test_files/declare_var_and_ns/externs.js b/test_files/declare_var_and_ns/externs.js index eb909e167..9b522a4c1 100644 --- a/test_files/declare_var_and_ns/externs.js +++ b/test_files/declare_var_and_ns/externs.js @@ -6,8 +6,6 @@ // Generated from: test_files/declare_var_and_ns/declare_var_and_ns.d.ts /** @type {!globalVariable.SomeInterface} */ var globalVariable; -/** @const */ -var globalVariable = {}; /** * @record * @struct diff --git a/test_files/enum.no_nstransform/enum.js b/test_files/enum.no_nstransform/enum.js new file mode 100644 index 000000000..7c4ae2376 --- /dev/null +++ b/test_files/enum.no_nstransform/enum.js @@ -0,0 +1,42 @@ +/** + * + * @fileoverview Check that enums are translated to a var declaration + * when namespace transformation is turned off, i.e. the build target + * has the attribute --allow_unoptimized_namespaces. + * Generated from: test_files/enum.no_nstransform/enum.ts + * @suppress {checkTypes,uselessCode} + * + */ +goog.module('test_files.enum.no_nstransform.enum'); +var module = module || { id: 'test_files/enum.no_nstransform/enum.ts' }; +goog.require('tslib'); +/** + * This enum should be translated to `var E = {...}` instead of the usual + * `const E = {...}` + * @enum {number} + */ +var E = { + e0: 0, + e1: 1, + e2: 2, +}; +exports.E = E; +E[E.e0] = 'e0'; +E[E.e1] = 'e1'; +E[E.e2] = 'e2'; +// We need to emit the enum as a var declaration so that declaration +// merging with a namespace works. The unoptimized namespace is emitted +// by tsc as a var declaration and an IIFE. +var E; +(function (E) { + /** + * @param {string} s + * @return {?} + */ + function fromString(s) { + return E.e0; + } + E.fromString = fromString; +})(E || (E = {})); +/** @type {!E} */ +const foo = E.e2; diff --git a/test_files/enum.no_nstransform/enum.ts b/test_files/enum.no_nstransform/enum.ts new file mode 100644 index 000000000..4f829e1d3 --- /dev/null +++ b/test_files/enum.no_nstransform/enum.ts @@ -0,0 +1,27 @@ +/** + * @fileoverview Check that enums are translated to a var declaration + * when namespace transformation is turned off, i.e. the build target + * has the attribute --allow_unoptimized_namespaces. + * @suppress {checkTypes,uselessCode} + */ + +/** + * This enum should be translated to `var E = {...}` instead of the usual + * `const E = {...}` + */ +export enum E { + e0 = 0, + e1, + e2 +} + +// We need to emit the enum as a var declaration so that declaration +// merging with a namespace works. The unoptimized namespace is emitted +// by tsc as a var declaration and an IIFE. +export namespace E { + export function fromString(s: string) { + return E.e0; + } +} + +const foo = E.e2; diff --git a/test_files/enum.puretransform/enum.js b/test_files/enum.puretransform/enum.js new file mode 100644 index 000000000..b0a8c2eb8 --- /dev/null +++ b/test_files/enum.puretransform/enum.js @@ -0,0 +1,21 @@ +/** + * @fileoverview Test devmode (i.e. no JSDoc or special enum transformer) emit + * for enum merged with namespace. + * @suppress {missingProperties} + */ +goog.module('test_files.enum.puretransform.enum'); +var module = module || { id: 'test_files/enum.puretransform/enum.ts' }; +goog.require('tslib'); +var E; +(function (E) { + E[E["e0"] = 0] = "e0"; + E[E["e1"] = 1] = "e1"; + E[E["e2"] = 2] = "e2"; +})(E || (E = {})); +exports.E = E; +(function (E) { + function fromString(s) { + return E.e0; + } + E.fromString = fromString; +})(E || (E = {})); diff --git a/test_files/enum.puretransform/enum.ts b/test_files/enum.puretransform/enum.ts new file mode 100644 index 000000000..ecca84131 --- /dev/null +++ b/test_files/enum.puretransform/enum.ts @@ -0,0 +1,17 @@ +/** + * @fileoverview Test devmode (i.e. no JSDoc or special enum transformer) emit + * for enum merged with namespace. + * @suppress {missingProperties} + */ + +export enum E { + e0 = 0, + e1, + e2 +} + +export namespace E { + export function fromString(s: string) { + return E.e0; + } +} diff --git a/test_files/enum/enum.js b/test_files/enum/enum.js index 8041302d5..e67603054 100644 --- a/test_files/enum/enum.js +++ b/test_files/enum/enum.js @@ -57,7 +57,8 @@ let variableUsingExportedEnum; const ComponentIndex = { Scheme: 1, UserInfo: 2, - Domain: 0, + // TODO: b/313666408 - Fix tsc to not duplicate comments like the following + Domain: 0, // Be sure to exercise the code with a 0 enum value. // Be sure to exercise the code with a 0 enum value. UserInfo2: 2, }; diff --git a/test_files/enum/enum.ts b/test_files/enum/enum.ts index 8f913933b..b070ff424 100644 --- a/test_files/enum/enum.ts +++ b/test_files/enum/enum.ts @@ -36,6 +36,7 @@ let variableUsingExportedEnum: EnumTest2; enum ComponentIndex { Scheme = 1, UserInfo, + // TODO: b/313666408 - Fix tsc to not duplicate comments like the following Domain = 0, // Be sure to exercise the code with a 0 enum value. UserInfo2 = UserInfo, } diff --git a/test_files/export_destructuring/export_destructuring.js b/test_files/export_destructuring/export_destructuring.js new file mode 100644 index 000000000..11a67dad3 --- /dev/null +++ b/test_files/export_destructuring/export_destructuring.js @@ -0,0 +1,33 @@ +goog.module('test_files.export_destructuring.export_destructuring'); +var module = module || { id: 'test_files/export_destructuring/export_destructuring.ts' }; +goog.require('tslib'); +var _a, _b; +/** + * + * @fileoverview + * Generated from: test_files/export_destructuring/export_destructuring.ts + * @suppress {undefinedVars} + * + */ +/** + * @param {number} n + * @return {!Array} + */ +function signal(n) { + return [n, n + 1]; +} +/** + * @param {number} n + * @return {{c: number, d: number}} + */ +function objectLiteral(n) { + return { c: n, d: n + 1 }; +} +_a = signal(0); +a__tsickle_destructured_1 = _a[0]; +b__tsickle_destructured_2 = _a[1]; +const a = /** @type {number} */ (a__tsickle_destructured_1); +const b = /** @type {number} */ (b__tsickle_destructured_2); +_b = objectLiteral(0); +exports.c = _b.c; +exports.d = _b.d; diff --git a/test_files/export_destructuring/export_destructuring.ts b/test_files/export_destructuring/export_destructuring.ts new file mode 100644 index 000000000..1d5f15211 --- /dev/null +++ b/test_files/export_destructuring/export_destructuring.ts @@ -0,0 +1,15 @@ +/** + * @fileoverview + * @suppress {undefinedVars} + */ + +function signal(n: number) { + return [n, n + 1]; +} +function objectLiteral(n: number) { + return {c: n, d: n + 1}; +} + +export const [a, b] = signal(0); + +export const {c, d} = objectLiteral(0); diff --git a/test_files/import_by_path.declaration.no_externs/clutz_input.d.ts b/test_files/import_by_path.declaration.no_externs/clutz_input.d.ts deleted file mode 100644 index 7dde5e336..000000000 --- a/test_files/import_by_path.declaration.no_externs/clutz_input.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Mocks for Clutz-generated .d.ts. - -declare namespace ಠ_ಠ.clutz.another.module { - export class SomeClass {} -} -declare module 'goog:another.module' { -import SomeClass = ಠ_ಠ.clutz.another.module.SomeClass; - export {SomeClass}; -} -declare module 'google3/another/file' { -import SomeClass = ಠ_ಠ.clutz.another.module.SomeClass; - export {SomeClass}; - const __clutz_actual_namespace: 'another.module'; -} diff --git a/test_files/import_by_path.declaration.no_externs/decluser.d.ts b/test_files/import_by_path.declaration.no_externs/decluser.d.ts deleted file mode 100644 index c845f22b6..000000000 --- a/test_files/import_by_path.declaration.no_externs/decluser.d.ts +++ /dev/null @@ -1,14 +0,0 @@ -//!! generated by tsickle from test_files/import_by_path.declaration.no_externs/decluser.ts -import "test_files/import_by_path.declaration.no_externs/clutz_input"; -import { SomeClass } from 'google3/another/file'; -export declare class UsingPathImports { - someField?: SomeClass; -} -declare global { - namespace ಠ_ಠ.clutz { - export { UsingPathImports as module$contents$test_files$import_by_path$declaration$no_externs$decluser_UsingPathImports }; - export namespace module$exports$test_files$import_by_path$declaration$no_externs$decluser { - export { UsingPathImports }; - } - } -} diff --git a/test_files/import_by_path.declaration.no_externs/decluser.ts b/test_files/import_by_path.declaration.no_externs/decluser.ts deleted file mode 100644 index 7b01a7f96..000000000 --- a/test_files/import_by_path.declaration.no_externs/decluser.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {SomeClass} from 'google3/another/file'; - -export class UsingPathImports { - someField?: SomeClass; -} diff --git a/test_files/import_by_path.declaration.no_externs/jsprovides.js b/test_files/import_by_path.declaration.no_externs/jsprovides.js deleted file mode 100644 index 26ec015a9..000000000 --- a/test_files/import_by_path.declaration.no_externs/jsprovides.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * @fileoverview Description of this file. - */ - -goog.module('another.module'); - -exports.SomeClass = class {}; diff --git a/test_files/internal.declaration/internal.d.ts b/test_files/internal.declaration/internal.d.ts index eae8448e9..1bd9c76a6 100644 --- a/test_files/internal.declaration/internal.d.ts +++ b/test_files/internal.declaration/internal.d.ts @@ -1,3 +1,8 @@ // test_files/internal.declaration/internal.ts(27,18): error TS0: transformation of plain namespace not supported. (go/ts-merged-namespaces) //!! generated by tsickle from test_files/internal.declaration/internal.ts +/** + * @fileoverview Test to reproduce that \@internal declarations are not + * re-exported for Clutz. There should not be any `.d.ts` aliases generated for + * the declarations below. + */ export {}; diff --git a/test_files/typeof_function_overloads/user.js b/test_files/typeof_function_overloads/user.js new file mode 100644 index 000000000..235469fe2 --- /dev/null +++ b/test_files/typeof_function_overloads/user.js @@ -0,0 +1,21 @@ +/** + * + * @fileoverview Test overloaded function type emit. + * + * Generated from: test_files/typeof_function_overloads/user.ts + */ +goog.module('test_files.typeof_function_overloads.user'); +var module = module || { id: 'test_files/typeof_function_overloads/user.ts' }; +goog.require('tslib'); +/** + * @param {?=} initialValue + * @return {null} + */ +function ɵinput(initialValue) { + return null; +} +exports.ɵinput = ɵinput; +/** @typedef {function(?=): null} */ +exports.InputFn; +/** @type {function(?=): null} */ +exports.input = ɵinput; diff --git a/test_files/typeof_function_overloads/user.ts b/test_files/typeof_function_overloads/user.ts new file mode 100644 index 000000000..f4233189f --- /dev/null +++ b/test_files/typeof_function_overloads/user.ts @@ -0,0 +1,11 @@ +/** + * @fileoverview Test overloaded function type emit. + */ + +export function ɵinput(): null; +export function ɵinput(initialValue: any): null; +export function ɵinput(initialValue?: any): null { + return null; +} +export type InputFn = typeof ɵinput; +export const input = ɵinput;