This allows a build using summaries to not need .metadata.json files at all any more. Part of #12787
		
			
				
	
	
		
			373 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			373 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * @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 {StaticSymbol, StaticSymbolCache, StaticSymbolResolver, StaticSymbolResolverHost, Summary, SummaryResolver} from '@angular/compiler';
 | |
| import {MetadataCollector} from '@angular/tsc-wrapped';
 | |
| import * as ts from 'typescript';
 | |
| 
 | |
| 
 | |
| 
 | |
| // This matches .ts files but not .d.ts files.
 | |
| const TS_EXT = /(^.|(?!\.d)..)\.ts$/;
 | |
| 
 | |
| describe('StaticSymbolResolver', () => {
 | |
|   const noContext = new StaticSymbol('', '');
 | |
|   let host: StaticSymbolResolverHost;
 | |
|   let symbolResolver: StaticSymbolResolver;
 | |
|   let symbolCache: StaticSymbolCache;
 | |
| 
 | |
|   beforeEach(() => { symbolCache = new StaticSymbolCache(); });
 | |
| 
 | |
|   function init(
 | |
|       testData: {[key: string]: any} = DEFAULT_TEST_DATA, summaries: Summary<StaticSymbol>[] = []) {
 | |
|     host = new MockStaticSymbolResolverHost(testData);
 | |
|     symbolResolver =
 | |
|         new StaticSymbolResolver(host, symbolCache, new MockSummaryResolver(summaries));
 | |
|   }
 | |
| 
 | |
|   beforeEach(() => init());
 | |
| 
 | |
|   it('should throw an exception for unsupported metadata versions', () => {
 | |
|     expect(
 | |
|         () => symbolResolver.resolveSymbol(
 | |
|             symbolResolver.getSymbolByModule('src/version-error', 'e')))
 | |
|         .toThrow(new Error(
 | |
|             'Metadata version mismatch for module /tmp/src/version-error.d.ts, found version 100, expected 3'));
 | |
|   });
 | |
| 
 | |
|   it('should throw an exception for version 2 metadata', () => {
 | |
|     expect(
 | |
|         () => symbolResolver.resolveSymbol(
 | |
|             symbolResolver.getSymbolByModule('src/version-2-error', 'e')))
 | |
|         .toThrowError(
 | |
|             'Unsupported metadata version 2 for module /tmp/src/version-2-error.d.ts. This module should be compiled with a newer version of ngc');
 | |
|   });
 | |
| 
 | |
|   it('should be produce the same symbol if asked twice', () => {
 | |
|     const foo1 = symbolResolver.getStaticSymbol('main.ts', 'foo');
 | |
|     const foo2 = symbolResolver.getStaticSymbol('main.ts', 'foo');
 | |
|     expect(foo1).toBe(foo2);
 | |
|   });
 | |
| 
 | |
|   it('should be able to produce a symbol for a module with no file', () => {
 | |
|     expect(symbolResolver.getStaticSymbol('angularjs', 'SomeAngularSymbol')).toBeDefined();
 | |
|   });
 | |
| 
 | |
|   it('should be able to split the metadata per symbol', () => {
 | |
|     init({
 | |
|       '/tmp/src/test.ts': `
 | |
|         export var a = 1;
 | |
|         export var b = 2;
 | |
|       `
 | |
|     });
 | |
|     expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'a'))
 | |
|                .metadata)
 | |
|         .toBe(1);
 | |
|     expect(symbolResolver.resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'b'))
 | |
|                .metadata)
 | |
|         .toBe(2);
 | |
|   });
 | |
| 
 | |
|   it('should be able to resolve static symbols with members', () => {
 | |
|     init({
 | |
|       '/tmp/src/test.ts': `
 | |
|         export {exportedObj} from './export';
 | |
| 
 | |
|         export var obj = {a: 1};
 | |
|         export class SomeClass {
 | |
|           static someField = 2;
 | |
|         }
 | |
|       `,
 | |
|       '/tmp/src/export.ts': `
 | |
|         export var exportedObj = {};
 | |
|       `,
 | |
|     });
 | |
|     expect(symbolResolver
 | |
|                .resolveSymbol(symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'obj', ['a']))
 | |
|                .metadata)
 | |
|         .toBe(1);
 | |
|     expect(symbolResolver
 | |
|                .resolveSymbol(
 | |
|                    symbolResolver.getStaticSymbol('/tmp/src/test.ts', 'SomeClass', ['someField']))
 | |
|                .metadata)
 | |
|         .toBe(2);
 | |
|     expect(symbolResolver
 | |
|                .resolveSymbol(symbolResolver.getStaticSymbol(
 | |
|                    '/tmp/src/test.ts', 'exportedObj', ['someMember']))
 | |
|                .metadata)
 | |
|         .toBe(symbolResolver.getStaticSymbol('/tmp/src/export.ts', 'exportedObj', ['someMember']));
 | |
|   });
 | |
