parent
2f70e90493
commit
844d510d3f
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* @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';
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../ngtsc/annotations';
|
||||
import {Decorator} from '../../ngtsc/host';
|
||||
import {CompileResult, DecoratorHandler} from '../../ngtsc/transform';
|
||||
import {NgccReflectionHost} from './host/ngcc_host';
|
||||
import {ParsedClass} from './parsing/parsed_class';
|
||||
import {ParsedFile} from './parsing/parsed_file';
|
||||
import {isDefined} from './utils';
|
||||
|
||||
export interface AnalyzedClass<T = any> extends ParsedClass {
|
||||
handler: DecoratorHandler<T>;
|
||||
analysis: any;
|
||||
diagnostics?: ts.Diagnostic[];
|
||||
compilation: CompileResult[];
|
||||
}
|
||||
|
||||
export interface AnalyzedFile {
|
||||
analyzedClasses: AnalyzedClass[];
|
||||
sourceFile: ts.SourceFile;
|
||||
}
|
||||
|
||||
export interface MatchingHandler<T> {
|
||||
handler: DecoratorHandler<T>;
|
||||
decorator: Decorator;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ResourceLoader` which directly uses the filesystem to resolve resources synchronously.
|
||||
*/
|
||||
export class FileResourceLoader implements ResourceLoader {
|
||||
load(url: string): string { return fs.readFileSync(url, 'utf8'); }
|
||||
}
|
||||
|
||||
export class Analyzer {
|
||||
resourceLoader = new FileResourceLoader();
|
||||
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.host);
|
||||
handlers: DecoratorHandler<any>[] = [
|
||||
new ComponentDecoratorHandler(
|
||||
this.typeChecker, this.host, this.scopeRegistry, false, this.resourceLoader),
|
||||
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
|
||||
new InjectableDecoratorHandler(this.host, false),
|
||||
new NgModuleDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
|
||||
new PipeDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, false),
|
||||
];
|
||||
|
||||
constructor(private typeChecker: ts.TypeChecker, private host: NgccReflectionHost) {}
|
||||
|
||||
/**
|
||||
* Analyize a parsed file to generate the information about decorated classes that
|
||||
* should be converted to use ivy definitions.
|
||||
* @param file The file to be analysed for decorated classes.
|
||||
*/
|
||||
analyzeFile(file: ParsedFile): AnalyzedFile {
|
||||
const analyzedClasses =
|
||||
file.decoratedClasses.map(clazz => this.analyzeClass(file.sourceFile, clazz))
|
||||
.filter(isDefined);
|
||||
|
||||
return {
|
||||
analyzedClasses,
|
||||
sourceFile: file.sourceFile,
|
||||
};
|
||||
}
|
||||
|
||||
protected analyzeClass(file: ts.SourceFile, clazz: ParsedClass): AnalyzedClass|undefined {
|
||||
const matchingHandlers =
|
||||
this.handlers.map(handler => ({handler, decorator: handler.detect(clazz.decorators)}))
|
||||
.filter(isMatchingHandler);
|
||||
|
||||
if (matchingHandlers.length > 1) {
|
||||
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
|
||||
}
|
||||
|
||||
if (matchingHandlers.length == 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {handler, decorator} = matchingHandlers[0];
|
||||
const {analysis, diagnostics} = handler.analyze(clazz.declaration, decorator);
|
||||
let compilation = handler.compile(clazz.declaration, analysis);
|
||||
if (!Array.isArray(compilation)) {
|
||||
compilation = [compilation];
|
||||
}
|
||||
return {...clazz, handler, analysis, diagnostics, compilation};
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchingHandler<T>(handler: Partial<MatchingHandler<T>>): handler is MatchingHandler<T> {
|
||||
return !!handler.decorator;
|
||||
}
|
|
@ -13,6 +13,7 @@ ts_library(
|
|||
"//packages/compiler-cli/src/ngcc",
|
||||
"//packages/compiler-cli/src/ngtsc/host",
|
||||
"//packages/compiler-cli/src/ngtsc/testing",
|
||||
"//packages/compiler-cli/src/ngtsc/transform",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* @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 ts from 'typescript';
|
||||
import {Decorator} from '../../ngtsc/host';
|
||||
import {DecoratorHandler} from '../../ngtsc/transform';
|
||||
import {AnalyzedFile, Analyzer} from '../src/analyzer';
|
||||
import {Esm2015ReflectionHost} from '../src/host/esm2015_host';
|
||||
import {ParsedClass} from '../src/parsing/parsed_class';
|
||||
import {ParsedFile} from '../src/parsing/parsed_file';
|
||||
import {getDeclaration, makeProgram} from './helpers/utils';
|
||||
|
||||
const TEST_PROGRAM = {
|
||||
name: 'test.js',
|
||||
contents: `
|
||||
import {Component, Injectable} from '@angular/core';
|
||||
|
||||
@Component()
|
||||
export class MyComponent {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {}
|
||||
`
|
||||
};
|
||||
|
||||
function createTestHandler() {
|
||||
const handler = jasmine.createSpyObj<DecoratorHandler<any>>('TestDecoratorHandler', [
|
||||
'detect',
|
||||
'analyze',
|
||||
'compile',
|
||||
]);
|
||||
// Only detect the Component decorator
|
||||
handler.detect.and.callFake(
|
||||
(decorators: Decorator[]) => decorators.find(d => d.name === 'Component'));
|
||||
// The "test" analysis is just the name of the decorator being analyzed
|
||||
handler.analyze.and.callFake(
|
||||
((decl: ts.Declaration, dec: Decorator) => ({analysis: dec.name, diagnostics: null})));
|
||||
// The "test" compilation result is just the name of the decorator being compiled
|
||||
handler.compile.and.callFake(((decl: ts.Declaration, analysis: any) => ({analysis})));
|
||||
return handler;
|
||||
}
|
||||
|
||||
function createParsedFile(program: ts.Program) {
|
||||
const file = new ParsedFile(program.getSourceFile('test.js') !);
|
||||
|
||||
const componentClass = getDeclaration(program, 'test.js', 'MyComponent', ts.isClassDeclaration);
|
||||
file.decoratedClasses.push(new ParsedClass('MyComponent', {} as any, [{
|
||||
name: 'Component',
|
||||
import: {from: '@angular/core', name: 'Component'},
|
||||
node: null as any,
|
||||
args: null
|
||||
}]));
|
||||
|
||||
const serviceClass = getDeclaration(program, 'test.js', 'MyService', ts.isClassDeclaration);
|
||||
file.decoratedClasses.push(new ParsedClass('MyService', {} as any, [{
|
||||
name: 'Injectable',
|
||||
import: {from: '@angular/core', name: 'Injectable'},
|
||||
node: null as any,
|
||||
args: null
|
||||
}]));
|
||||
|
||||
return file;
|
||||
}
|
||||
|
||||
describe('Analyzer', () => {
|
||||
describe('analyzeFile()', () => {
|
||||
let program: ts.Program;
|
||||
let testHandler: jasmine.SpyObj<DecoratorHandler<any>>;
|
||||
let result: AnalyzedFile;
|
||||
|
||||
beforeEach(() => {
|
||||
program = makeProgram(TEST_PROGRAM);
|
||||
const file = createParsedFile(program);
|
||||
const analyzer = new Analyzer(
|
||||
program.getTypeChecker(), new Esm2015ReflectionHost(program.getTypeChecker()));
|
||||
testHandler = createTestHandler();
|
||||
analyzer.handlers = [testHandler];
|
||||
result = analyzer.analyzeFile(file);
|
||||
});
|
||||
|
||||
it('should return an object containing a reference to the original source file',
|
||||
() => { expect(result.sourceFile).toBe(program.getSourceFile('test.js') !); });
|
||||
|
||||
it('should call detect on the decorator handlers with each class from the parsed file', () => {
|
||||
expect(testHandler.detect).toHaveBeenCalledTimes(2);
|
||||
expect(testHandler.detect.calls.allArgs()[0][0]).toEqual([jasmine.objectContaining(
|
||||
{name: 'Component'})]);
|
||||
expect(testHandler.detect.calls.allArgs()[1][0]).toEqual([jasmine.objectContaining(
|
||||
{name: 'Injectable'})]);
|
||||
});
|
||||
|
||||
it('should return an object containing the classes that were analyzed', () => {
|
||||
expect(result.analyzedClasses.length).toEqual(1);
|
||||
expect(result.analyzedClasses[0].name).toEqual('MyComponent');
|
||||
});
|
||||
|
||||
it('should analyze and compile the classes that are detected', () => {
|
||||
expect(testHandler.analyze).toHaveBeenCalledTimes(1);
|
||||
expect(testHandler.analyze.calls.allArgs()[0][1].name).toEqual('Component');
|
||||
|
||||
expect(testHandler.compile).toHaveBeenCalledTimes(1);
|
||||
expect(testHandler.compile.calls.allArgs()[0][1]).toEqual('Component');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue