| 
									
										
										
										
											2018-02-02 15:08:25 -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
 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import * as fs from 'fs'; | 
					
						
							|  |  |  | import * as ts from 'typescript'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export interface Symbol { name: string; } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export class SymbolExtractor { | 
					
						
							|  |  |  |   public actual: Symbol[]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static symbolSort(a: Symbol, b: Symbol): number { | 
					
						
							|  |  |  |     return a.name == b.name ? 0 : a.name < b.name ? -1 : 1; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static parse(path: string, contents: string): Symbol[] { | 
					
						
							|  |  |  |     const symbols: Symbol[] = []; | 
					
						
							|  |  |  |     const source: ts.SourceFile = ts.createSourceFile(path, contents, ts.ScriptTarget.Latest, true); | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |     let fnRecurseDepth = 0; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |     function visitor(child: ts.Node) { | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |       // Left for easier debugging.
 | 
					
						
							|  |  |  |       // console.log('>>>', ts.SyntaxKind[child.kind]);
 | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |       switch (child.kind) { | 
					
						
							|  |  |  |         case ts.SyntaxKind.FunctionExpression: | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           fnRecurseDepth++; | 
					
						
							|  |  |  |           if (fnRecurseDepth <= 1) { | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |             ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           fnRecurseDepth--; | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.SourceFile: | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableStatement: | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableDeclarationList: | 
					
						
							|  |  |  |         case ts.SyntaxKind.ExpressionStatement: | 
					
						
							|  |  |  |         case ts.SyntaxKind.CallExpression: | 
					
						
							|  |  |  |         case ts.SyntaxKind.ParenthesizedExpression: | 
					
						
							|  |  |  |         case ts.SyntaxKind.Block: | 
					
						
							|  |  |  |         case ts.SyntaxKind.PrefixUnaryExpression: | 
					
						
							|  |  |  |           ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.VariableDeclaration: | 
					
						
							|  |  |  |           const varDecl = child as ts.VariableDeclaration; | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           if (varDecl.initializer && fnRecurseDepth !== 0) { | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |             symbols.push({name: varDecl.name.getText()}); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  |           if (fnRecurseDepth == 0 && | 
					
						
							|  |  |  |               isRollupExportSymbol(child.parent as ts.VariableDeclarationList)) { | 
					
						
							|  |  |  |             ts.forEachChild(child, visitor); | 
					
						
							|  |  |  |           } | 
					
						
							| 
									
										
										
										
											2018-02-02 15:08:25 -08:00
										 |  |  |           break; | 
					
						
							|  |  |  |         case ts.SyntaxKind.FunctionDeclaration: | 
					
						
							|  |  |  |           const funcDecl = child as ts.FunctionDeclaration; | 
					
						
							|  |  |  |           funcDecl.name && symbols.push({name: funcDecl.name.getText()}); | 
					
						
							|  |  |  |           break; | 
					
						
							|  |  |  |         default: | 
					
						
							|  |  |  |           // Left for easier debugging.
 | 
					
						
							|  |  |  |           // console.log('###', ts.SyntaxKind[child.kind], child.getText());
 | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       if (symbols.length && symbols[symbols.length - 1].name == 'type') { | 
					
						
							|  |  |  |         debugger; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     visitor(source); | 
					
						
							|  |  |  |     symbols.sort(SymbolExtractor.symbolSort); | 
					
						
							|  |  |  |     return symbols; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   static diff(actual: Symbol[], expected: string|((Symbol | string)[])): {[name: string]: string} { | 
					
						
							|  |  |  |     if (typeof expected == 'string') { | 
					
						
							|  |  |  |       expected = JSON.parse(expected); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     const diff: {[name: string]: ('missing' | 'extra')} = {}; | 
					
						
							|  |  |  |     (expected as(Symbol | string)[]).forEach((nameOrSymbol) => { | 
					
						
							|  |  |  |       diff[typeof nameOrSymbol == 'string' ? nameOrSymbol : nameOrSymbol.name] = 'missing'; | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     actual.forEach((s) => { | 
					
						
							|  |  |  |       if (diff[s.name] === 'missing') { | 
					
						
							|  |  |  |         delete diff[s.name]; | 
					
						
							|  |  |  |       } else { | 
					
						
							|  |  |  |         diff[s.name] = 'extra'; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  |     return diff; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   constructor(private path: string, private contents: string) { | 
					
						
							|  |  |  |     this.actual = SymbolExtractor.parse(path, contents); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   expect(expectedSymbols: (string|Symbol)[]) { | 
					
						
							|  |  |  |     expect(SymbolExtractor.diff(this.actual, expectedSymbols)).toEqual({}); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   compareAndPrintError(goldenFilePath: string, expected: string|((Symbol | string)[])): boolean { | 
					
						
							|  |  |  |     let passed = true; | 
					
						
							|  |  |  |     const diff = SymbolExtractor.diff(this.actual, expected); | 
					
						
							|  |  |  |     Object.keys(diff).forEach((key) => { | 
					
						
							|  |  |  |       if (passed) { | 
					
						
							|  |  |  |         console.error(`Expected symbols in '${this.path}' did not match gold file.`); | 
					
						
							|  |  |  |         passed = false; | 
					
						
							|  |  |  |       } | 
					
						
							|  |  |  |       console.error(`   Symbol: ${key} => ${diff[key]}`); | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return passed; | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function toSymbol(v: string | Symbol): Symbol { | 
					
						
							|  |  |  |   return typeof v == 'string' ? {'name': v} : v as Symbol; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | function toName(symbol: Symbol): string { | 
					
						
							|  |  |  |   return symbol.name; | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-03-14 21:32:09 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Detects if VariableDeclarationList is format `var x = function(){}()`; | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Rollup produces this format when it wants to export symbols from a bundle. | 
					
						
							|  |  |  |  * @param child | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | function isRollupExportSymbol(child: ts.VariableDeclarationList): boolean { | 
					
						
							|  |  |  |   if (child.declarations.length !== 1) return false; | 
					
						
							|  |  |  |   const decl: ts.VariableDeclaration = child.declarations[0]; | 
					
						
							|  |  |  |   return !!(decl.initializer && decl.initializer.kind == ts.SyntaxKind.CallExpression); | 
					
						
							|  |  |  | } |