| 
 | |
|   it('should use summaries in resolveSymbol and prefer them over regular metadata', () => {
 | |
|     const someSymbol = symbolCache.get('/test.ts', 'a');
 | |
|     init({'/test.ts': 'export var a = 2'}, [{symbol: someSymbol, metadata: 1}]);
 | |
|     expect(symbolResolver.resolveSymbol(someSymbol).metadata).toBe(1);
 | |
|   });
 | |
| 
 | |
|   it('should be able to get all exported symbols of a file', () => {
 | |
|     expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/src/origin1.d.ts')).toEqual([
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'One'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Two'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/src/origin1.d.ts', 'Three'),
 | |
|     ]);
 | |
|   });
 | |
| 
 | |
|   it('should be able to get all reexported symbols of a file', () => {
 | |
|     expect(symbolResolver.getSymbolsOf('/tmp/src/reexport/reexport.d.ts')).toEqual([
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'One'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Two'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Four'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Five'),
 | |
|       symbolResolver.getStaticSymbol('/tmp/src/reexport/reexport.d.ts', 'Thirty')
 | |
|     ]);
 | |
|   });
 | |
| 
 | |
|   it('should merge the exported symbols of a file with the exported symbols of its summary', () => {
 | |
|     const someSymbol = symbolCache.get('/test.ts', 'a');
 | |
|     init(
 | |
|         {'/test.ts': 'export var b = 2'},
 | |
|         [{symbol: symbolCache.get('/test.ts', 'a'), metadata: 1}]);
 | |
|     expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([
 | |
|       symbolCache.get('/test.ts', 'a'), symbolCache.get('/test.ts', 'b')
 | |
|     ]);
 | |
|   });
 | |
| 
 | |
|   it('should replace references by StaticSymbols', () => {
 | |
|     init({
 | |
|       '/test.ts': `
 | |
|         import {b, y} from './test2';
 | |
|         export var a = b;
 | |
|         export var x = [y];
 | |
| 
 | |
|         export function simpleFn(fnArg) {
 | |
|           return [a, y, fnArg];
 | |
|         }
 | |
|       `,
 | |
|       '/test2.ts': `
 | |
|         export var b;
 | |
|         export var y;
 | |
|       `
 | |
|     });
 | |
|     expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'a')).metadata)
 | |
|         .toEqual(symbolCache.get('/test2.ts', 'b'));
 | |
|     expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'x')).metadata).toEqual([
 | |
|       symbolCache.get('/test2.ts', 'y')
 | |
|     ]);
 | |
|     expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'simpleFn')).metadata).toEqual({
 | |
|       __symbolic: 'function',
 | |
|       parameters: ['fnArg'],
 | |
|       value: [
 | |
|         symbolCache.get('/test.ts', 'a'), symbolCache.get('/test2.ts', 'y'),
 | |
|         Object({__symbolic: 'reference', name: 'fnArg'})
 | |
|       ]
 | |
|     });
 | |
|   });
 | |
| 
 | |
|   it('should ignore module references without a name', () => {
 | |
|     init({
 | |
|       '/test.ts': `
 | |
|         import Default from './test2';
 | |
|         export {Default};
 | |
|       `
 | |
|     });
 | |
| 
 | |
|     expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'Default')).metadata)
 | |
|         .toBeFalsy();
 | |
|   });
 | |
| 
 | |
|   it('should be able to trace a named export', () => {
 | |
|     const symbol = symbolResolver
 | |
|                        .resolveSymbol(symbolResolver.getSymbolByModule(
 | |
|                            './reexport/reexport', 'One', '/tmp/src/main.ts'))
 | |
|                        .metadata;
 | |
|     expect(symbol.name).toEqual('One');
 | |
|     expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
 | |
|   });
 | |
