518 lines
18 KiB
TypeScript
518 lines
18 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 {CollectorOptions, 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>[] = [],
|
|
summaryImportAs: {symbol: StaticSymbol, importAs: StaticSymbol}[] = []) {
|
|
host = new MockStaticSymbolResolverHost(testData);
|
|
symbolResolver = new StaticSymbolResolver(
|
|
host, symbolCache, new MockSummaryResolver(summaries, summaryImportAs));
|
|
}
|
|
|
|
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')
|
|
]);
|
|
});
|
|
|
|
describe('importAs', () => {
|
|
|
|
it('should calculate importAs relationship for non source files without summaries', () => {
|
|
init(
|
|
{
|
|
'/test.d.ts': [{
|
|
'__symbolic': 'module',
|
|
'version': 3,
|
|
'metadata': {
|
|
'a': {'__symbolic': 'reference', 'name': 'b', 'module': './test2'},
|
|
}
|
|
}],
|
|
'/test2.d.ts': [{
|
|
'__symbolic': 'module',
|
|
'version': 3,
|
|
'metadata': {
|
|
'b': {'__symbolic': 'reference', 'name': 'c', 'module': './test3'},
|
|
}
|
|
}]
|
|
},
|
|
[]);
|
|
symbolResolver.getSymbolsOf('/test.d.ts');
|
|
symbolResolver.getSymbolsOf('/test2.d.ts');
|
|
|
|
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'b')))
|
|
.toBe(symbolCache.get('/test.d.ts', 'a'));
|
|
expect(symbolResolver.getImportAs(symbolCache.get('/test3.d.ts', 'c')))
|
|
.toBe(symbolCache.get('/test.d.ts', 'a'));
|
|
});
|
|
|
|
it('should calculate importAs relationship for non source files with summaries', () => {
|
|
init(
|
|
{
|
|
'/test.ts': `
|
|
export {a} from './test2';
|
|
`
|
|
},
|
|
[], [{
|
|
symbol: symbolCache.get('/test2.d.ts', 'a'),
|
|
importAs: symbolCache.get('/test3.d.ts', 'b')
|
|
}]);
|
|
symbolResolver.getSymbolsOf('/test.ts');
|
|
|
|
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a')))
|
|
.toBe(symbolCache.get('/test3.d.ts', 'b'));
|
|
});
|
|
|
|
it('should calculate importAs for symbols with members based on importAs for symbols without',
|
|
() => {
|
|
init(
|
|
{
|
|
'/test.ts': `
|
|
export {a} from './test2';
|
|
`
|
|
},
|
|
[], [{
|
|
symbol: symbolCache.get('/test2.d.ts', 'a'),
|
|
importAs: symbolCache.get('/test3.d.ts', 'b')
|
|
}]);
|
|
symbolResolver.getSymbolsOf('/test.ts');
|
|
|
|
expect(symbolResolver.getImportAs(symbolCache.get('/test2.d.ts', 'a', ['someMember'])))
|
|
.toBe(symbolCache.get('/test3.d.ts', 'b', ['someMember']));
|
|
});
|
|
|
|
});
|
|
|
|
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 fill references to ambient symbols with undefined', () => {
|
|
init({
|
|
'/test.ts': `
|
|
export var y = 1;
|
|
export var z = [window, z];
|
|
`
|
|
});
|
|
|
|
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', 'z')).metadata).toEqual([
|
|
undefined, symbolCache.get('/test.ts', 'z')
|
|
]);
|
|
});
|
|
|
|
it('should allow to use symbols with __', () => {
|
|
init({
|
|
'/test.ts': `
|
|
export {__a__ as __b__} from './test2';
|
|
import {__c__} from './test2';
|
|
|
|
export var __x__ = 1;
|
|
export var __y__ = __c__;
|
|
`
|
|
});
|
|
|
|
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__x__')).metadata).toBe(1);
|
|
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__y__')).metadata)
|
|
.toBe(symbolCache.get('/test2.d.ts', '__c__'));
|
|
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.ts', '__b__')).metadata)
|
|
.toBe(symbolCache.get('/test2.d.ts', '__a__'));
|
|
|
|
expect(symbolResolver.getSymbolsOf('/test.ts')).toEqual([
|
|
symbolCache.get('/test.ts', '__x__'), symbolCache.get('/test.ts', '__y__'),
|
|
symbolCache.get('/test.ts', '__b__')
|
|
]);
|
|
});
|
|
|
|
it('should only use the arity for classes from libraries without summaries', () => {
|
|
init({
|
|
'/test.d.ts': [{
|
|
'__symbolic': 'module',
|
|
'version': 3,
|
|
'metadata': {
|
|
'AParam': {__symbolic: 'class'},
|
|
'AClass': {
|
|
__symbolic: 'class',
|
|
arity: 1,
|
|
members: {
|
|
__ctor__: [{
|
|
__symbolic: 'constructor',
|
|
parameters: [symbolCache.get('/test.d.ts', 'AParam')]
|
|
}]
|
|
}
|
|
}
|
|
}
|
|
}]
|
|
});
|
|
|
|
expect(symbolResolver.resolveSymbol(symbolCache.get('/test.d.ts', 'AClass')).metadata)
|
|
.toEqual({__symbolic: 'class', arity: 1});
|
|
});
|
|
|
|
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>[] = [], private importAs: {
|
|
symbol: StaticSymbol,
|
|
importAs: 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);
|
|
}
|
|
getImportAs(symbol: StaticSymbol): StaticSymbol {
|
|
const entry = this.importAs.find(entry => entry.symbol === symbol);
|
|
return entry ? entry.importAs : undefined !;
|
|
}
|
|
|
|
isLibraryFile(filePath: string): boolean { return filePath.endsWith('.d.ts'); }
|
|
getLibraryFileName(filePath: string): string { return filePath.replace(/(\.d)?\.ts$/, '.d.ts'); }
|
|
}
|
|
|
|
export class MockStaticSymbolResolverHost implements StaticSymbolResolverHost {
|
|
private collector: MetadataCollector;
|
|
|
|
constructor(private data: {[key: string]: any}, collectorOptions?: CollectorOptions) {
|
|
this.collector = new MetadataCollector(collectorOptions);
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
if (modulePath == 'unresolved') {
|
|
return undefined !;
|
|
}
|
|
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) {
|
|
const errors = diagnostics.map(d => `(${d.start}-${d.start+d.length}): ${d.messageText}`)
|
|
.join('\n ');
|
|
throw Error(`Error encountered during parse of file ${filePath}\n${errors}`);
|
|
}
|
|
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'}]
|
|
}
|
|
};
|