fix(ivy): incorrectly generating shared pure function between null and object literal (#35481)
In #33705 we made it so that we generate pure functions for object/array literals in order to avoid having them be shared across elements/views. The problem this introduced is that further down the line the `ContantPool` uses the generated literal in order to figure out whether to share an existing factory or to create a new one. `ConstantPool` determines whether to share a factory by creating a key from the AST node and using it to look it up in the factory cache, however the key generation function didn't handle function invocations and replaced them with `null`. This means that the key for `{foo: pureFunction0(...)}` and `{foo: null}` are the same. These changes rework the logic so that instead of generating a `null` key for function invocations, we generate a variable called `<unknown>` which shouldn't be able to collide with anything. Fixes #35298. PR Close #35481
This commit is contained in:
parent
9228d7f15d
commit
22786c8e88
|
@ -3128,6 +3128,159 @@ describe('compiler compliance', () => {
|
|||
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||
});
|
||||
|
||||
it('should not share pure functions between null and object literals', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: \`
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: {}}"></div>
|
||||
\`
|
||||
})
|
||||
export class MyApp {}
|
||||
|
||||
@NgModule({declarations: [MyApp]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const MyAppDeclaration = `
|
||||
const $c0$ = function () { return { foo: null }; };
|
||||
const $c1$ = function () { return {}; };
|
||||
const $c2$ = function (a0) { return { foo: a0 }; };
|
||||
…
|
||||
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||
type: MyApp,
|
||||
selectors: [["ng-component"]],
|
||||
decls: 2,
|
||||
vars: 6,
|
||||
consts: [[${AttributeMarker.Bindings}, "dir"]],
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelement(0, "div", 0);
|
||||
$r3$.ɵɵelement(1, "div", 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction0(2, $c0$));
|
||||
$r3$.ɵɵadvance(1);
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction1(4, $c2$, $r3$.ɵɵpureFunction0(3, $c1$)));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||
});
|
||||
|
||||
it('should not share pure functions between null and array literals', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: \`
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: []}"></div>
|
||||
\`
|
||||
})
|
||||
export class MyApp {}
|
||||
|
||||
@NgModule({declarations: [MyApp]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const MyAppDeclaration = `
|
||||
const $c0$ = function () { return { foo: null }; };
|
||||
const $c1$ = function () { return []; };
|
||||
const $c2$ = function (a0) { return { foo: a0 }; };
|
||||
…
|
||||
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||
type: MyApp,
|
||||
selectors: [["ng-component"]],
|
||||
decls: 2,
|
||||
vars: 6,
|
||||
consts: [[${AttributeMarker.Bindings}, "dir"]],
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelement(0, "div", 0);
|
||||
$r3$.ɵɵelement(1, "div", 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction0(2, $c0$));
|
||||
$r3$.ɵɵadvance(1);
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction1(4, $c2$, $r3$.ɵɵpureFunction0(3, $c1$)));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||
});
|
||||
|
||||
it('should not share pure functions between null and function calls', () => {
|
||||
const files = {
|
||||
app: {
|
||||
'spec.ts': `
|
||||
import {Component, NgModule} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
template: \`
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: getFoo()}"></div>
|
||||
\`
|
||||
})
|
||||
export class MyApp {
|
||||
getFoo() {
|
||||
return 'foo!';
|
||||
}
|
||||
}
|
||||
|
||||
@NgModule({declarations: [MyApp]})
|
||||
export class MyModule {}
|
||||
`
|
||||
}
|
||||
};
|
||||
|
||||
const MyAppDeclaration = `
|
||||
const $c0$ = function () { return { foo: null }; };
|
||||
const $c1$ = function (a0) { return { foo: a0 }; };
|
||||
…
|
||||
MyApp.ɵcmp = $r3$.ɵɵdefineComponent({
|
||||
type: MyApp,
|
||||
selectors: [["ng-component"]],
|
||||
decls: 2,
|
||||
vars: 5,
|
||||
consts: [[${AttributeMarker.Bindings}, "dir"]],
|
||||
template: function MyApp_Template(rf, ctx) {
|
||||
if (rf & 1) {
|
||||
$r3$.ɵɵelement(0, "div", 0);
|
||||
$r3$.ɵɵelement(1, "div", 0);
|
||||
}
|
||||
if (rf & 2) {
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction0(2, $c0$));
|
||||
$r3$.ɵɵadvance(1);
|
||||
$r3$.ɵɵproperty("dir", $r3$.ɵɵpureFunction1(3, $c1$, ctx.getFoo()));
|
||||
}
|
||||
},
|
||||
encapsulation: 2
|
||||
});
|
||||
`;
|
||||
|
||||
const result = compile(files, angularFiles);
|
||||
expectEmit(result.source, MyAppDeclaration, 'Invalid component definition');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('inherited base classes', () => {
|
||||
|
|
|
@ -11,6 +11,16 @@ import {OutputContext, error} from './util';
|
|||
|
||||
const CONSTANT_PREFIX = '_c';
|
||||
|
||||
/**
|
||||
* `ConstantPool` tries to reuse literal factories when two or more literals are identical.
|
||||
* We determine whether literals are identical by creating a key out of their AST using the
|
||||
* `KeyVisitor`. This constant is used to replace dynamic expressions which can't be safely
|
||||
* converted into a key. E.g. given an expression `{foo: bar()}`, since we don't know what
|
||||
* the result of `bar` will be, we create a key that looks like `{foo: <unknown>}`. Note
|
||||
* that we use a variable, rather than something like `null` in order to avoid collisions.
|
||||
*/
|
||||
const UNKNOWN_VALUE_KEY = o.variable('<unknown>');
|
||||
|
||||
export const enum DefinitionKind {Injector, Directive, Component, Pipe}
|
||||
|
||||
/**
|
||||
|
@ -127,16 +137,16 @@ export class ConstantPool {
|
|||
|
||||
getLiteralFactory(literal: o.LiteralArrayExpr|o.LiteralMapExpr):
|
||||
{literalFactory: o.Expression, literalFactoryArguments: o.Expression[]} {
|
||||
// Create a pure function that builds an array of a mix of constant and variable expressions
|
||||
// Create a pure function that builds an array of a mix of constant and variable expressions
|
||||
if (literal instanceof o.LiteralArrayExpr) {
|
||||
const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : o.literal(null));
|
||||
const argumentsForKey = literal.entries.map(e => e.isConstant() ? e : UNKNOWN_VALUE_KEY);
|
||||
const key = this.keyOf(o.literalArr(argumentsForKey));
|
||||
return this._getLiteralFactory(key, literal.entries, entries => o.literalArr(entries));
|
||||
} else {
|
||||
const expressionForKey = o.literalMap(
|
||||
literal.entries.map(e => ({
|
||||
key: e.key,
|
||||
value: e.value.isConstant() ? e.value : o.literal(null),
|
||||
value: e.value.isConstant() ? e.value : UNKNOWN_VALUE_KEY,
|
||||
quoted: e.quoted
|
||||
})));
|
||||
const key = this.keyOf(expressionForKey);
|
||||
|
|
|
@ -559,5 +559,65 @@ describe('components using pure function instructions internally', () => {
|
|||
.not.toBe(secondFixture.componentInstance.directive.value);
|
||||
});
|
||||
|
||||
it('should not confuse object literals and null inside of a literal', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: {}}"></div>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
@ViewChildren(Dir) directives !: QueryList<Dir>;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const values = fixture.componentInstance.directives.map(directive => directive.value);
|
||||
|
||||
expect(values).toEqual([{foo: null}, {foo: {}}]);
|
||||
});
|
||||
|
||||
it('should not confuse array literals and null inside of a literal', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: []}"></div>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
@ViewChildren(Dir) directives !: QueryList<Dir>;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const values = fixture.componentInstance.directives.map(directive => directive.value);
|
||||
|
||||
expect(values).toEqual([{foo: null}, {foo: []}]);
|
||||
});
|
||||
|
||||
it('should not confuse function calls and null inside of a literal', () => {
|
||||
@Component({
|
||||
template: `
|
||||
<div [dir]="{foo: null}"></div>
|
||||
<div [dir]="{foo: getFoo()}"></div>
|
||||
`
|
||||
})
|
||||
class App {
|
||||
@ViewChildren(Dir) directives !: QueryList<Dir>;
|
||||
getFoo() { return 'foo!'; }
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Dir, App]});
|
||||
const fixture = TestBed.createComponent(App);
|
||||
fixture.detectChanges();
|
||||
const values = fixture.componentInstance.directives.map(directive => directive.value);
|
||||
|
||||
expect(values).toEqual([{foo: null}, {foo: 'foo!'}]);
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue