refactor(ivy): implement `NgccReflectionHost.findDecoratedFiles` (#26082)

PR Close #26082
This commit is contained in:
Pete Bacon Darwin 2018-09-26 17:24:43 +01:00 committed by Miško Hevery
parent d17602f31d
commit 7f03528dbc
10 changed files with 280 additions and 6 deletions

View File

@ -12,9 +12,9 @@ import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations';
import {CompileResult, DecoratorHandler} from '../../ngtsc/transform'; import {CompileResult, DecoratorHandler} from '../../ngtsc/transform';
import {NgccReflectionHost} from './host/ngcc_host';
import {DecoratedClass} from './host/decorated_class'; import {DecoratedClass} from './host/decorated_class';
import {DecoratedFile} from './host/decorated_file'; import {DecoratedFile} from './host/decorated_file';
import {NgccReflectionHost} from './host/ngcc_host';
import {isDefined} from './utils'; import {isDefined} from './utils';
export interface AnalyzedClass<A = any, M = any> extends DecoratedClass { export interface AnalyzedClass<A = any, M = any> extends DecoratedClass {

View File

@ -7,9 +7,13 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host'; import {ClassMember, ClassMemberKind, Decorator, FunctionDefinition, Parameter} from '../../../ngtsc/host';
import {reflectObjectLiteral} from '../../../ngtsc/metadata'; import {reflectObjectLiteral} from '../../../ngtsc/metadata';
import {getNameText} from '../utils'; import {getNameText, getOriginalSymbol, isDefined} from '../utils';
import {DecoratedClass} from './decorated_class';
import {DecoratedFile} from './decorated_file';
import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './fesm2015_host'; import {Fesm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './fesm2015_host';
@ -121,6 +125,42 @@ export class Esm5ReflectionHost extends Fesm2015ReflectionHost {
return {node, body: statements || null, parameters}; return {node, body: statements || null, parameters};
} }
/**
* Find all the files accessible via an entry-point, that contain decorated classes.
* @param entryPoint The starting point file for finding files that contain decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import
* information.
*/
findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[] {
const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint);
const map = new Map<ts.SourceFile, DecoratedFile>();
const getParsedClass = (declaration: ts.VariableDeclaration) => {
const decorators = this.getDecoratorsOfDeclaration(declaration);
if (decorators) {
return new DecoratedClass(getNameText(declaration.name), declaration, decorators);
}
};
if (moduleSymbol) {
const classDeclarations = this.checker.getExportsOfModule(moduleSymbol)
.map(getOriginalSymbol(this.checker))
.map(exportSymbol => exportSymbol.valueDeclaration)
.filter(isDefined)
.filter(ts.isVariableDeclaration);
const decoratedClasses = classDeclarations.map(getParsedClass).filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new DecoratedFile(file));
}
map.get(file) !.decoratedClasses.push(clazz);
});
}
return Array.from(map.values());
}
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
/** /**

View File

@ -11,8 +11,10 @@ import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host'; import {ClassMember, ClassMemberKind, CtorParameter, Decorator, Import} from '../../../ngtsc/host';
import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata'; import {TypeScriptReflectionHost, reflectObjectLiteral} from '../../../ngtsc/metadata';
import {findAll, getNameText, isDefined} from '../utils'; import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils';
import {DecoratedClass} from './decorated_class';
import {DecoratedFile} from './decorated_file';
import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host';
export const DECORATORS = 'decorators' as ts.__String; export const DECORATORS = 'decorators' as ts.__String;
@ -294,6 +296,48 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements
return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id); return super.getImportOfIdentifier(id) || this.getImportOfNamespacedIdentifier(id);
} }
/*
* Find all the files accessible via an entry-point, that contain decorated classes.
* @param entryPoint The starting point file for finding files that contain decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import
* information.
*/
findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[] {
const moduleSymbol = this.checker.getSymbolAtLocation(entryPoint);
const map = new Map<ts.SourceFile, DecoratedFile>();
if (moduleSymbol) {
const exportedSymbols =
this.checker.getExportsOfModule(moduleSymbol).map(getOriginalSymbol(this.checker));
const exportedDeclarations =
exportedSymbols.map(exportSymbol => exportSymbol.valueDeclaration).filter(isDefined);
const decoratedClasses =
exportedDeclarations
.map(declaration => {
if (ts.isClassDeclaration(declaration) || ts.isVariableDeclaration(declaration)) {
const name = declaration.name && ts.isIdentifier(declaration.name) ?
declaration.name.text :
undefined;
const decorators = this.getDecoratorsOfDeclaration(declaration);
return decorators && isDefined(name) ?
new DecoratedClass(name, declaration, decorators) :
undefined;
}
return undefined;
})
.filter(isDefined);
decoratedClasses.forEach(clazz => {
const file = clazz.declaration.getSourceFile();
if (!map.has(file)) {
map.set(file, new DecoratedFile(file));
}
map.get(file) !.decoratedClasses.push(clazz);
});
}
return Array.from(map.values());
}
///////////// Protected Helpers ///////////// ///////////// Protected Helpers /////////////
/** /**

View File

@ -7,6 +7,7 @@
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReflectionHost} from '../../../ngtsc/host'; import {ReflectionHost} from '../../../ngtsc/host';
import {DecoratedFile} from './decorated_file';
export const PRE_NGCC_MARKER = '__PRE_NGCC__'; export const PRE_NGCC_MARKER = '__PRE_NGCC__';
export const POST_NGCC_MARKER = '__POST_NGCC__'; export const POST_NGCC_MARKER = '__POST_NGCC__';
@ -37,4 +38,12 @@ export interface NgccReflectionHost extends ReflectionHost {
* @returns An array of variable declarations that match. * @returns An array of variable declarations that match.
*/ */
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[]; getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
/**
* Find all the files accessible via an entry-point, that contain decorated classes.
* @param entryPoint The starting point file for finding files that contain decorated classes.
* @returns A collection of files objects that hold info about the decorated classes and import
* information.
*/
findDecoratedFiles(entryPoint: ts.SourceFile): DecoratedFile[];
} }

