Comparing version 0.2.2 to 0.2.3
@@ -0,11 +1,13 @@ | ||
/** | ||
* @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'; | ||
export declare const ANNOTATION_SUPPORT_CODE: string; | ||
export declare type Options = { | ||
omitCtorParameterAnnotations?: boolean; | ||
omitClassAnnotations?: boolean; | ||
omitPropertyAnnotations?: boolean; | ||
}; | ||
export declare function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile, options?: Options): { | ||
export declare function convertDecorators(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): { | ||
output: string; | ||
diagnostics: ts.Diagnostic[]; | ||
}; |
@@ -5,3 +5,5 @@ /** | ||
* | ||
* @param fileName The source file name, without an extension. | ||
* @param fileName The source file name. | ||
* @param moduleId The "module id", a module-identifying string that is | ||
* the value module.id in the scope of the module. | ||
* @param pathToModuleName A function that maps a filesystem .ts path to a | ||
@@ -11,6 +13,8 @@ * Closure module name, as found in a goog.require('...') statement. | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
* @param prelude An additional prelude to insert after the `goog.module` call, | ||
* e.g. with additional imports or requires. | ||
*/ | ||
export declare function processES5(fileName: string, content: string, pathToModuleName: (context: string, fileName: string) => string, isES5?: boolean): { | ||
export declare function processES5(fileName: string, moduleId: string, content: string, pathToModuleName: (context: string, fileName: string) => string, isES5?: boolean, prelude?: string): { | ||
output: string; | ||
referencedModules: string[]; | ||
}; |
/** | ||
* @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 | ||
*/ | ||
/** | ||
* TypeScript has an API for JSDoc already, but it's not exposed. | ||
@@ -26,1 +33,3 @@ * https://github.com/Microsoft/TypeScript/issues/7393 | ||
export declare function toString(tags: Tag[]): string; | ||
/** Merges multiple tags (of the same tagName type) into a single unified tag. */ | ||
export declare function merge(tags: Tag[]): Tag; |
@@ -0,1 +1,9 @@ | ||
/** | ||
* @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 { SourceMapGenerator } from 'source-map'; | ||
import * as ts from 'typescript'; | ||
@@ -9,5 +17,9 @@ /** | ||
protected file: ts.SourceFile; | ||
protected output: string[]; | ||
private output; | ||
/** Errors found while examining the code. */ | ||
protected diagnostics: ts.Diagnostic[]; | ||
/** The source map that's generated while rewriting this file. */ | ||
private sourceMap; | ||
/** Current position in the output. */ | ||
private position; | ||
/** | ||
@@ -22,2 +34,3 @@ * The current level of recursion through TypeScript Nodes. Used in formatting internal debug | ||
diagnostics: ts.Diagnostic[]; | ||
sourceMap: SourceMapGenerator; | ||
}; | ||
@@ -24,0 +37,0 @@ /** |
@@ -0,13 +1,41 @@ | ||
/** | ||
* @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 { SourceMapGenerator } from 'source-map'; | ||
import * as ts from 'typescript'; | ||
export { ANNOTATION_SUPPORT_CODE, convertDecorators } from './decorator-annotator'; | ||
export { processES5 as convertCommonJsToGoogModule } from './es5processor'; | ||
export { convertDecorators } from './decorator-annotator'; | ||
export { processES5 } from './es5processor'; | ||
export interface Options { | ||
/** | ||
* 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<string>; | ||
/** | ||
* Convert shorthand "/index" imports to full path (include the "/index"). | ||
* Annotation will be slower because every import must be resolved. | ||
*/ | ||
convertIndexImportShorthand?: boolean; | ||
} | ||
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; | ||
} | ||
@@ -20,2 +48,2 @@ /** | ||
export declare function formatDiagnostics(diags: ts.Diagnostic[]): string; | ||
export declare function annotate(program: ts.Program, file: ts.SourceFile, options?: Options): Output; | ||
export declare function annotate(program: ts.Program, file: ts.SourceFile, options?: Options, host?: ts.ModuleResolutionHost, tsOpts?: ts.CompilerOptions): Output; |
@@ -0,1 +1,8 @@ | ||
/** | ||
* @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'; | ||
@@ -36,10 +43,12 @@ export declare function assertTypeChecked(sourceFile: ts.SourceFile): void; | ||
symbolToString(sym: ts.Symbol): string; | ||
translate(type: ts.Type): string; | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
* is to work around the difference between TS and Closure destructuring. | ||
* translateTypeLiteral translates a ts.SymbolFlags.TypeLiteral type, which | ||
* is the anonymous type encountered in e.g. | ||
* let x: {a: number}; | ||
*/ | ||
translate(type: ts.Type, notNull?: boolean): string; | ||
private translateTypeLiteral(type); | ||
/** Converts a ts.Signature (function signature) to a Closure function type. */ | ||
private signatureToClosure(sig); | ||
private convertParams(sig); | ||
warn(msg: string): void; | ||
@@ -46,0 +55,0 @@ /** @return true if sym should always have type {?}. */ |
@@ -0,1 +1,8 @@ | ||
/** | ||
* @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 declare function toArray<T>(iterator: Iterator<T>): T[]; |
@@ -24,5 +24,6 @@ /** | ||
__extends(ES5Processor, _super); | ||
function ES5Processor(file, pathToModuleName) { | ||
function ES5Processor(file, pathToModuleName, prelude) { | ||
_super.call(this, file); | ||
this.pathToModuleName = pathToModuleName; | ||
this.prelude = prelude; | ||
/** | ||
@@ -56,2 +57,4 @@ * namespaceImports collects the variables for imported goog.modules. | ||
this.emit("goog.module('" + moduleName + "');"); | ||
if (this.prelude) | ||
this.emit(this.prelude); | ||
// Allow code to use `module.id` to discover its module URL, e.g. to resolve | ||
@@ -326,7 +329,10 @@ // a template URL against. | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
* @param prelude An additional prelude to insert after the `goog.module` call, | ||
* e.g. with additional imports or requires. | ||
*/ | ||
function processES5(fileName, moduleId, content, pathToModuleName, isES5) { | ||
function processES5(fileName, moduleId, content, pathToModuleName, isES5, prelude) { | ||
if (isES5 === void 0) { isES5 = true; } | ||
if (prelude === void 0) { prelude = ''; } | ||
var file = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES5, true); | ||
return new ES5Processor(file, pathToModuleName).process(moduleId, isES5); | ||
return new ES5Processor(file, pathToModuleName, prelude).process(moduleId, isES5); | ||
} | ||
@@ -333,0 +339,0 @@ exports.processES5 = processES5; |
@@ -94,3 +94,3 @@ /** | ||
*/ | ||
var JSDOC_TAGS_BLACKLIST = ['private', 'public', 'type']; | ||
var JSDOC_TAGS_BLACKLIST = ['constructor', 'extends', 'implements', 'private', 'public', 'type']; | ||
/** A list of JSDoc @tags that might include a {type} after them. */ | ||
@@ -111,3 +111,3 @@ var JSDOC_TAGS_WITH_TYPES = ['export', 'param', 'return']; | ||
// Strip all the " * " bits from the front of each line. | ||
comment = comment.replace(/^\s*\* /gm, ''); | ||
comment = comment.replace(/^\s*\*? ?/gm, ''); | ||
var lines = comment.split('\n'); | ||
@@ -165,6 +165,6 @@ var tags = []; | ||
if (tags.length === 0) { | ||
tags.push({ text: line.trim() }); | ||
tags.push({ text: line }); | ||
} | ||
else { | ||
tags[tags.length - 1].text += '\n * ' + line.trim(); | ||
tags[tags.length - 1].text += '\n' + line; | ||
} | ||
@@ -179,2 +179,30 @@ } | ||
exports.parse = parse; | ||
/** | ||
* Serializes a Tag into a string usable in a comment. | ||
* Returns a string like " @foo {bar} baz" (note the whitespace). | ||
*/ | ||
function tagToString(tag) { | ||
var out = ''; | ||
if (tag.tagName) { | ||
out += " @" + tag.tagName; | ||
} | ||
if (tag.type) { | ||
out += ' {'; | ||
if (tag.restParam) { | ||
out += '...'; | ||
} | ||
out += tag.type; | ||
if (tag.optional) { | ||
out += '='; | ||
} | ||
out += '}'; | ||
} | ||
if (tag.parameterName) { | ||
out += ' ' + tag.parameterName; | ||
} | ||
if (tag.text) { | ||
out += ' ' + tag.text.replace(/@/g, '\\@'); | ||
} | ||
return out; | ||
} | ||
/** Serializes a Comment out to a string usable in source code. */ | ||
@@ -184,2 +212,10 @@ function toString(tags) { | ||
return ''; | ||
if (tags.length === 1) { | ||
var 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. | ||
// /** @type {foo} */ | ||
return '/**' + tagToString(tag) + ' */\n'; | ||
} | ||
} | ||
var out = ''; | ||
@@ -189,23 +225,5 @@ out += '/**\n'; | ||
var tag = tags_1[_i]; | ||
out += ' * '; | ||
if (tag.tagName) { | ||
out += "@" + tag.tagName; | ||
} | ||
if (tag.type) { | ||
out += ' {'; | ||
if (tag.restParam) { | ||
out += '...'; | ||
} | ||
out += tag.type; | ||
if (tag.optional) { | ||
out += '='; | ||
} | ||
out += '}'; | ||
} | ||
if (tag.parameterName) { | ||
out += ' ' + tag.parameterName; | ||
} | ||
if (tag.text) { | ||
out += ' ' + tag.text; | ||
} | ||
out += ' *'; | ||
// If the tagToString is multi-line, insert " * " prefixes on subsequent lines. | ||
out += tagToString(tag).split('\n').join('\n * '); | ||
out += '\n'; | ||
@@ -217,3 +235,30 @@ } | ||
exports.toString = toString; | ||
/** Merges multiple tags (of the same tagName type) into a single unified tag. */ | ||
function merge(tags) { | ||
var tagNames = new Set(); | ||
var parameterNames = new Set(); | ||
var types = new Set(); | ||
var texts = new Set(); | ||
for (var _i = 0, tags_2 = tags; _i < tags_2.length; _i++) { | ||
var tag = tags_2[_i]; | ||
if (tag.tagName) | ||
tagNames.add(tag.tagName); | ||
if (tag.parameterName) | ||
parameterNames.add(tag.parameterName); | ||
if (tag.type) | ||
types.add(tag.type); | ||
if (tag.text) | ||
texts.add(tag.text); | ||
} | ||
if (tagNames.size !== 1) { | ||
throw new Error("cannot merge differing tags: " + JSON.stringify(tags)); | ||
} | ||
var tagName = tagNames.values().next().value; | ||
var parameterName = parameterNames.size > 0 ? Array.from(parameterNames).join('_or_') : undefined; | ||
var type = types.size > 0 ? Array.from(types).join('|') : undefined; | ||
var text = texts.size > 0 ? Array.from(texts).join(' / ') : undefined; | ||
return { tagName: tagName, parameterName: parameterName, type: type, text: text }; | ||
} | ||
exports.merge = merge; | ||
//# sourceMappingURL=jsdoc.js.map |
@@ -67,17 +67,26 @@ /** | ||
} | ||
/** | ||
* TypeScript allows parameters named "arguments", but Closure | ||
* disallows this, even in externs. | ||
*/ | ||
function makeLegalExternsParam(name) { | ||
switch (name) { | ||
case 'arguments': | ||
return 'tsickle_arguments'; | ||
function isDtsFileName(fileName) { | ||
return /\.d\.ts$/.test(fileName); | ||
} | ||
/** Returns the Closure name of a function parameter, special-casing destructuring. */ | ||
function getParameterName(param, index) { | ||
switch (param.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
var name_1 = rewriter_1.getIdentifierText(param.name); | ||
// TypeScript allows parameters named "arguments", but Closure | ||
// disallows this, even in externs. | ||
if (name_1 === 'arguments') | ||
name_1 = 'tsickle_arguments'; | ||
return name_1; | ||
case ts.SyntaxKind.ArrayBindingPattern: | ||
case ts.SyntaxKind.ObjectBindingPattern: | ||
// Closure crashes if you put a binding pattern in the externs. | ||
// Avoid this by just generating an unused name; the name is | ||
// ignored anyway. | ||
return "__" + index; | ||
default: | ||
return name; | ||
// The above list of kinds should be exhaustive. | ||
throw new Error("unhandled function parameter kind: " + ts.SyntaxKind[param.name.kind]); | ||
} | ||
} | ||
function isDtsFileName(fileName) { | ||
return /\.d\.ts$/.test(fileName); | ||
} | ||
var VISIBILITY_FLAGS = ts.NodeFlags.Private | ts.NodeFlags.Protected | ts.NodeFlags.Public; | ||
@@ -101,3 +110,2 @@ /** | ||
* - Total number of parameters will be the maximum count found across all variants. | ||
* - Any parameters beyond the minimum count found across all variants; prefix: "opt_" | ||
* - Different names at the same parameter index will be joined with "_or_" | ||
@@ -114,14 +122,13 @@ * - Variable args (...type[] in TypeScript) will be output as "...type", | ||
var newDoc = extraTags; | ||
var paramNamesToReturn = []; | ||
var abstract = { tagName: 'abstract' }; | ||
var lens = fnDecls.map(function (fnDecl) { return fnDecl.parameters.length; }); | ||
var minCount = Math.min.apply(Math, lens); | ||
var maxCount = Math.max.apply(Math, lens); | ||
var isOverloaded = fnDecls.length > 1; | ||
var isConstructor = fnDecls[0].kind === ts.SyntaxKind.Constructor; | ||
var paramData = { | ||
names: new Array(maxCount), | ||
types: new Array(maxCount), | ||
returns: new Set() | ||
}; | ||
var minArgsCount = Math.min.apply(Math, lens); | ||
var maxArgsCount = Math.max.apply(Math, lens); | ||
var isConstructor = fnDecls.find(function (d) { return d.kind === ts.SyntaxKind.Constructor; }) !== undefined; | ||
// For each parameter index i, paramTags[i] is an array of parameters | ||
// that can be found at index i. E.g. | ||
// function foo(x: string) | ||
// function foo(y: number, z: string) | ||
// then paramTags[0] = [info about x, info about y]. | ||
var paramTags = []; | ||
var returnTags = []; | ||
for (var _i = 0, fnDecls_1 = fnDecls; _i < fnDecls_1.length; _i++) { | ||
@@ -134,32 +141,29 @@ var fnDecl = fnDecls_1[_i]; | ||
// Copy all the tags other than @param/@return into the new | ||
// comment without any change; @param/@return are handled later. | ||
// TODO: handle for overloads. indexOf on the array doesn't match instances of the Tag type, | ||
// so another matching strategy is needed to avoid dupes. Also, there may be problems if | ||
// an annotation doesn't apply to all overloads. | ||
// JSDoc without any change; @param/@return are handled specially. | ||
// TODO: there may be problems if an annotation doesn't apply to all overloads; | ||
// is it worth checking for this and erroring? | ||
for (var _a = 0, jsDoc_1 = jsDoc; _a < jsDoc_1.length; _a++) { | ||
var tag = jsDoc_1[_a]; | ||
if (tag.tagName === 'param' || tag.tagName === 'return' || isOverloaded) | ||
if (tag.tagName === 'param' || tag.tagName === 'return') | ||
continue; | ||
newDoc.push(tag); | ||
} | ||
// Abstract | ||
if ((fnDecl.flags & ts.NodeFlags.Abstract) && (newDoc.indexOf(abstract) === -1)) { | ||
newDoc.push(abstract); | ||
// Add @abstract on "abstract" declarations. | ||
if (fnDecl.flags & ts.NodeFlags.Abstract) { | ||
newDoc.push({ tagName: 'abstract' }); | ||
} | ||
// Merge the parameters into a single list of merged names and list of types | ||
var sig = typeChecker.getSignatureFromDeclaration(fnDecl); | ||
// Parameters. | ||
// Iterate through both the AST parameter list and the type's parameter | ||
// list, as some information is only available in the former. | ||
for (var i = 0; i < sig.parameters.length; i++) { | ||
var paramNode = fnDecl.parameters[i]; | ||
var paramSym = sig.parameters[i]; | ||
var type = typeChecker.getTypeOfSymbolAtLocation(paramSym, fnDecl); | ||
for (var i = 0; i < sig.declaration.parameters.length; i++) { | ||
var paramNode = sig.declaration.parameters[i]; | ||
var destructuring = (paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
paramNode.name.kind === ts.SyntaxKind.ObjectBindingPattern); | ||
var name_2 = getParameterName(paramNode, i); | ||
var isThisParam = name_2 === 'this'; | ||
var newTag = { | ||
tagName: 'param', | ||
tagName: isThisParam ? 'this' : 'param', | ||
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined, | ||
parameterName: makeLegalExternsParam(rewriter_1.unescapeName(paramSym.getName())), | ||
parameterName: isThisParam ? undefined : name_2, | ||
}; | ||
var destructuring = (paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
paramNode.name.kind === ts.SyntaxKind.ObjectBindingPattern); | ||
var type = typeChecker.getTypeAtLocation(paramNode); | ||
if (paramNode.dotDotDotToken !== undefined) { | ||
@@ -173,36 +177,13 @@ newTag.restParam = true; | ||
newTag.type = this.typeToClosure(fnDecl, type, destructuring); | ||
if (!isOverloaded) { | ||
// Search for this parameter in the JSDoc @params. | ||
// TODO: Consider adding text from existing JSDoc into overloads | ||
for (var _b = 0, jsDoc_2 = jsDoc; _b < jsDoc_2.length; _b++) { | ||
var _c = jsDoc_2[_b], tagName = _c.tagName, parameterName = _c.parameterName, text = _c.text; | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
break; | ||
} | ||
for (var _b = 0, jsDoc_2 = jsDoc; _b < jsDoc_2.length; _b++) { | ||
var _c = jsDoc_2[_b], tagName = _c.tagName, parameterName = _c.parameterName, text = _c.text; | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
break; | ||
} | ||
// push to the newDoc for rendering the single function | ||
newDoc.push(newTag); | ||
} | ||
else { | ||
// build set of param names at this index | ||
if (newTag.parameterName) { | ||
if (!paramData.names[i]) { | ||
paramData.names[i] = new Set(); | ||
} | ||
paramData.names[i].add(newTag.parameterName); | ||
} | ||
// build set of type names at this index | ||
if (newTag.type) { | ||
if (!paramData.types[i]) { | ||
paramData.types[i] = new Set(); | ||
} | ||
paramData.types[i].add(newTag.type); | ||
} | ||
} | ||
if (!paramTags[i]) | ||
paramTags.push([]); | ||
paramTags[i].push(newTag); | ||
} | ||
// If this method is not overloaded, we set the list of names to those in the only declaration | ||
if (!isOverloaded) { | ||
paramNamesToReturn = fnDecl.parameters.map(function (p) { return p.name.getText(); }); | ||
} | ||
// Return type. | ||
@@ -212,48 +193,39 @@ if (!isConstructor) { | ||
var retTypeString = this.typeToClosure(fnDecl, retType); | ||
if (!isOverloaded) { | ||
// TODO: Consider adding text from existing JSDoc into overloads | ||
var returnDoc = void 0; | ||
for (var _d = 0, jsDoc_3 = jsDoc; _d < jsDoc_3.length; _d++) { | ||
var _e = jsDoc_3[_d], tagName = _e.tagName, text = _e.text; | ||
if (tagName === 'return') { | ||
returnDoc = text; | ||
break; | ||
} | ||
var returnDoc = void 0; | ||
for (var _d = 0, jsDoc_3 = jsDoc; _d < jsDoc_3.length; _d++) { | ||
var _e = jsDoc_3[_d], tagName = _e.tagName, text = _e.text; | ||
if (tagName === 'return') { | ||
returnDoc = text; | ||
break; | ||
} | ||
newDoc.push({ | ||
tagName: 'return', | ||
type: retTypeString, | ||
text: returnDoc, | ||
}); | ||
} | ||
else { | ||
if (retTypeString) { | ||
paramData.returns.add(retTypeString); | ||
} | ||
} | ||
returnTags.push({ | ||
tagName: 'return', | ||
type: retTypeString, | ||
text: returnDoc, | ||
}); | ||
} | ||
} | ||
if (isOverloaded) { | ||
// Build actual JSDoc tags for the merged param names/types and return types | ||
for (var i = 0; i < maxCount; i++) { | ||
paramNamesToReturn.push(Array.from(paramData.names[i].values()).join('_or_')); | ||
if (i >= minCount) { | ||
paramNamesToReturn[i] = 'opt_' + paramNamesToReturn[i]; | ||
} | ||
var concatenatedTypes = Array.from(paramData.types[i].values()).join('|'); | ||
newDoc.push({ | ||
tagName: 'param', | ||
parameterName: paramNamesToReturn[i], | ||
type: concatenatedTypes, | ||
}); | ||
// Merge the JSDoc tags for each overloaded parameter. | ||
for (var i = 0; i < maxArgsCount; i++) { | ||
var paramTag = jsdoc.merge(paramTags[i]); | ||
// If any overload marks this param optional, mark it optional in the | ||
// merged output. | ||
var optional = paramTags[i].find(function (t) { return t.optional === true; }) !== undefined; | ||
if (optional || i >= minArgsCount) { | ||
paramTag.type += '='; | ||
} | ||
if (!isConstructor) { | ||
newDoc.push({ | ||
tagName: 'return', | ||
type: Array.from(paramData.returns.values()).join('|'), | ||
}); | ||
// If any overload marks this param as a ..., mark it ... in the | ||
// merged output. | ||
if (paramTags[i].find(function (t) { return t.restParam === true; }) !== undefined) { | ||
paramTag.restParam = true; | ||
} | ||
newDoc.push(paramTag); | ||
} | ||
// Merge the JSDoc tags for each overloaded return. | ||
if (!isConstructor) { | ||
newDoc.push(jsdoc.merge(returnTags)); | ||
} | ||
this.emit('\n' + jsdoc.toString(newDoc)); | ||
return paramNamesToReturn; | ||
return newDoc.filter(function (t) { return t.tagName === 'param'; }).map(function (t) { return t.parameterName; }); | ||
}; | ||
@@ -317,3 +289,3 @@ /** | ||
translator.warn = function (msg) { return _this.debugWarn(context, msg); }; | ||
return translator.translate(type, true); | ||
return translator.translate(type); | ||
}; | ||
@@ -345,4 +317,6 @@ /** | ||
__extends(Annotator, _super); | ||
function Annotator(program, file, options) { | ||
function Annotator(program, file, options, host, tsOpts) { | ||
_super.call(this, program, file, options); | ||
this.host = host; | ||
this.tsOpts = tsOpts; | ||
/** Exported symbol names that have been generated by expanding an "export * from ...". */ | ||
@@ -386,2 +360,4 @@ this.generatedExports = new Set(); | ||
var exportDecl = node; | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit('export'); | ||
if (!exportDecl.exportClause && exportDecl.moduleSpecifier) { | ||
@@ -391,8 +367,14 @@ // It's an "export * from ..." statement. | ||
var exports_1 = this.expandSymbolsFromExportStar(exportDecl); | ||
this.writeRange(exportDecl.getFullStart(), exportDecl.getStart()); | ||
this.emit("export {" + exports_1.join(',') + "} from"); | ||
this.writeRange(exportDecl.moduleSpecifier.getFullStart(), node.getEnd()); | ||
return true; | ||
this.emit(" {" + exports_1.join(',') + "}"); | ||
} | ||
return false; | ||
else { | ||
if (exportDecl.exportClause) | ||
this.visit(exportDecl.exportClause); | ||
} | ||
if (exportDecl.moduleSpecifier) { | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(exportDecl.moduleSpecifier); | ||
} | ||
this.emit(';'); | ||
return true; | ||
case ts.SyntaxKind.InterfaceDeclaration: | ||
@@ -558,4 +540,4 @@ this.emitInterface(node); | ||
var sym = exports_2[_a]; | ||
var name_1 = rewriter_1.unescapeName(sym.name); | ||
if (localSet.has(name_1)) { | ||
var name_3 = rewriter_1.unescapeName(sym.name); | ||
if (localSet.has(name_3)) { | ||
// This name is shadowed by a local definition, such as: | ||
@@ -566,8 +548,8 @@ // - export var foo ... | ||
} | ||
if (this.generatedExports.has(name_1)) { | ||
if (this.generatedExports.has(name_3)) { | ||
// Already exported via an earlier expansion of an "export * from ...". | ||
continue; | ||
} | ||
this.generatedExports.add(name_1); | ||
reexports.add(name_1); | ||
this.generatedExports.add(name_3); | ||
reexports.add(name_3); | ||
} | ||
@@ -577,2 +559,30 @@ return util_1.toArray(reexports.keys()); | ||
/** | ||
* Convert from implicit `import {} from 'pkg'` to `import {} from 'pkg/index'. | ||
* TypeScript supports the shorthand, but not all ES6 module loaders do. | ||
* Workaround for https://github.com/Microsoft/TypeScript/issues/12597 | ||
*/ | ||
Annotator.prototype.writeModuleSpecifier = function (moduleSpecifier) { | ||
if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { | ||
throw new Error("unhandled moduleSpecifier kind: " + ts.SyntaxKind[moduleSpecifier.kind]); | ||
} | ||
var moduleId = moduleSpecifier.text; | ||
if (this.options.convertIndexImportShorthand) { | ||
if (!this.tsOpts || !this.host) { | ||
throw new Error('option convertIndexImportShorthand requires that annotate be called with a TypeScript host and options.'); | ||
} | ||
var resolved = ts.resolveModuleName(moduleId, this.file.fileName, this.tsOpts, this.host); | ||
if (resolved && resolved.resolvedModule) { | ||
var resolvedModule = resolved.resolvedModule.resolvedFileName.replace(/(\.d)?\.ts$/, ''); | ||
var requestedModule = moduleId.replace(/\.js$/, ''); | ||
// If the imported module resolves to foo/index, but the specified module was foo, then we | ||
// append the /index. | ||
if (resolvedModule.substr(resolvedModule.length - 6) === '/index' && | ||
requestedModule.substr(requestedModule.length - 6) !== '/index') { | ||
moduleId += '/index'; | ||
} | ||
} | ||
} | ||
this.emit(" '" + moduleId + "'"); | ||
}; | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
@@ -584,11 +594,17 @@ * We need to do a bit of rewriting so that imported types show up under the | ||
Annotator.prototype.emitImportDeclaration = function (decl) { | ||
if (this.options.untyped) | ||
return false; | ||
this.writeRange(decl.getFullStart(), decl.getStart()); | ||
this.emit('import'); | ||
var importClause = decl.importClause; | ||
if (!importClause) { | ||
// import './foo'; | ||
return false; // Use default processing. | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
return true; | ||
} | ||
else if (importClause.name || (importClause.namedBindings && | ||
importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) { | ||
this.visit(importClause); | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
// importClause.name implies | ||
@@ -604,19 +620,20 @@ // import a from ...; | ||
// imports and make an alias for each for JSDoc purposes. | ||
var localNames = void 0; | ||
if (importClause.name) { | ||
// import a from ...; | ||
localNames = [rewriter_1.getIdentifierText(importClause.name)]; | ||
if (!this.options.untyped) { | ||
var localNames = void 0; | ||
if (importClause.name) { | ||
// import a from ...; | ||
localNames = [rewriter_1.getIdentifierText(importClause.name)]; | ||
} | ||
else { | ||
// import {a as b} from ...; | ||
var namedImports = importClause.namedBindings; | ||
localNames = namedImports.elements.map(function (imp) { return rewriter_1.getIdentifierText(imp.name); }); | ||
} | ||
for (var _i = 0, localNames_1 = localNames; _i < localNames_1.length; _i++) { | ||
var name_4 = localNames_1[_i]; | ||
// This may look like a self-reference but TypeScript will rename the | ||
// right-hand side! | ||
this.emit("\nconst " + name_4 + ": NeverTypeCheckMe = " + name_4 + "; /* local alias for Closure JSDoc */"); | ||
} | ||
} | ||
else { | ||
// import {a as b} from ...; | ||
var namedImports = importClause.namedBindings; | ||
localNames = namedImports.elements.map(function (imp) { return rewriter_1.getIdentifierText(imp.name); }); | ||
} | ||
this.writeNode(decl); | ||
for (var _i = 0, localNames_1 = localNames; _i < localNames_1.length; _i++) { | ||
var name_2 = localNames_1[_i]; | ||
// This may look like a self-reference but TypeScript will rename the | ||
// right-hand side! | ||
this.emit("\nconst " + name_2 + ": NeverTypeCheckMe = " + name_2 + "; /* local alias for Closure JSDoc */"); | ||
} | ||
return true; | ||
@@ -627,3 +644,7 @@ } | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
this.visit(importClause); | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
return true; | ||
} | ||
@@ -640,9 +661,5 @@ else { | ||
} | ||
if (jsDoc.length === 0) { | ||
this.writeRange(classDecl.getFullStart(), classDecl.getStart()); | ||
} | ||
else { | ||
this.emit('\n'); | ||
this.emit('\n'); | ||
if (jsDoc.length > 0) | ||
this.emit(jsdoc.toString(jsDoc)); | ||
} | ||
if (classDecl.members.length > 0) { | ||
@@ -758,6 +775,6 @@ // We must visit all members individually, to strip out any | ||
} | ||
var jsDoc = this.getJSDoc(p) || []; | ||
var tags = this.getJSDoc(p) || []; | ||
var existingAnnotation = ''; | ||
for (var _i = 0, jsDoc_4 = jsDoc; _i < jsDoc_4.length; _i++) { | ||
var _a = jsDoc_4[_i], tagName = _a.tagName, text = _a.text; | ||
for (var _i = 0, tags_1 = tags; _i < tags_1.length; _i++) { | ||
var _a = tags_1[_i], tagName = _a.tagName, text = _a.text; | ||
if (tagName) { | ||
@@ -770,8 +787,4 @@ existingAnnotation += "@" + tagName + "\n"; | ||
} | ||
this.emit(' /**'); | ||
if (existingAnnotation) { | ||
this.emit('\n * ' + existingAnnotation + ' *'); | ||
} | ||
var endComment = existingAnnotation ? '\n */\n' : ' */\n'; | ||
this.emit(" @type {" + this.typeToClosure(p) + "}" + endComment); | ||
tags.push({ tagName: 'type', type: this.typeToClosure(p) }); | ||
this.emit(jsdoc.toString(tags)); | ||
namespace = namespace.concat([name]); | ||
@@ -908,6 +921,6 @@ this.emit(namespace.join('.') + ";\n"); | ||
// E.g. "declare namespace foo {" | ||
var name_3 = rewriter_1.getIdentifierText(decl.name); | ||
if (name_3 === undefined) | ||
var name_5 = rewriter_1.getIdentifierText(decl.name); | ||
if (name_5 === undefined) | ||
break; | ||
namespace = namespace.concat(name_3); | ||
namespace = namespace.concat(name_5); | ||
if (this.isFirstDeclaration(decl)) { | ||
@@ -945,11 +958,17 @@ this.emit('/** @const */\n'); | ||
case ts.SyntaxKind.FunctionDeclaration: | ||
var f = node; | ||
var name_4 = f.name; | ||
if (!name_4) { | ||
this.error(f, 'anonymous function in externs'); | ||
var fnDecl = node; | ||
var name_6 = fnDecl.name; | ||
if (!name_6) { | ||
this.error(fnDecl, 'anonymous function in externs'); | ||
break; | ||
} | ||
this.emitFunctionType([f]); | ||
var params = f.parameters.map(function (p) { return p.name.getText(); }); | ||
this.writeExternsFunction(name_4.getText(), params, namespace); | ||
// Gather up all overloads of this function. | ||
var sym = this.program.getTypeChecker().getSymbolAtLocation(name_6); | ||
var decls = sym.declarations.filter(function (d) { return d.kind === | ||
ts.SyntaxKind.FunctionDeclaration; }); | ||
// Only emit the first declaration of each overloaded function. | ||
if (fnDecl !== decls[0]) | ||
break; | ||
var params = this.emitFunctionType(decls); | ||
this.writeExternsFunction(name_6.getText(), params, namespace); | ||
break; | ||
@@ -1080,8 +1099,8 @@ case ts.SyntaxKind.VariableStatement: | ||
if (decl.name.kind === ts.SyntaxKind.Identifier) { | ||
var name_5 = rewriter_1.getIdentifierText(decl.name); | ||
if (exports.closureExternsBlacklist.indexOf(name_5) >= 0) | ||
var name_7 = rewriter_1.getIdentifierText(decl.name); | ||
if (exports.closureExternsBlacklist.indexOf(name_7) >= 0) | ||
return; | ||
this.emitJSDocType(decl); | ||
this.emit('\n'); | ||
this.writeExternsVariable(name_5, namespace); | ||
this.writeExternsVariable(name_7, namespace); | ||
} | ||
@@ -1092,10 +1111,13 @@ else { | ||
}; | ||
ExternsWriter.prototype.writeExternsVariable = function (name, namespace) { | ||
ExternsWriter.prototype.writeExternsVariable = function (name, namespace, value) { | ||
var qualifiedName = namespace.concat([name]).join('.'); | ||
if (namespace.length === 0) | ||
this.emit("var "); | ||
this.emit(qualifiedName + ";\n"); | ||
this.emit(qualifiedName); | ||
if (value) | ||
this.emit(" = " + value); | ||
this.emit(';\n'); | ||
}; | ||
ExternsWriter.prototype.writeExternsFunction = function (name, params, namespace) { | ||
var paramsStr = params.map(makeLegalExternsParam).join(', '); | ||
var paramsStr = params.join(', '); | ||
if (namespace.length > 0) { | ||
@@ -1110,5 +1132,6 @@ name = namespace.concat([name]).join('.'); | ||
ExternsWriter.prototype.writeExternsEnum = function (decl, namespace) { | ||
namespace = namespace.concat([rewriter_1.getIdentifierText(decl.name)]); | ||
var name = rewriter_1.getIdentifierText(decl.name); | ||
this.emit('\n/** @const */\n'); | ||
this.emit(namespace.join('.') + " = {};\n"); | ||
this.writeExternsVariable(name, namespace, '{}'); | ||
namespace = namespace.concat([name]); | ||
for (var _i = 0, _a = decl.members; _i < _a.length; _i++) { | ||
@@ -1133,5 +1156,4 @@ var member = _a[_i]; | ||
} | ||
var name_6 = namespace.concat([memberName]).join('.'); | ||
this.emit('/** @const {number} */\n'); | ||
this.emit(name_6 + ";\n"); | ||
this.writeExternsVariable(memberName, namespace); | ||
} | ||
@@ -1145,6 +1167,6 @@ }; | ||
}(ClosureRewriter)); | ||
function annotate(program, file, options) { | ||
function annotate(program, file, options, host, tsOpts) { | ||
if (options === void 0) { options = {}; } | ||
type_translator_1.assertTypeChecked(file); | ||
return new Annotator(program, file, options).annotate(); | ||
return new Annotator(program, file, options, host, tsOpts).annotate(); | ||
} | ||
@@ -1151,0 +1173,0 @@ exports.annotate = annotate; |
@@ -176,7 +176,3 @@ /** | ||
}; | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
* is to work around the difference between TS and Closure destructuring. | ||
*/ | ||
TypeTranslator.prototype.translate = function (type, notNull) { | ||
TypeTranslator.prototype.translate = function (type) { | ||
// See the function `buildTypeDisplay` in the TypeScript compiler source | ||
@@ -219,3 +215,2 @@ // for guidance on a similar operation. | ||
return '?'; | ||
var notNullPrefix = notNull ? '!' : ''; | ||
if (type.flags & ts.TypeFlags.Class) { | ||
@@ -226,3 +221,3 @@ if (!type.symbol) { | ||
} | ||
return notNullPrefix + this.symbolToString(type.symbol); | ||
return '!' + this.symbolToString(type.symbol); | ||
} | ||
@@ -250,3 +245,3 @@ else if (type.flags & ts.TypeFlags.Interface) { | ||
} | ||
return notNullPrefix + this.symbolToString(type.symbol); | ||
return '!' + this.symbolToString(type.symbol); | ||
} | ||
@@ -270,7 +265,7 @@ else if (type.flags & ts.TypeFlags.Reference) { | ||
} | ||
typeStr += this.translate(referenceType.target, notNull); | ||
typeStr += this.translate(referenceType.target); | ||
} | ||
if (referenceType.typeArguments) { | ||
var params = referenceType.typeArguments.map(function (t) { return _this.translate(t, true); }); | ||
typeStr += isTuple ? notNullPrefix + "Array" : "<" + params.join(', ') + ">"; | ||
var params = referenceType.typeArguments.map(function (t) { return _this.translate(t); }); | ||
typeStr += isTuple ? "!Array" : "<" + params.join(', ') + ">"; | ||
} | ||
@@ -289,3 +284,3 @@ return typeStr; | ||
if (type.symbol.flags === ts.SymbolFlags.TypeLiteral) { | ||
return this.translateTypeLiteral(type, notNull); | ||
return this.translateTypeLiteral(type); | ||
} | ||
@@ -304,3 +299,3 @@ else if (type.symbol.flags === ts.SymbolFlags.Function || | ||
var unionType = type; | ||
var parts_1 = unionType.types.map(function (t) { return _this.translate(t, true); }); | ||
var parts_1 = unionType.types.map(function (t) { return _this.translate(t); }); | ||
// In union types that include boolean literal and other literals can | ||
@@ -321,4 +316,3 @@ // end up repeating the same closure type. For example: true | boolean | ||
*/ | ||
TypeTranslator.prototype.translateTypeLiteral = function (type, notNull) { | ||
var notNullPrefix = notNull ? '!' : ''; | ||
TypeTranslator.prototype.translateTypeLiteral = function (type) { | ||
// Avoid infinite loops on recursive types. | ||
@@ -346,4 +340,12 @@ // It would be nice to just emit the name of the recursive type here, | ||
var paramsStr = params.length ? (', ' + params.join(', ')) : ''; | ||
var constructedType = this.translate(ctors[0].getReturnType(), false); | ||
return "function(new: " + constructedType + paramsStr + "): ?"; | ||
var constructedType = this.translate(ctors[0].getReturnType()); | ||
// In the specific case of the "new" in a function, it appears that | ||
// function(new: !Bar) | ||
// fails to parse, while | ||
// function(new: (!Bar)) | ||
// parses in the way you'd expect. | ||
// It appears from testing that Closure ignores the ! anyway and just | ||
// assumes the result will be non-null in either case. (To be pedantic, | ||
// it's possible to return null from a ctor it seems like a bad idea.) | ||
return "function(new: (" + constructedType + ")" + paramsStr + "): ?"; | ||
} | ||
@@ -362,3 +364,3 @@ for (var _i = 0, _a = Object.keys(type.symbol.members); _i < _a.length; _i++) { | ||
// optional members are handled by the type including |undefined in a union type. | ||
var memberType = this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node), true); | ||
var memberType = this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node)); | ||
fields.push(field + ": " + memberType); | ||
@@ -386,5 +388,5 @@ } | ||
this.warn('unknown index key type'); | ||
return notNullPrefix + "Object<?,?>"; | ||
return "!Object<?,?>"; | ||
} | ||
return notNullPrefix + ("Object<" + keyType + "," + this.translate(valType, true) + ">"); | ||
return "!Object<" + keyType + "," + this.translate(valType) + ">"; | ||
} | ||
@@ -394,3 +396,3 @@ else if (!callable && !indexable) { | ||
// TODO(evanm): revisit this if it is a problem. | ||
return notNullPrefix + 'Object'; | ||
return '!Object'; | ||
} | ||
@@ -409,3 +411,3 @@ } | ||
var typeStr = "function(" + params.join(', ') + ")"; | ||
var retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig), true); | ||
var retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig)); | ||
if (retType) { | ||
@@ -420,3 +422,3 @@ typeStr += ": " + retType; | ||
var paramType = _this.typeChecker.getTypeOfSymbolAtLocation(param, _this.node); | ||
return _this.translate(paramType, true); | ||
return _this.translate(paramType); | ||
}); | ||
@@ -423,0 +425,0 @@ }; |
{ | ||
"name": "tsickle", | ||
"version": "0.2.2", | ||
"version": "0.2.3", | ||
"description": "Transpile TypeScript code to JavaScript with Closure annotations.", | ||
@@ -5,0 +5,0 @@ "main": "build/src/tsickle.js", |
@@ -47,4 +47,4 @@ /** | ||
constructor( | ||
file: ts.SourceFile, | ||
private pathToModuleName: (context: string, fileName: string) => string) { | ||
file: ts.SourceFile, private pathToModuleName: (context: string, fileName: string) => string, | ||
private prelude: string) { | ||
super(file); | ||
@@ -59,2 +59,3 @@ } | ||
this.emit(`goog.module('${moduleName}');`); | ||
if (this.prelude) this.emit(this.prelude); | ||
// Allow code to use `module.id` to discover its module URL, e.g. to resolve | ||
@@ -322,9 +323,11 @@ // a template URL against. | ||
* imports with relative paths like "import * as foo from '../foo';". | ||
* @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): {output: string, referencedModules: string[]} { | ||
pathToModuleName: (context: string, fileName: string) => string, isES5 = true, | ||
prelude = ''): {output: string, referencedModules: string[]} { | ||
let file = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES5, true); | ||
return new ES5Processor(file, pathToModuleName).process(moduleId, isES5); | ||
return new ES5Processor(file, pathToModuleName, prelude).process(moduleId, isES5); | ||
} |
@@ -120,3 +120,3 @@ /** | ||
*/ | ||
const JSDOC_TAGS_BLACKLIST = ['private', 'public', 'type']; | ||
const JSDOC_TAGS_BLACKLIST = ['constructor', 'extends', 'implements', 'private', 'public', 'type']; | ||
@@ -138,3 +138,3 @@ /** A list of JSDoc @tags that might include a {type} after them. */ | ||
// Strip all the " * " bits from the front of each line. | ||
comment = comment.replace(/^\s*\* /gm, ''); | ||
comment = comment.replace(/^\s*\*? ?/gm, ''); | ||
let lines = comment.split('\n'); | ||
@@ -187,5 +187,5 @@ let tags: Tag[] = []; | ||
if (tags.length === 0) { | ||
tags.push({text: line.trim()}); | ||
tags.push({text: line}); | ||
} else { | ||
tags[tags.length - 1].text += '\n * ' + line.trim(); | ||
tags[tags.length - 1].text += '\n' + line; | ||
} | ||
@@ -200,29 +200,50 @@ } | ||
/** | ||
* Serializes a Tag into a string usable in a comment. | ||
* Returns a string like " @foo {bar} baz" (note the whitespace). | ||
*/ | ||
function tagToString(tag: Tag): string { | ||
let out = ''; | ||
if (tag.tagName) { | ||
out += ` @${tag.tagName}`; | ||
} | ||
if (tag.type) { | ||
out += ' {'; | ||
if (tag.restParam) { | ||
out += '...'; | ||
} | ||
out += tag.type; | ||
if (tag.optional) { | ||
out += '='; | ||
} | ||
out += '}'; | ||
} | ||
if (tag.parameterName) { | ||
out += ' ' + tag.parameterName; | ||
} | ||
if (tag.text) { | ||
out += ' ' + tag.text.replace(/@/g, '\\@'); | ||
} | ||
return out; | ||
} | ||
/** Serializes a Comment out to a string usable in source code. */ | ||
export function toString(tags: Tag[]): string { | ||
if (tags.length === 0) return ''; | ||
if (tags.length === 1) { | ||
let 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. | ||
// /** @type {foo} */ | ||
return '/**' + tagToString(tag) + ' */\n'; | ||
} | ||
// Otherwise, fall through to the multi-line output. | ||
} | ||
let out = ''; | ||
out += '/**\n'; | ||
for (let tag of tags) { | ||
out += ' * '; | ||
if (tag.tagName) { | ||
out += `@${tag.tagName}`; | ||
} | ||
if (tag.type) { | ||
out += ' {'; | ||
if (tag.restParam) { | ||
out += '...'; | ||
} | ||
out += tag.type; | ||
if (tag.optional) { | ||
out += '='; | ||
} | ||
out += '}'; | ||
} | ||
if (tag.parameterName) { | ||
out += ' ' + tag.parameterName; | ||
} | ||
if (tag.text) { | ||
out += ' ' + tag.text; | ||
} | ||
out += ' *'; | ||
// If the tagToString is multi-line, insert " * " prefixes on subsequent lines. | ||
out += tagToString(tag).split('\n').join('\n * '); | ||
out += '\n'; | ||
@@ -233,1 +254,25 @@ } | ||
} | ||
/** Merges multiple tags (of the same tagName type) into a single unified tag. */ | ||
export function merge(tags: Tag[]): Tag { | ||
let tagNames = new Set<string>(); | ||
let parameterNames = new Set<string>(); | ||
let types = new Set<string>(); | ||
let texts = new Set<string>(); | ||
for (const tag of tags) { | ||
if (tag.tagName) tagNames.add(tag.tagName); | ||
if (tag.parameterName) parameterNames.add(tag.parameterName); | ||
if (tag.type) types.add(tag.type); | ||
if (tag.text) texts.add(tag.text); | ||
} | ||
if (tagNames.size !== 1) { | ||
throw new Error(`cannot merge differing tags: ${JSON.stringify(tags)}`); | ||
} | ||
const tagName = tagNames.values().next().value; | ||
const parameterName = | ||
parameterNames.size > 0 ? Array.from(parameterNames).join('_or_') : undefined; | ||
const type = types.size > 0 ? Array.from(types).join('|') : undefined; | ||
const text = texts.size > 0 ? Array.from(texts).join(' / ') : undefined; | ||
return {tagName, parameterName, type, text}; | ||
} |
@@ -34,2 +34,7 @@ /** | ||
typeBlackListPaths?: Set<string>; | ||
/** | ||
* Convert shorthand "/index" imports to full path (include the "/index"). | ||
* Annotation will be slower because every import must be resolved. | ||
*/ | ||
convertIndexImportShorthand?: boolean; | ||
} | ||
@@ -94,19 +99,27 @@ | ||
/** | ||
* TypeScript allows parameters named "arguments", but Closure | ||
* disallows this, even in externs. | ||
*/ | ||
function makeLegalExternsParam(name: string): string { | ||
switch (name) { | ||
case 'arguments': | ||
return 'tsickle_arguments'; | ||
function isDtsFileName(fileName: string): boolean { | ||
return /\.d\.ts$/.test(fileName); | ||
} | ||
/** Returns the Closure name of a function parameter, special-casing destructuring. */ | ||
function getParameterName(param: ts.ParameterDeclaration, index: number): string { | ||
switch (param.name.kind) { | ||
case ts.SyntaxKind.Identifier: | ||
let name = getIdentifierText(param.name as ts.Identifier); | ||
// TypeScript allows parameters named "arguments", but Closure | ||
// disallows this, even in externs. | ||
if (name === 'arguments') name = 'tsickle_arguments'; | ||
return name; | ||
case ts.SyntaxKind.ArrayBindingPattern: | ||
case ts.SyntaxKind.ObjectBindingPattern: | ||
// Closure crashes if you put a binding pattern in the externs. | ||
// Avoid this by just generating an unused name; the name is | ||
// ignored anyway. | ||
return `__${index}`; | ||
default: | ||
return name; | ||
// The above list of kinds should be exhaustive. | ||
throw new Error(`unhandled function parameter kind: ${ts.SyntaxKind[param.name.kind]}`); | ||
} | ||
} | ||
function isDtsFileName(fileName: string): boolean { | ||
return /\.d\.ts$/.test(fileName); | ||
} | ||
const VISIBILITY_FLAGS = ts.NodeFlags.Private | ts.NodeFlags.Protected | ts.NodeFlags.Public; | ||
@@ -129,3 +142,2 @@ | ||
* - Total number of parameters will be the maximum count found across all variants. | ||
* - Any parameters beyond the minimum count found across all variants; prefix: "opt_" | ||
* - Different names at the same parameter index will be joined with "_or_" | ||
@@ -139,16 +151,15 @@ * - Variable args (...type[] in TypeScript) will be output as "...type", | ||
emitFunctionType(fnDecls: ts.SignatureDeclaration[], extraTags: jsdoc.Tag[] = []): string[] { | ||
let typeChecker = this.program.getTypeChecker(); | ||
const typeChecker = this.program.getTypeChecker(); | ||
let newDoc = extraTags; | ||
let paramNamesToReturn: string[] = []; | ||
const abstract = {tagName: 'abstract'}; | ||
const lens = fnDecls.map(fnDecl => fnDecl.parameters.length); | ||
const minCount = Math.min(...lens); | ||
const maxCount = Math.max(...lens); | ||
const isOverloaded = fnDecls.length > 1; | ||
const isConstructor = fnDecls[0].kind === ts.SyntaxKind.Constructor; | ||
let paramData = { | ||
names: new Array<Set<string>>(maxCount), | ||
types: new Array<Set<string>>(maxCount), | ||
returns: new Set<string>() | ||
}; | ||
const minArgsCount = Math.min(...lens); | ||
const maxArgsCount = Math.max(...lens); | ||
const isConstructor = fnDecls.find(d => d.kind === ts.SyntaxKind.Constructor) !== undefined; | ||
// For each parameter index i, paramTags[i] is an array of parameters | ||
// that can be found at index i. E.g. | ||
// function foo(x: string) | ||
// function foo(y: number, z: string) | ||
// then paramTags[0] = [info about x, info about y]. | ||
const paramTags: jsdoc.Tag[][] = []; | ||
const returnTags: jsdoc.Tag[] = []; | ||
@@ -162,35 +173,33 @@ for (let fnDecl of fnDecls) { | ||
// Copy all the tags other than @param/@return into the new | ||
// comment without any change; @param/@return are handled later. | ||
// TODO: handle for overloads. indexOf on the array doesn't match instances of the Tag type, | ||
// so another matching strategy is needed to avoid dupes. Also, there may be problems if | ||
// an annotation doesn't apply to all overloads. | ||
// JSDoc without any change; @param/@return are handled specially. | ||
// TODO: there may be problems if an annotation doesn't apply to all overloads; | ||
// is it worth checking for this and erroring? | ||
for (let tag of jsDoc) { | ||
if (tag.tagName === 'param' || tag.tagName === 'return' || isOverloaded) continue; | ||
if (tag.tagName === 'param' || tag.tagName === 'return') continue; | ||
newDoc.push(tag); | ||
} | ||
// Abstract | ||
if ((fnDecl.flags & ts.NodeFlags.Abstract) && (newDoc.indexOf(abstract) === -1)) { | ||
newDoc.push(abstract); | ||
// Add @abstract on "abstract" declarations. | ||
if (fnDecl.flags & ts.NodeFlags.Abstract) { | ||
newDoc.push({tagName: 'abstract'}); | ||
} | ||
// Merge the parameters into a single list of merged names and list of types | ||
let sig = typeChecker.getSignatureFromDeclaration(fnDecl); | ||
// Parameters. | ||
// Iterate through both the AST parameter list and the type's parameter | ||
// list, as some information is only available in the former. | ||
for (let i = 0; i < sig.parameters.length; i++) { | ||
let paramNode = fnDecl.parameters[i]; | ||
let paramSym = sig.parameters[i]; | ||
let type = typeChecker.getTypeOfSymbolAtLocation(paramSym, fnDecl); | ||
const sig = typeChecker.getSignatureFromDeclaration(fnDecl); | ||
for (let i = 0; i < sig.declaration.parameters.length; i++) { | ||
const paramNode = sig.declaration.parameters[i]; | ||
const destructuring = | ||
(paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
paramNode.name.kind === ts.SyntaxKind.ObjectBindingPattern); | ||
const name = getParameterName(paramNode, i); | ||
const isThisParam = name === 'this'; | ||
let newTag: jsdoc.Tag = { | ||
tagName: 'param', | ||
tagName: isThisParam ? 'this' : 'param', | ||
optional: paramNode.initializer !== undefined || paramNode.questionToken !== undefined, | ||
parameterName: makeLegalExternsParam(unescapeName(paramSym.getName())), | ||
parameterName: isThisParam ? undefined : name, | ||
}; | ||
let destructuring = | ||
(paramNode.name.kind === ts.SyntaxKind.ArrayBindingPattern || | ||
paramNode.name.kind === ts.SyntaxKind.ObjectBindingPattern); | ||
let type = typeChecker.getTypeAtLocation(paramNode); | ||
if (paramNode.dotDotDotToken !== undefined) { | ||
@@ -203,37 +212,13 @@ newTag.restParam = true; | ||
} | ||
newTag.type = this.typeToClosure(fnDecl, type, destructuring); | ||
if (!isOverloaded) { | ||
// Search for this parameter in the JSDoc @params. | ||
// TODO: Consider adding text from existing JSDoc into overloads | ||
for (let {tagName, parameterName, text} of jsDoc) { | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
break; | ||
} | ||
for (let {tagName, parameterName, text} of jsDoc) { | ||
if (tagName === 'param' && parameterName === newTag.parameterName) { | ||
newTag.text = text; | ||
break; | ||
} | ||
// push to the newDoc for rendering the single function | ||
newDoc.push(newTag); | ||
} else { | ||
// build set of param names at this index | ||
if (newTag.parameterName) { | ||
if (!paramData.names[i]) { | ||
paramData.names[i] = new Set<string>(); | ||
} | ||
paramData.names[i].add(newTag.parameterName); | ||
} | ||
// build set of type names at this index | ||
if (newTag.type) { | ||
if (!paramData.types[i]) { | ||
paramData.types[i] = new Set<string>(); | ||
} | ||
paramData.types[i].add(newTag.type); | ||
} | ||
} | ||
if (!paramTags[i]) paramTags.push([]); | ||
paramTags[i].push(newTag); | ||
} | ||
// If this method is not overloaded, we set the list of names to those in the only declaration | ||
if (!isOverloaded) { | ||
paramNamesToReturn = fnDecl.parameters.map(p => p.name.getText()); | ||
} | ||
@@ -244,46 +229,41 @@ // Return type. | ||
let retTypeString: string = this.typeToClosure(fnDecl, retType); | ||
if (!isOverloaded) { | ||
// TODO: Consider adding text from existing JSDoc into overloads | ||
let returnDoc: string|undefined; | ||
for (let {tagName, text} of jsDoc) { | ||
if (tagName === 'return') { | ||
returnDoc = text; | ||
break; | ||
} | ||
let returnDoc: string|undefined; | ||
for (let {tagName, text} of jsDoc) { | ||
if (tagName === 'return') { | ||
returnDoc = text; | ||
break; | ||
} | ||
newDoc.push({ | ||
tagName: 'return', | ||
type: retTypeString, | ||
text: returnDoc, | ||
}); | ||
} else { | ||
if (retTypeString) { | ||
paramData.returns.add(retTypeString); | ||
} | ||
} | ||
returnTags.push({ | ||
tagName: 'return', | ||
type: retTypeString, | ||
text: returnDoc, | ||
}); | ||
} | ||
} | ||
if (isOverloaded) { | ||
// Build actual JSDoc tags for the merged param names/types and return types | ||
for (let i = 0; i < maxCount; i++) { | ||
paramNamesToReturn.push(Array.from(paramData.names[i].values()).join('_or_')); | ||
if (i >= minCount) { | ||
paramNamesToReturn[i] = 'opt_' + paramNamesToReturn[i]; | ||
} | ||
let concatenatedTypes = Array.from(paramData.types[i].values()).join('|'); | ||
newDoc.push({ | ||
tagName: 'param', | ||
parameterName: paramNamesToReturn[i], | ||
type: concatenatedTypes, | ||
}); | ||
// Merge the JSDoc tags for each overloaded parameter. | ||
for (let i = 0; i < maxArgsCount; i++) { | ||
let paramTag = jsdoc.merge(paramTags[i]); | ||
// If any overload marks this param optional, mark it optional in the | ||
// merged output. | ||
const optional = paramTags[i].find(t => t.optional === true) !== undefined; | ||
if (optional || i >= minArgsCount) { | ||
paramTag.type += '='; | ||
} | ||
if (!isConstructor) { | ||
newDoc.push({ | ||
tagName: 'return', | ||
type: Array.from(paramData.returns.values()).join('|'), | ||
}); | ||
// If any overload marks this param as a ..., mark it ... in the | ||
// merged output. | ||
if (paramTags[i].find(t => t.restParam === true) !== undefined) { | ||
paramTag.restParam = true; | ||
} | ||
newDoc.push(paramTag); | ||
} | ||
// Merge the JSDoc tags for each overloaded return. | ||
if (!isConstructor) { | ||
newDoc.push(jsdoc.merge(returnTags)); | ||
} | ||
this.emit('\n' + jsdoc.toString(newDoc)); | ||
return paramNamesToReturn; | ||
return newDoc.filter(t => t.tagName === 'param').map(t => t.parameterName!); | ||
} | ||
@@ -349,3 +329,3 @@ | ||
translator.warn = msg => this.debugWarn(context, msg); | ||
return translator.translate(type, true); | ||
return translator.translate(type); | ||
} | ||
@@ -385,3 +365,5 @@ | ||
constructor(program: ts.Program, file: ts.SourceFile, options: Options) { | ||
constructor( | ||
program: ts.Program, file: ts.SourceFile, options: Options, | ||
private host?: ts.ModuleResolutionHost, private tsOpts?: ts.CompilerOptions) { | ||
super(program, file, options); | ||
@@ -434,2 +416,4 @@ this.externsWriter = new ExternsWriter(program, file, options); | ||
let exportDecl = <ts.ExportDeclaration>node; | ||
this.writeRange(node.getFullStart(), node.getStart()); | ||
this.emit('export'); | ||
if (!exportDecl.exportClause && exportDecl.moduleSpecifier) { | ||
@@ -439,8 +423,12 @@ // It's an "export * from ..." statement. | ||
let exports = this.expandSymbolsFromExportStar(exportDecl); | ||
this.writeRange(exportDecl.getFullStart(), exportDecl.getStart()); | ||
this.emit(`export {${exports.join(',')}} from`); | ||
this.writeRange(exportDecl.moduleSpecifier.getFullStart(), node.getEnd()); | ||
return true; | ||
this.emit(` {${exports.join(',')}}`); | ||
} else { | ||
if (exportDecl.exportClause) this.visit(exportDecl.exportClause); | ||
} | ||
return false; | ||
if (exportDecl.moduleSpecifier) { | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(exportDecl.moduleSpecifier); | ||
} | ||
this.emit(';'); | ||
return true; | ||
case ts.SyntaxKind.InterfaceDeclaration: | ||
@@ -631,2 +619,32 @@ this.emitInterface(node as ts.InterfaceDeclaration); | ||
/** | ||
* Convert from implicit `import {} from 'pkg'` to `import {} from 'pkg/index'. | ||
* TypeScript supports the shorthand, but not all ES6 module loaders do. | ||
* Workaround for https://github.com/Microsoft/TypeScript/issues/12597 | ||
*/ | ||
private writeModuleSpecifier(moduleSpecifier: ts.Expression) { | ||
if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { | ||
throw new Error(`unhandled moduleSpecifier kind: ${ts.SyntaxKind[moduleSpecifier.kind]}`); | ||
} | ||
let moduleId = (moduleSpecifier as ts.StringLiteral).text; | ||
if (this.options.convertIndexImportShorthand) { | ||
if (!this.tsOpts || !this.host) { | ||
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); | ||
if (resolved && resolved.resolvedModule) { | ||
const resolvedModule = resolved.resolvedModule.resolvedFileName.replace(/(\.d)?\.ts$/, ''); | ||
const requestedModule = moduleId.replace(/\.js$/, ''); | ||
// If the imported module resolves to foo/index, but the specified module was foo, then we | ||
// append the /index. | ||
if (resolvedModule.substr(resolvedModule.length - 6) === '/index' && | ||
requestedModule.substr(requestedModule.length - 6) !== '/index') { | ||
moduleId += '/index'; | ||
} | ||
} | ||
} | ||
this.emit(` '${moduleId}'`); | ||
} | ||
/** | ||
* Handles emit of an "import ..." statement. | ||
@@ -638,11 +656,19 @@ * We need to do a bit of rewriting so that imported types show up under the | ||
private emitImportDeclaration(decl: ts.ImportDeclaration): boolean { | ||
if (this.options.untyped) return false; | ||
this.writeRange(decl.getFullStart(), decl.getStart()); | ||
this.emit('import'); | ||
const importClause = decl.importClause; | ||
if (!importClause) { | ||
// import './foo'; | ||
return false; // Use default processing. | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
return true; | ||
} else if ( | ||
importClause.name || (importClause.namedBindings && | ||
importClause.namedBindings.kind === ts.SyntaxKind.NamedImports)) { | ||
this.visit(importClause); | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
// importClause.name implies | ||
@@ -658,20 +684,20 @@ // import a from ...; | ||
// imports and make an alias for each for JSDoc purposes. | ||
if (!this.options.untyped) { | ||
let localNames: string[]; | ||
if (importClause.name) { | ||
// import a from ...; | ||
localNames = [getIdentifierText(importClause.name)]; | ||
} else { | ||
// import {a as b} from ...; | ||
const namedImports = importClause.namedBindings as ts.NamedImports; | ||
localNames = namedImports.elements.map(imp => getIdentifierText(imp.name)); | ||
} | ||
let localNames: string[]; | ||
if (importClause.name) { | ||
// import a from ...; | ||
localNames = [getIdentifierText(importClause.name)]; | ||
} else { | ||
// import {a as b} from ...; | ||
const namedImports = importClause.namedBindings as ts.NamedImports; | ||
localNames = namedImports.elements.map(imp => getIdentifierText(imp.name)); | ||
for (let name of localNames) { | ||
// This may look like a self-reference but TypeScript will rename the | ||
// right-hand side! | ||
this.emit( | ||
`\nconst ${name}: NeverTypeCheckMe = ${name}; /* local alias for Closure JSDoc */`); | ||
} | ||
} | ||
this.writeNode(decl); | ||
for (let name of localNames) { | ||
// This may look like a self-reference but TypeScript will rename the | ||
// right-hand side! | ||
this.emit( | ||
`\nconst ${name}: NeverTypeCheckMe = ${name}; /* local alias for Closure JSDoc */`); | ||
} | ||
return true; | ||
@@ -682,3 +708,7 @@ } else if ( | ||
// import * as foo from ...; | ||
return false; // Use default processing. | ||
this.visit(importClause); | ||
this.emit(' from'); | ||
this.writeModuleSpecifier(decl.moduleSpecifier); | ||
this.emit(';'); | ||
return true; | ||
} else { | ||
@@ -695,8 +725,4 @@ this.errorUnimplementedKind(decl, 'unexpected kind of import'); | ||
} | ||
if (jsDoc.length === 0) { | ||
this.writeRange(classDecl.getFullStart(), classDecl.getStart()); | ||
} else { | ||
this.emit('\n'); | ||
this.emit(jsdoc.toString(jsDoc)); | ||
} | ||
this.emit('\n'); | ||
if (jsDoc.length > 0) this.emit(jsdoc.toString(jsDoc)); | ||
if (classDecl.members.length > 0) { | ||
@@ -813,5 +839,5 @@ // We must visit all members individually, to strip out any | ||
let jsDoc = this.getJSDoc(p) || []; | ||
let tags = this.getJSDoc(p) || []; | ||
let existingAnnotation = ''; | ||
for (let {tagName, text} of jsDoc) { | ||
for (let {tagName, text} of tags) { | ||
if (tagName) { | ||
@@ -823,8 +849,4 @@ existingAnnotation += `@${tagName}\n`; | ||
} | ||
this.emit(' /**'); | ||
if (existingAnnotation) { | ||
this.emit('\n * ' + existingAnnotation + ' *'); | ||
} | ||
const endComment = existingAnnotation ? '\n */\n' : ' */\n'; | ||
this.emit(` @type {${this.typeToClosure(p)}}${endComment}`); | ||
tags.push({tagName: 'type', type: this.typeToClosure(p)}); | ||
this.emit(jsdoc.toString(tags)); | ||
namespace = namespace.concat([name]); | ||
@@ -985,10 +1007,17 @@ this.emit(`${namespace.join('.')};\n`); | ||
case ts.SyntaxKind.FunctionDeclaration: | ||
const f = <ts.FunctionDeclaration>node; | ||
const name = f.name; | ||
const fnDecl = node as ts.FunctionDeclaration; | ||
const name = fnDecl.name; | ||
if (!name) { | ||
this.error(f, 'anonymous function in externs'); | ||
this.error(fnDecl, 'anonymous function in externs'); | ||
break; | ||
} | ||
this.emitFunctionType([f]); | ||
const params = f.parameters.map((p) => p.name.getText()); | ||
// Gather up all overloads of this function. | ||
const sym = this.program.getTypeChecker().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; | ||
const params = this.emitFunctionType(decls); | ||
this.writeExternsFunction(name.getText(), params, namespace); | ||
@@ -1126,10 +1155,12 @@ break; | ||
private writeExternsVariable(name: string, namespace: string[]) { | ||
private writeExternsVariable(name: string, namespace: string[], value?: string) { | ||
let qualifiedName = namespace.concat([name]).join('.'); | ||
if (namespace.length === 0) this.emit(`var `); | ||
this.emit(`${qualifiedName};\n`); | ||
this.emit(qualifiedName); | ||
if (value) this.emit(` = ${value}`); | ||
this.emit(';\n'); | ||
} | ||
private writeExternsFunction(name: string, params: string[], namespace: string[]) { | ||
let paramsStr = params.map(makeLegalExternsParam).join(', '); | ||
let paramsStr = params.join(', '); | ||
if (namespace.length > 0) { | ||
@@ -1144,5 +1175,6 @@ name = namespace.concat([name]).join('.'); | ||
private writeExternsEnum(decl: ts.EnumDeclaration, namespace: string[]) { | ||
namespace = namespace.concat([getIdentifierText(decl.name)]); | ||
const name = getIdentifierText(decl.name); | ||
this.emit('\n/** @const */\n'); | ||
this.emit(`${namespace.join('.')} = {};\n`); | ||
this.writeExternsVariable(name, namespace, '{}'); | ||
namespace = namespace.concat([name]); | ||
for (let member of decl.members) { | ||
@@ -1165,5 +1197,4 @@ let memberName: string|undefined; | ||
} | ||
let name = namespace.concat([memberName]).join('.'); | ||
this.emit('/** @const {number} */\n'); | ||
this.emit(`${name};\n`); | ||
this.writeExternsVariable(memberName, namespace); | ||
} | ||
@@ -1178,5 +1209,7 @@ } | ||
export function annotate(program: ts.Program, file: ts.SourceFile, options: Options = {}): Output { | ||
export function annotate( | ||
program: ts.Program, file: ts.SourceFile, options: Options = {}, host?: ts.ModuleResolutionHost, | ||
tsOpts?: ts.CompilerOptions): Output { | ||
assertTypeChecked(file); | ||
return new Annotator(program, file, options).annotate(); | ||
return new Annotator(program, file, options, host, tsOpts).annotate(); | ||
} |
@@ -184,7 +184,3 @@ /** | ||
/** | ||
* @param notNull When true, insert a ! before any type references. This | ||
* is to work around the difference between TS and Closure destructuring. | ||
*/ | ||
translate(type: ts.Type, notNull: boolean): string { | ||
translate(type: ts.Type): string { | ||
// See the function `buildTypeDisplay` in the TypeScript compiler source | ||
@@ -228,4 +224,2 @@ // for guidance on a similar operation. | ||
let notNullPrefix = notNull ? '!' : ''; | ||
if (type.flags & ts.TypeFlags.Class) { | ||
@@ -236,3 +230,3 @@ if (!type.symbol) { | ||
} | ||
return notNullPrefix + this.symbolToString(type.symbol); | ||
return '!' + this.symbolToString(type.symbol); | ||
} else if (type.flags & ts.TypeFlags.Interface) { | ||
@@ -259,3 +253,3 @@ // Note: ts.InterfaceType has a typeParameters field, but that | ||
} | ||
return notNullPrefix + this.symbolToString(type.symbol); | ||
return '!' + this.symbolToString(type.symbol); | ||
} else if (type.flags & ts.TypeFlags.Reference) { | ||
@@ -280,7 +274,7 @@ // A reference to another type, e.g. Array<number> refers to Array. | ||
} | ||
typeStr += this.translate(referenceType.target, notNull); | ||
typeStr += this.translate(referenceType.target); | ||
} | ||
if (referenceType.typeArguments) { | ||
let params = referenceType.typeArguments.map(t => this.translate(t, true)); | ||
typeStr += isTuple ? notNullPrefix + `Array` : `<${params.join(', ')}>`; | ||
let params = referenceType.typeArguments.map(t => this.translate(t)); | ||
typeStr += isTuple ? `!Array` : `<${params.join(', ')}>`; | ||
} | ||
@@ -299,3 +293,3 @@ return typeStr; | ||
if (type.symbol.flags === ts.SymbolFlags.TypeLiteral) { | ||
return this.translateTypeLiteral(type, notNull); | ||
return this.translateTypeLiteral(type); | ||
} else if ( | ||
@@ -313,3 +307,3 @@ type.symbol.flags === ts.SymbolFlags.Function || | ||
let unionType = type as ts.UnionType; | ||
let parts = unionType.types.map(t => this.translate(t, true)); | ||
let parts = unionType.types.map(t => this.translate(t)); | ||
// In union types that include boolean literal and other literals can | ||
@@ -331,4 +325,3 @@ // end up repeating the same closure type. For example: true | boolean | ||
*/ | ||
private translateTypeLiteral(type: ts.Type, notNull: boolean): string { | ||
const notNullPrefix = notNull ? '!' : ''; | ||
private translateTypeLiteral(type: ts.Type): string { | ||
// Avoid infinite loops on recursive types. | ||
@@ -357,4 +350,12 @@ // It would be nice to just emit the name of the recursive type here, | ||
const paramsStr = params.length ? (', ' + params.join(', ')) : ''; | ||
const constructedType = this.translate(ctors[0].getReturnType(), false); | ||
return `function(new: ${constructedType}${paramsStr}): ?`; | ||
const constructedType = this.translate(ctors[0].getReturnType()); | ||
// In the specific case of the "new" in a function, it appears that | ||
// function(new: !Bar) | ||
// fails to parse, while | ||
// function(new: (!Bar)) | ||
// parses in the way you'd expect. | ||
// It appears from testing that Closure ignores the ! anyway and just | ||
// assumes the result will be non-null in either case. (To be pedantic, | ||
// it's possible to return null from a ctor it seems like a bad idea.) | ||
return `function(new: (${constructedType})${paramsStr}): ?`; | ||
} | ||
@@ -374,3 +375,3 @@ | ||
let memberType = | ||
this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node), true); | ||
this.translate(this.typeChecker.getTypeOfSymbolAtLocation(member, this.node)); | ||
fields.push(`${field}: ${memberType}`); | ||
@@ -398,9 +399,9 @@ } | ||
this.warn('unknown index key type'); | ||
return notNullPrefix + `Object<?,?>`; | ||
return `!Object<?,?>`; | ||
} | ||
return notNullPrefix + `Object<${keyType},${this.translate(valType, true)}>`; | ||
return `!Object<${keyType},${this.translate(valType)}>`; | ||
} else if (!callable && !indexable) { | ||
// Special-case the empty object {} because Closure doesn't like it. | ||
// TODO(evanm): revisit this if it is a problem. | ||
return notNullPrefix + 'Object'; | ||
return '!Object'; | ||
} | ||
@@ -423,3 +424,3 @@ } | ||
let retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig), true); | ||
let retType = this.translate(this.typeChecker.getReturnTypeOfSignature(sig)); | ||
if (retType) { | ||
@@ -435,3 +436,3 @@ typeStr += `: ${retType}`; | ||
let paramType = this.typeChecker.getTypeOfSymbolAtLocation(param, this.node); | ||
return this.translate(paramType, true); | ||
return this.translate(paramType); | ||
}); | ||
@@ -438,0 +439,0 @@ } |
@@ -5,2 +5,3 @@ { | ||
"module": "commonjs", | ||
"declaration": true, | ||
"noImplicitAny": true, | ||
@@ -7,0 +8,0 @@ "noEmitOnError": true, |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
561347
62
9200