fix(compiler): Metadata should not include methods on Object.prototype (#38292)

This commit fixes a bug in View Engine whereby the compiler errorneously
thinks that a method of a component has decorator metadata when that
method is one of those in `Object.prototype`, for example `toString`.

This bug is discovered in v10.0.4 of `@angular/language-service` after
the default bundle format was switched from ES5 to ES2015.

ES5 output:
```js
if (propMetadata[propName]) {
    decorators.push.apply(decorators, __spread(propMetadata[propName]));
}
```

ES2015 output:
```js
if (propMetadata[propName]) {
    decorators.push(...propMetadata[propName]);
}
```

The bug was not discovered in ES5 because the polyfill for the spread
operator happily accepts parameters that do not have the `iterable`
symbol:

```js
function __spread() {
    for (var ar = [], i = 0; i < arguments.length; i++)
        ar = ar.concat(__read(arguments[i]));
    return ar;
}
```

whereas in es2015 it’ll fail since the iterable symbol is not present in
`propMetadata['toString']` which evaluates to a function.

Fixes https://github.com/angular/vscode-ng-language-service/issues/859

PR Close #38292
This commit is contained in:
Keen Yee Liau 2020-07-29 13:25:17 -07:00 committed by Alex Rickabaugh
parent 3a525d196b
commit fd51e01335
2 changed files with 32 additions and 1 deletions

View File

@ -235,7 +235,9 @@ export class StaticReflector implements CompileReflector {
const prop = (<any[]>propData)
.find(a => a['__symbolic'] == 'property' || a['__symbolic'] == 'method');
const decorators: any[] = [];
if (propMetadata![propName]) {
// hasOwnProperty() is used here to make sure we do not look up methods
// on `Object.prototype`.
if (propMetadata?.hasOwnProperty(propName)) {
decorators.push(...propMetadata![propName]);
}
propMetadata![propName] = decorators;

View File

@ -777,6 +777,35 @@ describe('StaticReflector', () => {
.toEqual({});
});
it('should not inherit methods from Object.prototype', () => {
const filePath = '/tmp/test.ts';
init({
...DEFAULT_TEST_DATA,
[filePath]: `
import {Component} from '@angular/core';
@Component({
selector: 'test-component',
})
export class TestComponent {
title = 'Hello World';
toString() {
return 'Test Component';
}
}
`,
});
const declaration = reflector.getStaticSymbol(filePath, 'TestComponent');
expect(declaration.filePath).toBe(filePath);
expect(declaration.name).toBe('TestComponent');
const propMetadata = reflector.propMetadata(declaration);
// 'toString' is a member of TestComponent so it should be part of the metadata.
expect(propMetadata.hasOwnProperty('toString')).toBe(true);
// There are no decorators on 'toString' so it should be an empty array.
expect(propMetadata['toString']).toEqual([]);
});
it('should inherit lifecycle hooks', () => {
initWithDecorator({
'/tmp/src/main.ts': `