View File

@ -8,9 +8,9 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NgccReflectionHost} from '../host/ngcc_host';
import {DecoratedClass} from '../host/decorated_class'; import {DecoratedClass} from '../host/decorated_class';
import {DecoratedFile} from '../host/decorated_file'; import {DecoratedFile} from '../host/decorated_file';
import {NgccReflectionHost} from '../host/ngcc_host';
import {getOriginalSymbol, isDefined} from '../utils'; import {getOriginalSymbol, isDefined} from '../utils';
import {FileParser} from './file_parser'; import {FileParser} from './file_parser';

View File

@ -8,9 +8,9 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {NgccReflectionHost} from '../host/ngcc_host';
import {DecoratedClass} from '../host/decorated_class'; import {DecoratedClass} from '../host/decorated_class';
import {DecoratedFile} from '../host/decorated_file'; import {DecoratedFile} from '../host/decorated_file';
import {NgccReflectionHost} from '../host/ngcc_host';
import {getNameText, getOriginalSymbol, isDefined} from '../utils'; import {getNameText, getOriginalSymbol, isDefined} from '../utils';
import {FileParser} from './file_parser'; import {FileParser} from './file_parser';

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import * as ts from 'typescript'; import * as ts from 'typescript';
import {Decorator} from '../../ngtsc/host'; import {Decorator} from '../../ngtsc/host';
import {DecoratorHandler} from '../../ngtsc/transform'; import {DecoratorHandler} from '../../ngtsc/transform';
import {AnalyzedFile, Analyzer} from '../src/analyzer'; import {AnalyzedFile, Analyzer} from '../src/analyzer';
import {Fesm2015ReflectionHost} from '../src/host/fesm2015_host';
import {DecoratedClass} from '../src/host/decorated_class'; import {DecoratedClass} from '../src/host/decorated_class';
import {DecoratedFile} from '../src/host/decorated_file'; import {DecoratedFile} from '../src/host/decorated_file';
import {Fesm2015ReflectionHost} from '../src/host/fesm2015_host';
import {getDeclaration, makeProgram} from './helpers/utils'; import {getDeclaration, makeProgram} from './helpers/utils';
const TEST_PROGRAM = { const TEST_PROGRAM = {

View File

@ -50,6 +50,39 @@ const MARKER_FILE = {
` `
}; };
const DECORATED_FILES = [
{
name: '/primary.js',
contents: `
import {Directive} from '@angular/core';
class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
function x() {}
function y() {}
class B {}
B.decorators = [
{ type: Directive, args: [{ selector: '[b]' }] }
];
class C {}
export { A, x, C };
export { D } from '/secondary';
`
},
{
name: '/secondary.js',
contents: `
import {Directive} from '@angular/core';
class D {}
D.decorators = [
{ type: Directive, args: [{ selector: '[d]' }] }
];
export { D };
`
}
];
describe('Esm2015ReflectionHost', () => { describe('Esm2015ReflectionHost', () => {
describe('getGenericArityOfClass()', () => { describe('getGenericArityOfClass()', () => {
it('should properly count type parameters', () => { it('should properly count type parameters', () => {
@ -84,4 +117,28 @@ describe('Esm2015ReflectionHost', () => {
]); ]);
}); });
}); });
describe('findDecoratedFiles()', () => {
it('should return an array of objects for each file that has exported and decorated classes',
() => {
const program = makeProgram(...DECORATED_FILES);
const dtsMapper = new DtsMapper('/src', '/typings');
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsMapper);
const decoratedFiles =
host.findDecoratedFiles(program.getSourceFile(DECORATED_FILES[0].name) !);
expect(decoratedFiles.length).toEqual(2);
const primary = decoratedFiles[0];
expect(primary.decoratedClasses.length).toEqual(1);
const classA = primary.decoratedClasses.find(c => c.name === 'A') !;
expect(classA.name).toEqual('A');
expect(ts.isClassDeclaration(classA.declaration)).toBeTruthy();
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = decoratedFiles[1];
expect(secondary.decoratedClasses.length).toEqual(1);
const classD = secondary.decoratedClasses.find(c => c.name === 'D') !;
expect(classD.name).toEqual('D');
expect(ts.isClassDeclaration(classD.declaration)).toBeTruthy();
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
});
}); });

