feat(ivy): update compiler to specification (#21657)

PR Close #21657
This commit is contained in:
Chuck Jazdzewski
2018-01-11 15:37:56 -08:00
committed by Miško Hevery
parent 8c51c276c7
commit 86d9612230
9 changed files with 565 additions and 90 deletions

View File

@ -6,13 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler';
import {AotCompilerHost, AotCompilerOptions, AotSummaryResolver, CompileDirectiveMetadata, CompileMetadataResolver, CompilerConfig, DirectiveNormalizer, DirectiveResolver, DomElementSchemaRegistry, HtmlParser, I18NHtmlParser, Lexer, NgModuleResolver, Parser, PipeResolver, StaticReflector, StaticSymbol, StaticSymbolCache, StaticSymbolResolver, TemplateParser, TypeScriptEmitter, analyzeNgModules, createAotUrlResolver} from '@angular/compiler';
import {ViewEncapsulation} from '@angular/core';
import * as ts from 'typescript';
import {ConstantPool} from '../../src/constant_pool';
import * as o from '../../src/output/output_ast';
import {compileComponent} from '../../src/render3/r3_view_compiler';
import {compileComponent, compileDirective} from '../../src/render3/r3_view_compiler';
import {OutputContext} from '../../src/util';
import {MockAotCompilerHost, MockCompilerHost, MockData, MockDirectory, arrayToMockDir, settings, setup, toMockFileArray} from '../aot/test_util';
@ -99,8 +99,327 @@ describe('r3_view_compiler', () => {
const result = compile(files, angularFiles);
expect(result.source).toContain('@angular/core');
});
/* These tests are codified version of the tests in compiler_canonical_spec.ts. Every
* test in compiler_canonical_spec.ts should have a corresponding test here.
*/
describe('compiler conformance', () => {
describe('elements', () => {
it('should translate DOM structure', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'my-component',
template: \`<div class="my-app" title="Hello">Hello <b>World</b>!</div>\`
})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
// The factory should look like this:
const factory = 'factory: () => { return new MyComponent(); }';
// The template should look like this (where IDENT is a wild card for an identifier):
const template = `
template: (ctx: IDENT, cm: IDENT) => {
if (cm) {
IDENT.ɵE(0, 'div', IDENT);
IDENT.ɵT(1, 'Hello ');
IDENT.ɵE(2, 'b');
IDENT.ɵT(3, 'World');
IDENT.ɵe();
IDENT.ɵT(4, '!');
IDENT.ɵe();
}
}
`;
// The compiler should also emit a const array like this:
const constants = `const IDENT = ['class', 'my-app', 'title', 'Hello'];`;
const result = compile(files, angularFiles);
expectEmit(result.source, factory, 'Incorrect factory');
expectEmit(result.source, template, 'Incorrect template');
expectEmit(result.source, constants, 'Incorrect shared constants');
});
});
});
describe('components & directives', () => {
it('should instantiate directives', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule} from '@angular/core';
@Component({selector: 'child', template: 'child-view'})
export class ChildComponent {}
@Directive({selector: '[some-directive]'})
export class SomeDirective {}
@Component({selector: 'my-component', template: '<child some-directive></child>!'})
export class MyComponent {}
@NgModule({declarations: [ChildComponent, SomeDirective, MyComponent]})
export class MyModule{}
`
}
};
// ChildComponent definition should be:
const ChildComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
tag: 'child',
factory: () => { return new ChildComponent(); },
template: (ctx: IDENT, cm: IDENT) => {
if (cm) {
IDENT.ɵT(0, 'child-view');
}
}
});`;
// SomeDirective definition should be:
const SomeDirectiveDefinition = `
static ngDirectiveDef = IDENT.ɵdefineDirective({
factory: () => {return new SomeDirective(); }
});
`;
// MyComponent definition should be:
const MyComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
tag: 'my-component',
factory: () => { return new MyComponent(); },
template: (ctx: IDENT, cm: IDENT) => {
if (cm) {
IDENT.ɵE(0, ChildComponent, IDENT, IDENT);
IDENT.ɵe();
IDENT.ɵT(3, '!');
}
ChildComponent.ngComponentDef.r(1, 0);
SomeDirective.ngDirectiveDef.r(2, 0);
}
});
`;
// The following constants should be emitted as well.
const AttributesConstant = `const IDENT = ['some-directive', ''];`;
const DirectivesConstant = `const IDENT = [SomeDirective];`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, ChildComponentDefinition, 'Incorrect ChildComponent.ngComponentDef');
expectEmit(source, SomeDirectiveDefinition, 'Incorrect SomeDirective.ngDirectiveDef');
expectEmit(source, MyComponentDefinition, 'Incorrect MyComponentDefinition.ngComponentDef');
expectEmit(source, AttributesConstant, 'Incorrect shared attributes constant');
expectEmit(source, DirectivesConstant, 'Incorrect share directives constant');
});
it('should support structural directives', () => {
const files = {
app: {
'spec.ts': `
import {Component, Directive, NgModule, TemplateRef} from '@angular/core';
@Directive({selector: '[if]'})
export class IfDirective {
constructor(template: TemplateRef<any>) { }
}
@Component({
selector: 'my-component',
template: '<ul #foo><li *if>{{salutation}} {{foo}}</li></ul>'
})
export class MyComponent {
salutation = 'Hello';
}
@NgModule({declarations: [IfDirective, MyComponent]})
export class MyModule {}
`
}
};
const IfDirectiveDefinition = `
static ngDirectiveDef = IDENT.ɵdefineDirective({
factory: () => { return new IfDirective(IDENT.ɵinjectTemplateRef()); }
});`;
const MyComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
tag: 'my-component',
factory: () => { return new MyComponent(); },
template: (ctx: IDENT, cm: IDENT) => {
if (cm) {
IDENT.ɵE(0, 'ul', null, null, IDENT);
IDENT.ɵC(2, IDENT, C2Template);
IDENT.ɵe();
}
const IDENT = IDENT.ɵm(1);
IDENT.ɵcR(2);
IfDirective.ngDirectiveDef.r(3,2);
IDENT.ɵcr();
function C2Template(ctx0: IDENT, cm: IDENT) {
if (cm) {
IDENT.ɵE(0, 'li');
IDENT.ɵT(1);
IDENT.ɵe();
}
IDENT.ɵt(1, IDENT.ɵb2('', ctx.salutation, ' ', IDENT, ''));
}
}
});`;
const locals = `const IDENT = ['foo', ''];`;
const directives = `const IDENT = [IfDirective];`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, IfDirectiveDefinition, 'Incorrect IfDirective.ngDirectiveDef');
expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
expectEmit(source, locals, 'Incorrect share locals constant');
expectEmit(source, directives, 'Incorrect shared directive constant');
});
it('local reference', () => {
const files = {
app: {
'spec.ts': `
import {Component, NgModule} from '@angular/core';
@Component({selector: 'my-component', template: '<input #user>Hello {{user.value}}!'})
export class MyComponent {}
@NgModule({declarations: [MyComponent]})
export class MyModule {}
`
}
};
const MyComponentDefinition = `
static ngComponentDef = IDENT.ɵdefineComponent({
tag: 'my-component',
factory: () => { return new MyComponent(); },
template: (ctx: IDENT, cm: IDENT) => {
if (cm) {
IDENT.ɵE(0, 'input', null, null, IDENT);
IDENT.ɵe();
IDENT.ɵT(2);
}
const IDENT = IDENT.ɵm(1);
IDENT.ɵt(2, IDENT.ɵb1('Hello ', IDENT.value, '!'));
}
});
`;
const locals = `
const IDENT = ['user', ''];
`;
const result = compile(files, angularFiles);
const source = result.source;
expectEmit(source, MyComponentDefinition, 'Incorrect MyComponent.ngComponentDef');
expectEmit(source, locals, 'Incorrect locals constant definition');
});
});
});
const IDENTIFIER = /[A-Za-z_$ɵ][A-Za-z0-9_$]*/;
const OPERATOR =
/!|%|\*|\/|\^|\&|\&\&\|\||\|\||\(|\)|\{|\}|\[|\]|:|;|\.|<|<=|>|>=|=|==|===|!=|!==|=>|\+|\+\+|-|--|@|,|\.|\.\.\./;
const STRING = /\'[^'\n]*\'|"[^'\n]*"|`[^`]*`/;
const NUMBER = /[0-9]+/;
const TOKEN = new RegExp(
`^((${IDENTIFIER.source})|(${OPERATOR.source})|(${STRING.source})|${NUMBER.source})`);
const WHITESPACE = /^\s+/;
type Piece = string | RegExp;
const IDENT = /[A-Za-z$_][A-Za-z0-9$_]*/;
function tokenize(text: string): Piece[] {
function matches(exp: RegExp): string|false {
const m = text.match(exp);
if (!m) return false;
text = text.substr(m[0].length);
return m[0];
}
function next(): string {
const result = matches(TOKEN);
if (!result) {
throw Error(`Invalid test, no token found for '${text.substr(0, 30)}...'`);
}
matches(WHITESPACE);
return result;
}
const pieces: Piece[] = [];
matches(WHITESPACE);
while (text) {
const token = next();
if (token === 'IDENT') {
pieces.push(IDENT);
} else {
pieces.push(token);
}
}
return pieces;
}
const contextWidth = 100;
function expectEmit(source: string, emitted: string, description: string) {
const pieces = tokenize(emitted);
const expr = r(...pieces);
if (!expr.test(source)) {
let last: number = 0;
for (let i = 1; i < pieces.length; i++) {
let t = r(...pieces.slice(0, i));
let m = source.match(t);
let expected = pieces[i - 1] == IDENT ? '<IDENT>' : pieces[i - 1];
if (!m) {
const contextPieceWidth = contextWidth / 2;
fail(
`${description}: Expected to find ${expected} '${source.substr(0,last)}[<---HERE]${source.substr(last)}'`);
return;
} else {
last = (m.index || 0) + m[0].length;
}
}
fail(
'Test helper failure: Expected expression failed but the reporting logic could not find where it failed');
}
}
const IDENT_LIKE = /^[a-z][A-Z]/;
const SPECIAL_RE_CHAR = /\/|\(|\)|\||\*|\+|\[|\]|\{|\}/g;
function r(...pieces: (string | RegExp)[]): RegExp {
let results: string[] = [];
let first = true;
for (const piece of pieces) {
if (!first)
results.push(`\\s${typeof piece === 'string' && IDENT_LIKE.test(piece) ? '+' : '*'}`);
first = false;
if (typeof piece === 'string') {
results.push(piece.replace(SPECIAL_RE_CHAR, s => '\\' + s));
} else {
results.push('(' + piece.source + ')');
}
}
return new RegExp(results.join(''));
}
function compile(
data: MockDirectory, angularFiles: MockData, options: AotCompilerOptions = {},
errorCollector: (error: any, fileName?: string) => void = error => { throw error; }) {
@ -156,7 +475,7 @@ function compile(
const directives = Array.from(analyzedModules.ngModuleByPipeOrDirective.keys());
const fakeOuputContext: OutputContext = {
const fakeOutputContext: OutputContext = {
genFilePath: 'fakeFactory.ts',
statements: [],
importExpr(symbol: StaticSymbol, typeParams: o.Type[]) {
@ -182,7 +501,10 @@ function compile(
// Compile the directives.
for (const directive of directives) {
const module = analyzedModules.ngModuleByPipeOrDirective.get(directive) !;
const module = analyzedModules.ngModuleByPipeOrDirective.get(directive);
if (!module || !module.type.reference.filePath.startsWith('/app')) {
continue;
}
if (resolver.isDirective(directive)) {
const metadata = resolver.getDirectiveMetadata(directive);
if (metadata.isComponent) {
@ -196,17 +518,24 @@ function compile(
const parsedTemplate = templateParser.parse(
metadata, htmlAst, directives, pipes, module.schemas, fakeUrl, false);
compileComponent(fakeOuputContext, metadata, parsedTemplate.template, staticReflector);
compileComponent(fakeOutputContext, metadata, parsedTemplate.template, staticReflector);
} else {
compileDirective(fakeOutputContext, metadata, staticReflector);
}
}
}
fakeOuputContext.statements.unshift(...fakeOuputContext.constantPool.statements);
fakeOutputContext.statements.unshift(...fakeOutputContext.constantPool.statements);
const emitter = new TypeScriptEmitter();
const result = emitter.emitStatementsAndContext(
fakeOuputContext.genFilePath, fakeOuputContext.statements, '', false);
const moduleName = compilerHost.fileNameToModuleName(
fakeOutputContext.genFilePath, fakeOutputContext.genFilePath);
return {source: result.sourceText, outputContext: fakeOuputContext};
const result = emitter.emitStatementsAndContext(
fakeOutputContext.genFilePath, fakeOutputContext.statements, '', false,
/* referenceFilter */ undefined,
/* importFilter */ e => e.moduleName != null && e.moduleName.startsWith('/app'));
return {source: result.sourceText, outputContext: fakeOutputContext};
}

View File

@ -407,7 +407,7 @@ class ArrayConsole implements Console {
expectVisitedNode(
new class extends
NullVisitor{visitReference(ast: ReferenceAst, context: any): any{return ast;}},
new ReferenceAst('foo', null !, null !));
new ReferenceAst('foo', null !, null !, null !));
});
it('should visit VariableAst', () => {
@ -474,7 +474,7 @@ class ArrayConsole implements Console {
new NgContentAst(0, 0, null !),
new EmbeddedTemplateAst([], [], [], [], [], [], false, [], [], 0, null !),
new ElementAst('foo', [], [], [], [], [], [], false, [], [], 0, null !, null !),
new ReferenceAst('foo', null !, null !), new VariableAst('foo', 'bar', null !),
new ReferenceAst('foo', null !, 'bar', null !), new VariableAst('foo', 'bar', null !),
new BoundEventAst('foo', 'bar', 'goo', null !, null !),
new BoundElementPropertyAst('foo', null !, null !, null !, 'bar', null !),
new AttrAst('foo', 'bar', null !), new BoundTextAst(null !, 0, null !),