fix(ivy): ngtsc should pay attention to declaration order (#25392)

When generating the 'directives:' property of ngComponentDef, ngtsc
needs to be conscious of declaration order. If a directive being
written into the array is declarated after the component currently
being compiled, then the entire directives array needs to be wrapped
in a closure.

This commit fixes ngtsc to pay attention to such ordering issues
within directives arrays.

PR Close #25392
This commit is contained in:
Alex Rickabaugh 2018-08-06 14:49:35 +02:00 committed by Ben Lesh
parent 6f085f8610
commit 2befc65777
7 changed files with 94 additions and 13 deletions

View File

@ -142,6 +142,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// analyzed and the full compilation scope for the component can be realized. // analyzed and the full compilation scope for the component can be realized.
pipes: EMPTY_MAP, pipes: EMPTY_MAP,
directives: EMPTY_MAP, directives: EMPTY_MAP,
wrapDirectivesInClosure: false,
} }
}; };
} }
@ -155,7 +156,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// Replace the empty components and directives from the analyze() step with a fully expanded // Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during compile() the whole compilation unit has been // scope. This is possible now because during compile() the whole compilation unit has been
// fully analyzed. // fully analyzed.
analysis = {...analysis, ...scope}; const {directives, pipes, containsForwardDecls} = scope;
const wrapDirectivesInClosure: boolean = !!containsForwardDecls;
analysis = {...analysis, directives, pipes, wrapDirectivesInClosure};
} }
const res = compileComponentFromMetadata(analysis, pool, makeBindingParser()); const res = compileComponentFromMetadata(analysis, pool, makeBindingParser());

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Expression, ExternalExpr, ExternalReference} from '@angular/compiler'; import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ReflectionHost} from '../../host'; import {ReflectionHost} from '../../host';
@ -33,6 +33,7 @@ export interface ModuleData {
export interface CompilationScope<T> { export interface CompilationScope<T> {
directives: Map<string, T>; directives: Map<string, T>;
pipes: Map<string, T>; pipes: Map<string, T>;
containsForwardDecls?: boolean;
} }
/** /**
@ -146,7 +147,7 @@ export class SelectorScopeRegistry {
// The scope as cached is in terms of References, not Expressions. Converting between them // The scope as cached is in terms of References, not Expressions. Converting between them
// requires knowledge of the context file (in this case, the component node's source file). // requires knowledge of the context file (in this case, the component node's source file).
return convertScopeToExpressions(scope, node.getSourceFile()); return convertScopeToExpressions(scope, node);
} }
// This is the first time the scope for this module is being computed. // This is the first time the scope for this module is being computed.
@ -179,7 +180,7 @@ export class SelectorScopeRegistry {
this._compilationScopeCache.set(node, scope); this._compilationScopeCache.set(node, scope);
// Convert References to Expressions in the context of the component's source file. // Convert References to Expressions in the context of the component's source file.
return convertScopeToExpressions(scope, node.getSourceFile()); return convertScopeToExpressions(scope, node);
} }
/** /**
@ -390,8 +391,40 @@ function convertReferenceMap(
} }
function convertScopeToExpressions( function convertScopeToExpressions(
scope: CompilationScope<Reference>, context: ts.SourceFile): CompilationScope<Expression> { scope: CompilationScope<Reference>, context: ts.Declaration): CompilationScope<Expression> {
const directives = convertReferenceMap(scope.directives, context); const sourceContext = ts.getOriginalNode(context).getSourceFile();
const pipes = convertReferenceMap(scope.pipes, context); const directives = convertReferenceMap(scope.directives, sourceContext);
return {directives, pipes}; const pipes = convertReferenceMap(scope.pipes, sourceContext);
const declPointer = maybeUnwrapNameOfDeclaration(context);
let containsForwardDecls = false;
directives.forEach(expr => {
containsForwardDecls =
containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext);
});
!containsForwardDecls && pipes.forEach(expr => {
containsForwardDecls =
containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext);
});
return {directives, pipes, containsForwardDecls};
} }
function isExpressionForwardReference(
expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean {
if (isWrappedTsNodeExpr(expr)) {
const node = ts.getOriginalNode(expr.node);
return node.getSourceFile() === contextSource && context.pos < node.pos;
}
return false;
}
function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr<ts.Node> {
return expr instanceof WrappedNodeExpr;
}
function maybeUnwrapNameOfDeclaration(decl: ts.Declaration): ts.Declaration|ts.Identifier {
if ((ts.isClassDeclaration(decl) || ts.isVariableDeclaration(decl)) && decl.name !== undefined &&
ts.isIdentifier(decl.name)) {
return decl.name;
}
return decl;
}

View File

@ -1002,7 +1002,7 @@ describe('compiler compliance', () => {
$r3$.ɵEe(1, "div", $e0_attrs$); $r3$.ɵEe(1, "div", $e0_attrs$);
} }
}, },
directives:[SomeDirective] directives: function () { return [SomeDirective]; }
});`; });`;
const result = compile(files, angularFiles); const result = compile(files, angularFiles);
@ -1582,7 +1582,7 @@ describe('compiler compliance', () => {
} }
if (rf & 2) { $r3$.ɵp(1,"forOf",$r3$.ɵb(ctx.items)); } if (rf & 2) { $r3$.ɵp(1,"forOf",$r3$.ɵb(ctx.items)); }
}, },
directives: [ForOfDirective] directives: function() { return [ForOfDirective]; }
}); });
`; `;
@ -1660,7 +1660,7 @@ describe('compiler compliance', () => {
$r3$.ɵp(1, "forOf", $r3$.ɵb(ctx.items)); $r3$.ɵp(1, "forOf", $r3$.ɵb(ctx.items));
} }
}, },
directives: [ForOfDirective] directives: function() { return [ForOfDirective]; }
}); });
`; `;
@ -1758,7 +1758,7 @@ describe('compiler compliance', () => {
$r3$.ɵp(1, "forOf", $r3$.ɵb(ctx.items)); $r3$.ɵp(1, "forOf", $r3$.ɵb(ctx.items));
} }
}, },
directives: [ForOfDirective] directives: function () { return [ForOfDirective]; }
});`; });`;
const result = compile(files, angularFiles); const result = compile(files, angularFiles);

View File

@ -715,4 +715,36 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents) expect(jsContents)
.toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }'); .toContain('function GrandChild_Factory(t) { return new (t || GrandChild)(); }');
}); });
it('should wrap "directives" in component metadata in a closure when forward references are present',
() => {
writeConfig();
write('test.ts', `
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'cmp-a',
template: '<cmp-b></cmp-b>',
})
class CmpA {}
@Component({
selector: 'cmp-b',
template: 'This is B',
})
class CmpB {}
@NgModule({
declarations: [CmpA, CmpB],
})
class Module {}
`);
const exitCode = main(['-p', basePath], errorSpy);
expect(errorSpy).not.toHaveBeenCalled();
expect(exitCode).toBe(0);
const jsContents = getContents('test.js');
expect(jsContents).toContain('directives: function () { return [CmpB]; }');
});
}); });

View File

@ -144,6 +144,13 @@ export interface R3ComponentMetadata extends R3DirectiveMetadata {
* scope of the compilation. * scope of the compilation.
*/ */
directives: Map<string, o.Expression>; directives: Map<string, o.Expression>;
/**
* Whether to wrap the 'directives' array, if one is generated, in a closure.
*
* This is done when the directives contain forward references.
*/
wrapDirectivesInClosure: boolean;
} }
/** /**

View File

@ -165,7 +165,11 @@ export function compileComponentFromMetadata(
// e.g. `directives: [MyDirective]` // e.g. `directives: [MyDirective]`
if (directivesUsed.size) { if (directivesUsed.size) {
definitionMap.set('directives', o.literalArr(Array.from(directivesUsed))); let directivesExpr: o.Expression = o.literalArr(Array.from(directivesUsed));
if (meta.wrapDirectivesInClosure) {
directivesExpr = o.fn([], [new o.ReturnStatement(directivesExpr)]);
}
definitionMap.set('directives', directivesExpr);
} }
// e.g. `pipes: [MyPipe]` // e.g. `pipes: [MyPipe]`
@ -241,6 +245,7 @@ export function compileComponentFromRender2(
directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx), directives: typeMapToExpressionMap(directiveTypeBySel, outputCtx),
pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx), pipes: typeMapToExpressionMap(pipeTypeByName, outputCtx),
viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx), viewQueries: queriesFromGlobalMetadata(component.viewQueries, outputCtx),
wrapDirectivesInClosure: false,
}; };
const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser); const res = compileComponentFromMetadata(meta, outputCtx.constantPool, bindingParser);

View File

@ -73,6 +73,7 @@ export function compileComponent(type: Type<any>, metadata: Component): void {
directives: new Map(), directives: new Map(),
pipes: new Map(), pipes: new Map(),
viewQueries: [], viewQueries: [],
wrapDirectivesInClosure: false,
}, },
constantPool, makeBindingParser()); constantPool, makeBindingParser());
const preStatements = [...constantPool.statements, ...res.statements]; const preStatements = [...constantPool.statements, ...res.statements];