| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | import * as path from 'path'; | 
					
						
							|  |  |  | import * as ts from 'typescript'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const baseTsOptions: ts.CompilerOptions = { | 
					
						
							|  |  |  |   // We don't want symbols from external modules to be resolved, so we use the
 | 
					
						
							|  |  |  |   // classic algorithm.
 | 
					
						
							|  |  |  |   moduleResolution: ts.ModuleResolutionKind.Classic | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | export interface JsDocTagOptions { | 
					
						
							|  |  |  |   /** | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |    * An array of names of jsdoc tags, one of which must exist. If no tags are provided, there are no | 
					
						
							|  |  |  |    * required tags. | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |   requireAtLeastOne?: string[]; | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * An array of names of jsdoc tags that must not exist. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   banned?: string[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * An array of names of jsdoc tags that will be copied to the serialized code. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   toCopy?: string[]; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | export interface SerializationOptions { | 
					
						
							|  |  |  |   /** | 
					
						
							|  |  |  |    * Removes all exports matching the regular expression. | 
					
						
							|  |  |  |    */ | 
					
						
							| 
									
										
										
										
											2018-09-19 13:40:21 +02:00
										 |  |  |   stripExportPattern?: RegExp|RegExp[]; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   /** | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  |    * Allows these identifiers as modules in the output. For example, | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |    * ```
 | 
					
						
							|  |  |  |    * import * as angular from './angularjs'; | 
					
						
							|  |  |  |    * | 
					
						
							|  |  |  |    * export class Foo extends angular.Bar {} | 
					
						
							|  |  |  |    * ```
 | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  |    * will produce `export class Foo extends angular.Bar {}` and requires explicitly allowing | 
					
						
							|  |  |  |    * `angular` as a module identifier. | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |    */ | 
					
						
							|  |  |  |   allowModuleIdentifiers?: string[]; | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   /** The jsdoc tag options for top level exports */ | 
					
						
							|  |  |  |   exportTags?: JsDocTagOptions; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** The jsdoc tag options for properties/methods/etc of exports */ | 
					
						
							|  |  |  |   memberTags?: JsDocTagOptions; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   /** The jsdoc tag options for parameters of members/functions */ | 
					
						
							|  |  |  |   paramTags?: JsDocTagOptions; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export type DiagnosticSeverity = 'warn' | 'error' | 'none'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function publicApi(fileName: string, options: SerializationOptions = {}): string { | 
					
						
							|  |  |  |   return publicApiInternal(ts.createCompilerHost(baseTsOptions), fileName, baseTsOptions, options); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export function publicApiInternal( | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     host: ts.CompilerHost, fileName: string, tsOptions: ts.CompilerOptions, | 
					
						
							|  |  |  |     options: SerializationOptions = {}): string { | 
					
						
							| 
									
										
										
										
											2018-07-23 22:36:24 +02:00
										 |  |  |   // Since the entry point will be compared with the source files from the TypeScript program,
 | 
					
						
							|  |  |  |   // the path needs to be normalized with forward slashes in order to work within Windows.
 | 
					
						
							|  |  |  |   const entrypoint = path.normalize(fileName).replace(/\\/g, '/'); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |   // Setup default tag options
 | 
					
						
							|  |  |  |   options = { | 
					
						
							|  |  |  |     ...options, | 
					
						
							|  |  |  |     exportTags: applyDefaultTagOptions(options.exportTags), | 
					
						
							|  |  |  |     memberTags: applyDefaultTagOptions(options.memberTags), | 
					
						
							|  |  |  |     paramTags: applyDefaultTagOptions(options.paramTags) | 
					
						
							|  |  |  |   }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   if (!entrypoint.match(/\.d\.ts$/)) { | 
					
						
							|  |  |  |     throw new Error(`Source file "${fileName}" is not a declaration file`); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   const program = ts.createProgram([entrypoint], tsOptions, host); | 
					
						
							|  |  |  |   return new ResolvedDeclarationEmitter(program, entrypoint, options).emit(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface Diagnostic { | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |   type?: DiagnosticSeverity; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   message: string; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ResolvedDeclarationEmitter { | 
					
						
							|  |  |  |   private program: ts.Program; | 
					
						
							|  |  |  |   private fileName: string; | 
					
						
							|  |  |  |   private typeChecker: ts.TypeChecker; | 
					
						
							|  |  |  |   private options: SerializationOptions; | 
					
						
							|  |  |  |   private diagnostics: Diagnostic[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(program: ts.Program, fileName: string, options: SerializationOptions) { | 
					
						
							|  |  |  |     this.program = program; | 
					
						
							|  |  |  |     this.fileName = fileName; | 
					
						
							|  |  |  |     this.options = options; | 
					
						
							|  |  |  |     this.diagnostics = []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     this.typeChecker = this.program.getTypeChecker(); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   emit(): string { | 
					
						
							|  |  |  |     const sourceFile = this.program.getSourceFiles().find(sf => sf.fileName === this.fileName); | 
					
						
							|  |  |  |     if (!sourceFile) { | 
					
						
							|  |  |  |       throw new Error(`Source file "${this.fileName}" not found`); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-26 14:44:45 -08:00
										 |  |  |     let output: string[] = []; | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     const resolvedSymbols = this.getResolvedSymbols(sourceFile); | 
					
						
							|  |  |  |     // Sort all symbols so that the output is more deterministic
 | 
					
						
							|  |  |  |     resolvedSymbols.sort(symbolCompareFunction); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for (const symbol of resolvedSymbols) { | 
					
						
							| 
									
										
										
										
											2018-09-19 13:40:21 +02:00
										 |  |  |       if (this.isExportPatternStripped(symbol.name)) { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-26 14:44:45 -08:00
										 |  |  |       const typeDecl = symbol.declarations && symbol.declarations[0]; | 
					
						
							|  |  |  |       const valDecl = symbol.valueDeclaration; | 
					
						
							|  |  |  |       if (!typeDecl && !valDecl) { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         this.diagnostics.push({ | 
					
						
							|  |  |  |           type: 'warn', | 
					
						
							|  |  |  |           message: `${sourceFile.fileName}: error: No declaration found for symbol "${symbol.name}"` | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         continue; | 
					
						
							|  |  |  |       } | 
					
						
							| 
									
										
										
										
											2018-11-26 14:44:45 -08:00
										 |  |  |       typeDecl && this.emitDeclaration(symbol, typeDecl, output); | 
					
						
							|  |  |  |       if (valDecl && typeDecl.kind === ts.SyntaxKind.InterfaceDeclaration) { | 
					
						
							|  |  |  |         // Only generate value declarations in case of interfaces.
 | 
					
						
							|  |  |  |         valDecl && this.emitDeclaration(symbol, valDecl, output); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (this.diagnostics.length) { | 
					
						
							|  |  |  |       const message = this.diagnostics.map(d => d.message).join('\n'); | 
					
						
							|  |  |  |       console.warn(message); | 
					
						
							|  |  |  |       if (this.diagnostics.some(d => d.type === 'error')) { | 
					
						
							|  |  |  |         throw new Error(message); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-11-26 14:44:45 -08:00
										 |  |  |     return output.join(''); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   emitDeclaration(symbol: ts.Symbol, decl: ts.Node, output: string[]) { | 
					
						
							|  |  |  |     // The declaration node may not be a complete statement, e.g. for var/const
 | 
					
						
							|  |  |  |     // symbols. We need to find the complete export statement by traversing
 | 
					
						
							|  |  |  |     // upwards.
 | 
					
						
							|  |  |  |     while (!hasModifier(decl, ts.SyntaxKind.ExportKeyword) && decl.parent) { | 
					
						
							|  |  |  |       decl = decl.parent; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (hasModifier(decl, ts.SyntaxKind.ExportKeyword)) { | 
					
						
							|  |  |  |       // Make an empty line between two exports
 | 
					
						
							|  |  |  |       if (output.length) { | 
					
						
							|  |  |  |         output.push('\n'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       const jsdocComment = this.processJsDocTags(decl, this.options.exportTags); | 
					
						
							|  |  |  |       if (jsdocComment) { | 
					
						
							|  |  |  |         output.push(jsdocComment + '\n'); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       output.push(stripEmptyLines(this.emitNode(decl)) + '\n'); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       // This may happen for symbols re-exported from external modules.
 | 
					
						
							|  |  |  |       this.diagnostics.push({ | 
					
						
							|  |  |  |         type: 'warn', | 
					
						
							|  |  |  |         message: createErrorMessage(decl, `No export declaration found for symbol "${symbol.name}"`) | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-19 13:40:21 +02:00
										 |  |  |   private isExportPatternStripped(symbolName: string): boolean { | 
					
						
							|  |  |  |     return [].concat(this.options.stripExportPattern).some(p => !!(p && symbolName.match(p))); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   private getResolvedSymbols(sourceFile: ts.SourceFile): ts.Symbol[] { | 
					
						
							|  |  |  |     const ms = (<any>sourceFile).symbol; | 
					
						
							|  |  |  |     const rawSymbols = ms ? (this.typeChecker.getExportsOfModule(ms) || []) : []; | 
					
						
							|  |  |  |     return rawSymbols.map(s => { | 
					
						
							|  |  |  |       if (s.flags & ts.SymbolFlags.Alias) { | 
					
						
							|  |  |  |         const resolvedSymbol = this.typeChecker.getAliasedSymbol(s); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         // This will happen, e.g. for symbols re-exported from external modules.
 | 
					
						
							|  |  |  |         if (!resolvedSymbol.valueDeclaration && !resolvedSymbol.declarations) { | 
					
						
							|  |  |  |           return s; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         if (resolvedSymbol.name !== s.name) { | 
					
						
							| 
									
										
										
										
											2018-09-19 13:40:21 +02:00
										 |  |  |           if (this.isExportPatternStripped(s.name)) { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |             return s; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |           throw new Error( | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |               `Symbol "${resolvedSymbol.name}" was aliased as "${s.name}". ` + | 
					
						
							|  |  |  |               `Aliases are not supported."`); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return resolvedSymbol; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         return s; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   emitNode(node: ts.Node) { | 
					
						
							|  |  |  |     if (hasModifier(node, ts.SyntaxKind.PrivateKeyword)) { | 
					
						
							|  |  |  |       return ''; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     const firstQualifier: ts.Identifier|null = getFirstQualifier(node); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							|  |  |  |     if (firstQualifier) { | 
					
						
							|  |  |  |       let isAllowed = false; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // Try to resolve the qualifier.
 | 
					
						
							|  |  |  |       const resolvedSymbol = this.typeChecker.getSymbolAtLocation(firstQualifier); | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |       if (resolvedSymbol && resolvedSymbol.declarations && resolvedSymbol.declarations.length > 0) { | 
					
						
							|  |  |  |         // If the qualifier can be resolved, and it's not a namespaced import, then it should be
 | 
					
						
							|  |  |  |         // allowed.
 | 
					
						
							|  |  |  |         isAllowed = | 
					
						
							|  |  |  |             resolvedSymbol.declarations.every(decl => decl.kind !== ts.SyntaxKind.NamespaceImport); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       // If it is not allowed otherwise, it's allowed if it's on the list of allowed identifiers.
 | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |       isAllowed = isAllowed || | 
					
						
							|  |  |  |           !(!this.options.allowModuleIdentifiers || | 
					
						
							|  |  |  |             this.options.allowModuleIdentifiers.indexOf(firstQualifier.text) < 0); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |       if (!isAllowed) { | 
					
						
							|  |  |  |         this.diagnostics.push({ | 
					
						
							|  |  |  |           type: 'error', | 
					
						
							|  |  |  |           message: createErrorMessage( | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |               firstQualifier, | 
					
						
							|  |  |  |               `Module identifier "${firstQualifier.text}" is not allowed. Remove it ` + | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  |                   `from source or allow it via --allowModuleIdentifiers.`) | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         }); | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-09 15:45:11 +01:00
										 |  |  |     let children: ts.Node[] = []; | 
					
						
							|  |  |  |     if (ts.isFunctionDeclaration(node)) { | 
					
						
							|  |  |  |       // Used ts.isFunctionDeclaration instead of node.kind because this is a type guard
 | 
					
						
							|  |  |  |       const symbol = this.typeChecker.getSymbolAtLocation(node.name); | 
					
						
							|  |  |  |       symbol.declarations.forEach(x => children = children.concat(x.getChildren())); | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       children = node.getChildren(); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |     const sourceText = node.getSourceFile().text; | 
					
						
							|  |  |  |     if (children.length) { | 
					
						
							|  |  |  |       // Sort declarations under a class or an interface
 | 
					
						
							|  |  |  |       if (node.kind === ts.SyntaxKind.SyntaxList) { | 
					
						
							|  |  |  |         switch (node.parent && node.parent.kind) { | 
					
						
							|  |  |  |           case ts.SyntaxKind.ClassDeclaration: | 
					
						
							|  |  |  |           case ts.SyntaxKind.InterfaceDeclaration: { | 
					
						
							|  |  |  |             // There can be multiple SyntaxLists under a class or an interface,
 | 
					
						
							|  |  |  |             // since SyntaxList is just an arbitrary data structure generated
 | 
					
						
							|  |  |  |             // by Node#getChildren(). We need to check that we are sorting the
 | 
					
						
							|  |  |  |             // right list.
 | 
					
						
							|  |  |  |             if (children.every(node => node.kind in memberDeclarationOrder)) { | 
					
						
							|  |  |  |               children = children.slice(); | 
					
						
							|  |  |  |               children.sort((a: ts.NamedDeclaration, b: ts.NamedDeclaration) => { | 
					
						
							|  |  |  |                 // Static after normal
 | 
					
						
							|  |  |  |                 return compareFunction( | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |                            hasModifier(a, ts.SyntaxKind.StaticKeyword), | 
					
						
							|  |  |  |                            hasModifier(b, ts.SyntaxKind.StaticKeyword)) || | 
					
						
							|  |  |  |                     // Our predefined order
 | 
					
						
							|  |  |  |                     compareFunction( | 
					
						
							|  |  |  |                            memberDeclarationOrder[a.kind], memberDeclarationOrder[b.kind]) || | 
					
						
							|  |  |  |                     // Alphebetical order
 | 
					
						
							|  |  |  |                     // We need safe dereferencing due to edge cases, e.g. having two call signatures
 | 
					
						
							|  |  |  |                     compareFunction((a.name || a).getText(), (b.name || b).getText()); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |               }); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             break; | 
					
						
							|  |  |  |           } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |       let output: string = children.filter(x => x.kind !== ts.SyntaxKind.JSDocComment) | 
					
						
							|  |  |  |                                .map(n => this.emitNode(n)) | 
					
						
							|  |  |  |                                .join(''); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |       // Print stability annotation for fields and parmeters
 | 
					
						
							| 
									
										
										
										
											2018-03-09 15:45:11 +01:00
										 |  |  |       if (ts.isParameter(node) || node.kind in memberDeclarationOrder) { | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |         const tagOptions = ts.isParameter(node) ? this.options.paramTags : this.options.memberTags; | 
					
						
							|  |  |  |         const jsdocComment = this.processJsDocTags(node, tagOptions); | 
					
						
							|  |  |  |         if (jsdocComment) { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |           // Add the annotation after the leading whitespace
 | 
					
						
							| 
									
										
										
										
											2019-04-25 10:37:41 +02:00
										 |  |  |           output = output.replace(/^(\r?\n\s*)/, `$1${jsdocComment} `); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return output; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       const ranges = ts.getLeadingCommentRanges(sourceText, node.pos); | 
					
						
							|  |  |  |       let tail = node.pos; | 
					
						
							|  |  |  |       for (const range of ranges || []) { | 
					
						
							|  |  |  |         if (range.end > tail) { | 
					
						
							|  |  |  |           tail = range.end; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       return sourceText.substring(tail, node.end); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  |   private processJsDocTags(node: ts.Node, tagOptions: JsDocTagOptions) { | 
					
						
							|  |  |  |     const jsDocTags = getJsDocTags(node); | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |     const requireAtLeastOne = tagOptions.requireAtLeastOne; | 
					
						
							|  |  |  |     const isMissingAnyRequiredTag = requireAtLeastOne != null && requireAtLeastOne.length > 0 && | 
					
						
							|  |  |  |         jsDocTags.every(tag => requireAtLeastOne.indexOf(tag) === -1); | 
					
						
							|  |  |  |     if (isMissingAnyRequiredTag) { | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |       this.diagnostics.push({ | 
					
						
							|  |  |  |         type: 'error', | 
					
						
							|  |  |  |         message: createErrorMessage( | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |             node, 'Required jsdoc tags - One of the tags: ' + | 
					
						
							|  |  |  |                 requireAtLeastOne.map(tag => `"@${tag}"`).join(', ') + | 
					
						
							|  |  |  |                 ` - must exist on ${getName(node)}.`) | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const bannedTagsFound = | 
					
						
							|  |  |  |         tagOptions.banned.filter(bannedTag => jsDocTags.some(tag => tag === bannedTag)); | 
					
						
							|  |  |  |     if (bannedTagsFound.length) { | 
					
						
							|  |  |  |       this.diagnostics.push({ | 
					
						
							|  |  |  |         type: 'error', | 
					
						
							|  |  |  |         message: createErrorMessage( | 
					
						
							|  |  |  |             node, 'Banned jsdoc tags - ' + bannedTagsFound.map(tag => `"@${tag}"`).join(', ') + | 
					
						
							|  |  |  |                 ` - were found on ${getName(node)}.`) | 
					
						
							|  |  |  |       }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const tagsToCopy = | 
					
						
							|  |  |  |         jsDocTags.filter(tag => tagOptions.toCopy.some(tagToCopy => tag === tagToCopy)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if (tagsToCopy.length === 1) { | 
					
						
							|  |  |  |       return `/** @${tagsToCopy[0]} */`; | 
					
						
							|  |  |  |     } else if (tagsToCopy.length > 1) { | 
					
						
							|  |  |  |       return '/**\n' + tagsToCopy.map(tag => ` * @${tag}`).join('\n') + ' */\n'; | 
					
						
							|  |  |  |     } else { | 
					
						
							|  |  |  |       return ''; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const tagRegex = /@(\w+)/g; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getJsDocTags(node: ts.Node): string[] { | 
					
						
							|  |  |  |   const sourceText = node.getSourceFile().text; | 
					
						
							|  |  |  |   const trivia = sourceText.substr(node.pos, node.getLeadingTriviaWidth()); | 
					
						
							|  |  |  |   // We use a hash so that we don't collect duplicate jsdoc tags
 | 
					
						
							|  |  |  |   // (e.g. if a property has a getter and setter with the same tag).
 | 
					
						
							|  |  |  |   const jsdocTags: {[key: string]: boolean} = {}; | 
					
						
							|  |  |  |   let match: RegExpExecArray; | 
					
						
							|  |  |  |   while (match = tagRegex.exec(trivia)) { | 
					
						
							|  |  |  |     jsdocTags[match[1]] = true; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  |   return Object.keys(jsdocTags); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function symbolCompareFunction(a: ts.Symbol, b: ts.Symbol) { | 
					
						
							|  |  |  |   return a.name.localeCompare(b.name); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function compareFunction<T>(a: T, b: T) { | 
					
						
							|  |  |  |   return a === b ? 0 : a > b ? 1 : -1; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  | const memberDeclarationOrder: {[key: number]: number} = { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   [ts.SyntaxKind.PropertySignature]: 0, | 
					
						
							|  |  |  |   [ts.SyntaxKind.PropertyDeclaration]: 0, | 
					
						
							|  |  |  |   [ts.SyntaxKind.GetAccessor]: 0, | 
					
						
							|  |  |  |   [ts.SyntaxKind.SetAccessor]: 0, | 
					
						
							|  |  |  |   [ts.SyntaxKind.CallSignature]: 1, | 
					
						
							|  |  |  |   [ts.SyntaxKind.Constructor]: 2, | 
					
						
							|  |  |  |   [ts.SyntaxKind.ConstructSignature]: 2, | 
					
						
							|  |  |  |   [ts.SyntaxKind.IndexSignature]: 3, | 
					
						
							|  |  |  |   [ts.SyntaxKind.MethodSignature]: 4, | 
					
						
							|  |  |  |   [ts.SyntaxKind.MethodDeclaration]: 4 | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function stripEmptyLines(text: string): string { | 
					
						
							| 
									
										
										
										
											2018-11-03 10:56:52 +01:00
										 |  |  |   return text.split(/\r?\n/).filter(x => !!x.length).join('\n'); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Returns the first qualifier if the input node is a dotted expression. | 
					
						
							|  |  |  |  */ | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  | function getFirstQualifier(node: ts.Node): ts.Identifier|null { | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |   switch (node.kind) { | 
					
						
							|  |  |  |     case ts.SyntaxKind.PropertyAccessExpression: { | 
					
						
							|  |  |  |       // For expression position
 | 
					
						
							|  |  |  |       let lhs = node; | 
					
						
							|  |  |  |       do { | 
					
						
							|  |  |  |         lhs = (<ts.PropertyAccessExpression>lhs).expression; | 
					
						
							|  |  |  |       } while (lhs && lhs.kind !== ts.SyntaxKind.Identifier); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return <ts.Identifier>lhs; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     case ts.SyntaxKind.TypeReference: { | 
					
						
							|  |  |  |       // For type position
 | 
					
						
							|  |  |  |       let lhs: ts.Node = (<ts.TypeReferenceNode>node).typeName; | 
					
						
							|  |  |  |       do { | 
					
						
							|  |  |  |         lhs = (<ts.QualifiedName>lhs).left; | 
					
						
							|  |  |  |       } while (lhs && lhs.kind !== ts.SyntaxKind.Identifier); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |       return <ts.Identifier>lhs; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     default: | 
					
						
							|  |  |  |       return null; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function createErrorMessage(node: ts.Node, message: string): string { | 
					
						
							|  |  |  |   const sourceFile = node.getSourceFile(); | 
					
						
							|  |  |  |   let position; | 
					
						
							|  |  |  |   if (sourceFile) { | 
					
						
							| 
									
										
										
										
											2018-03-01 10:41:35 -08:00
										 |  |  |     const {line, character} = sourceFile.getLineAndCharacterOfPosition(node.getStart()); | 
					
						
							| 
									
										
										
										
											2018-03-02 14:19:01 -08:00
										 |  |  |     position = `${sourceFile.fileName}(${line + 1},${character + 1})`; | 
					
						
							|  |  |  |   } else { | 
					
						
							|  |  |  |     position = '<unknown>'; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   return `${position}: error: ${message}`; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function hasModifier(node: ts.Node, modifierKind: ts.SyntaxKind): boolean { | 
					
						
							|  |  |  |   return !!node.modifiers && node.modifiers.some(x => x.kind === modifierKind); | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | 
 | 
					
						
							|  |  |  | function applyDefaultTagOptions(tagOptions: JsDocTagOptions | undefined): JsDocTagOptions { | 
					
						
							| 
									
										
										
										
											2019-04-10 13:45:26 -07:00
										 |  |  |   return {requireAtLeastOne: [], banned: [], toCopy: [], ...tagOptions}; | 
					
						
							| 
									
										
										
										
											2018-10-19 10:44:33 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function getName(node: any) { | 
					
						
							|  |  |  |   return '`' + (node.name && node.name.text ? node.name.text : node.getText()) + '`'; | 
					
						
							| 
									
										
										
										
											2019-03-29 11:20:17 -07:00
										 |  |  | } |