| 
 | |
|   it('should be able to trace a renamed export', () => {
 | |
|     const symbol = symbolResolver
 | |
|                        .resolveSymbol(symbolResolver.getSymbolByModule(
 | |
|                            './reexport/reexport', 'Four', '/tmp/src/main.ts'))
 | |
|                        .metadata;
 | |
|     expect(symbol.name).toEqual('Three');
 | |
|     expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
 | |
|   });
 | |
| 
 | |
|   it('should be able to trace an export * export', () => {
 | |
|     const symbol = symbolResolver
 | |
|                        .resolveSymbol(symbolResolver.getSymbolByModule(
 | |
|                            './reexport/reexport', 'Five', '/tmp/src/main.ts'))
 | |
|                        .metadata;
 | |
|     expect(symbol.name).toEqual('Five');
 | |
|     expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin5.d.ts');
 | |
|   });
 | |
| 
 | |
|   it('should be able to trace a multi-level re-export', () => {
 | |
|     const symbol1 = symbolResolver
 | |
|                         .resolveSymbol(symbolResolver.getSymbolByModule(
 | |
|                             './reexport/reexport', 'Thirty', '/tmp/src/main.ts'))
 | |
|                         .metadata;
 | |
|     expect(symbol1.name).toEqual('Thirty');
 | |
|     expect(symbol1.filePath).toEqual('/tmp/src/reexport/src/reexport2.d.ts');
 | |
|     const symbol2 = symbolResolver.resolveSymbol(symbol1).metadata;
 | |
|     expect(symbol2.name).toEqual('Thirty');
 | |
|     expect(symbol2.filePath).toEqual('/tmp/src/reexport/src/origin30.d.ts');
 | |
|   });
 | |
| 
 | |
|   it('should cache tracing a named export', () => {
 | |
|     const moduleNameToFileNameSpy = spyOn(host, 'moduleNameToFileName').and.callThrough();
 | |
|     const getMetadataForSpy = spyOn(host, 'getMetadataFor').and.callThrough();
 | |
|     symbolResolver.resolveSymbol(
 | |
|         symbolResolver.getSymbolByModule('./reexport/reexport', 'One', '/tmp/src/main.ts'));
 | |
|     moduleNameToFileNameSpy.calls.reset();
 | |
|     getMetadataForSpy.calls.reset();
 | |
| 
 | |
|     const symbol = symbolResolver
 | |
|                        .resolveSymbol(symbolResolver.getSymbolByModule(
 | |
|                            './reexport/reexport', 'One', '/tmp/src/main.ts'))
 | |
|                        .metadata;
 | |
|     expect(moduleNameToFileNameSpy.calls.count()).toBe(1);
 | |
|     expect(getMetadataForSpy.calls.count()).toBe(0);
 | |
|     expect(symbol.name).toEqual('One');
 | |
|     expect(symbol.filePath).toEqual('/tmp/src/reexport/src/origin1.d.ts');
 | |
|   });
 | |
| 
 | |
| });
 | |
| 
 | |
| export class MockSummaryResolver implements SummaryResolver<StaticSymbol> {
 | |
|   constructor(private summaries: Summary<StaticSymbol>[] = []) {}
 | |
| 
 | |
|   resolveSummary(reference: StaticSymbol): Summary<StaticSymbol> {
 | |
|     return this.summaries.find(summary => summary.symbol === reference);
 | |
|   };
 | |
|   getSymbolsOf(filePath: string): StaticSymbol[] {
 | |
|     return this.summaries.filter(summary => summary.symbol.filePath === filePath)
 | |
|         .map(summary => summary.symbol);
 | |
|   }
 | |
| }
 | |
| 
 | |
| export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
 | |
|   private collector = new MetadataCollector();
 | |
| 
 | |
|   constructor(private data: {[key: string]: any}) {}
 | |
| 
 | |
|   // In tests, assume that symbols are not re-exported
 | |