View File

@ -427,6 +427,51 @@ const FUNCTION_BODY_FILE = {
` `
}; };
const DECORATED_FILES = [
{
name: '/primary.js',
contents: `
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
return A;
}());
var B = (function() {
function B() {}
B.decorators = [
{ type: Directive, args: [{ selector: '[b]' }] }
];
return B;
}());
function x() {}
function y() {}
var C = (function() {
function C() {}
return C;
});
export { A, x, C };
export { D } from '/secondary';
`
},
{
name: '/secondary.js',
contents: `
import {Directive} from '@angular/core';
var D = (function() {
function D() {}
D.decorators = [
{ type: Directive, args: [{ selector: '[d]' }] }
];
return D;
}());
export { D };
`
}
];
describe('Esm5ReflectionHost', () => { describe('Esm5ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
@ -1206,4 +1251,25 @@ describe('Esm5ReflectionHost', () => {
expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode); expect(getClassSymbolSpy).toHaveBeenCalledWith(mockNode);
}); });
}); });
describe('fileDecoratedFiles()', () => {
it('should return an array of objects for each file that has exported and decorated classes',
() => {
const program = makeProgram(...DECORATED_FILES);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const decoratedFiles =
host.findDecoratedFiles(program.getSourceFile(DECORATED_FILES[0].name) !);
expect(decoratedFiles.length).toEqual(2);
const primary = decoratedFiles[0];
expect(primary.decoratedClasses.length).toEqual(1);
const classA = primary.decoratedClasses[0];
expect(classA.name).toEqual('A');
expect(classA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
const secondary = decoratedFiles[1];
expect(secondary.decoratedClasses.length).toEqual(1);
const classD = secondary.decoratedClasses[0];
expect(classD.name).toEqual('D');
expect(classD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
});
});
}); });

View File

@ -392,6 +392,37 @@ const MARKER_FILE = {
` `
}; };
const DECORATED_FILE = {
name: '/primary.js',
contents: `
import {Directive} from '@angular/core';
class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] }
];
class B {}
B.decorators = [
{ type: Directive, args: [{ selector: '[b]' }] }
];
function x() {}
function y() {}
class C {}
let D = class D {}
D = tslib_1.__decorate([
Directive({ selector: '[d]' }),
OtherD()
], D);
export {D};
export { A, x, C };
`
};
describe('Fesm2015ReflectionHost', () => { describe('Fesm2015ReflectionHost', () => {
describe('getDecoratorsOfDeclaration()', () => { describe('getDecoratorsOfDeclaration()', () => {
@ -1152,4 +1183,29 @@ describe('Fesm2015ReflectionHost', () => {
]); ]);
}); });
}); });
describe('findDecoratedFiles()', () => {
it('should return an array of objects for each file that has exported and decorated classes',
() => {
const program = makeProgram(DECORATED_FILE);
const host = new Fesm2015ReflectionHost(false, program.getTypeChecker());
const decoratedFiles =
host.findDecoratedFiles(program.getSourceFile(DECORATED_FILE.name) !);
expect(decoratedFiles.length).toEqual(1);
const decoratedClasses = decoratedFiles[0].decoratedClasses;
expect(decoratedClasses.length).toEqual(2);
const decoratedClassA = decoratedClasses.find(c => c.name === 'A') !;
expect(decoratedClassA.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
expect(decoratedClassA.decorators.map(
decorator => decorator.args && decorator.args.map(arg => arg.getText())))
.toEqual([[`{ selector: '[a]' }`]]);
const decoratedClassD = decoratedClasses.find(c => c.name === 'D') !;
expect(decoratedClassD.decorators.map(decorator => decorator.name)).toEqual(['Directive']);
expect(decoratedClassD.decorators.map(
decorator => decorator.args && decorator.args.map(arg => arg.getText())))
.toEqual([[`{ selector: '[d]' }`]]);
});
});
}); });