|   moduleNameToFileName(modulePath: string, containingFile?: string): string {
 | |
|     function splitPath(path: string): string[] { return path.split(/\/|\\/g); }
 | |
| 
 | |
|     function resolvePath(pathParts: string[]): string {
 | |
|       const result: string[] = [];
 | |
|       pathParts.forEach((part, index) => {
 | |
|         switch (part) {
 | |
|           case '':
 | |
|           case '.':
 | |
|             if (index > 0) return;
 | |
|             break;
 | |
|           case '..':
 | |
|             if (index > 0 && result.length != 0) result.pop();
 | |
|             return;
 | |
|         }
 | |
|         result.push(part);
 | |
|       });
 | |
|       return result.join('/');
 | |
|     }
 | |
| 
 | |
|     function pathTo(from: string, to: string): string {
 | |
|       let result = to;
 | |
|       if (to.startsWith('.')) {
 | |
|         const fromParts = splitPath(from);
 | |
|         fromParts.pop();  // remove the file name.
 | |
|         const toParts = splitPath(to);
 | |
|         result = resolvePath(fromParts.concat(toParts));
 | |
|       }
 | |
|       return result;
 | |
|     }
 | |
| 
 | |
|     if (modulePath.indexOf('.') === 0) {
 | |
|       const baseName = pathTo(containingFile, modulePath);
 | |
|       const tsName = baseName + '.ts';
 | |
|       if (this._getMetadataFor(tsName)) {
 | |
|         return tsName;
 | |
|       }
 | |
|       return baseName + '.d.ts';
 | |
|     }
 | |
|     return '/tmp/' + modulePath + '.d.ts';
 | |
|   }
 | |
| 
 | |
|   getMetadataFor(moduleId: string): any { return this._getMetadataFor(moduleId); }
 | |
| 
 | |
|   private _getMetadataFor(filePath: string): any {
 | |
|     if (this.data[filePath] && filePath.match(TS_EXT)) {
 | |
|       const text = this.data[filePath];
 | |
|       if (typeof text === 'string') {
 | |
|         const sf = ts.createSourceFile(
 | |
|             filePath, this.data[filePath], ts.ScriptTarget.ES5, /* setParentNodes */ true);
 | |
|         const diagnostics: ts.Diagnostic[] = (<any>sf).parseDiagnostics;
 | |
|         if (diagnostics && diagnostics.length) {
 | |
|           throw Error(`Error encountered during parse of file ${filePath}`);
 | |
|         }
 | |
|         return [this.collector.getMetadata(sf)];
 | |
|       }
 | |
|     }
 | |
|     const result = this.data[filePath];
 | |
|     if (result) {
 | |
|       return Array.isArray(result) ? result : [result];
 | |
|     } else {
 | |
|       return null;
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| const DEFAULT_TEST_DATA: {[key: string]: any} = {
 | |
|   '/tmp/src/version-error.d.ts': {'__symbolic': 'module', 'version': 100, metadata: {e: 's'}},
 | |
|   '/tmp/src/version-2-error.d.ts': {'__symbolic': 'module', 'version': 2, metadata: {e: 's'}},
 | |
|   '/tmp/src/reexport/reexport.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {},
 | |
|     exports: [
 | |
|       {from: './src/origin1', export: ['One', 'Two', {name: 'Three', as: 'Four'}]},
 | |
|       {from: './src/origin5'}, {from: './src/reexport2'}
 | |
|     ]
 | |
|   },
 | |
|   '/tmp/src/reexport/src/origin1.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {
 | |
|       One: {__symbolic: 'class'},
 | |
|       Two: {__symbolic: 'class'},
 | |
|       Three: {__symbolic: 'class'},
 | |
|     },
 | |
|   },
 | |
|   '/tmp/src/reexport/src/origin5.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {
 | |
|       Five: {__symbolic: 'class'},
 | |
|     },
 | |
|   },
 | |
|   '/tmp/src/reexport/src/origin30.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {
 | |
|       Thirty: {__symbolic: 'class'},
 | |
|     },
 | |
|   },
 | |
|   '/tmp/src/reexport/src/originNone.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {},
 | |
|   },
 | |
|   '/tmp/src/reexport/src/reexport2.d.ts': {
 | |
|     __symbolic: 'module',
 | |
|     version: 3,
 | |
|     metadata: {},
 | |
|     exports: [{from: './originNone'}, {from: './origin30'}]
 | |
|   }
 | |
| };
 |