Skip to content

Commit 51e5cb2

Browse files
devversionprofanis
authored andcommitted
fix(core): detect DI parameters in JIT mode for downleveled ES2015 classes (angular#38463)
In the Angular Package Format, we always shipped UMD bundles and previously even ES5 module output. With V10, we removed the ES5 module output but kept the UMD ES5 output. For this, we were able to remove our second TypeScript transpilation. Instead we started only building ES2015 output and then downleveled it to ES5 UMD for the NPM packages. This worked as expected but unveiled an issue in the `@angular/core` reflection capabilities. In JIT mode, Angular determines constructor parameters (for DI) using the `ReflectionCapabilities`. The reflection capabilities basically read runtime metadata of classes to determine the DI parameters. Such metadata can be either stored in static class properties like `ctorParameters` or within TypeScript's `design:params`. If Angular comes across a class that does not have any parameter metadata, it tries to detect if the given class is actually delegating to an inherited class. It does this naively in JIT by checking if the stringified class (function in ES5) matches a certain pattern. e.g. ```js function MatTable() { var _this = _super.apply(this, arguments) || this; ``` These patterns are reluctant to changes of the class output. If a class is not recognized properly, the DI parameters will be assumed empty and the class is **incorrectly** constructed without arguments. This actually happened as part of v10 now. Since we downlevel ES2015 to ES5 (instead of previously compiling sources directly to ES5), the class output changed slightly so that Angular no longer detects it. e.g. ```js var _this = _super.apply(this, __spread(arguments)) || this; ``` This happens because the ES2015 output will receive an auto-generated constructor if the class defines class properties. This constructor is then already containing an explicit `super` call. ```js export class MatTable extends CdkTable { constructor() { super(...arguments); this.disabled = true; } } ``` If we then downlevel this file to ES5 with `--downlevelIteration`, TypeScript adjusts the `super` call so that the spread operator is no longer used (not supported in ES5). The resulting super call is different to the super call that would have been emitted if we would directly transpile to ES5. Ultimately, Angular no longer detects such classes as having an delegate constructor -> and DI breaks. We fix this by expanding the rather naive RegExp patterns used for the reflection capabilities so that downleveled pass-through/delegate constructors are properly detected. There is a risk of a false-positive as we cannot detect whether `__spread` is actually the TypeScript spread helper, but given the reflection patterns already make lots of assumptions (e.g. that `super` is actually the superclass, we should be fine making this assumption too. The false-positive would not result in a broken app, but rather in unnecessary providers being injected (as a noop). Fixes angular#38453 PR Close angular#38463
1 parent 633201e commit 51e5cb2

File tree

6 files changed

+108
-7
lines changed

6 files changed

+108
-7
lines changed

.circleci/config.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,18 @@ jobs:
656656
- run: yarn tsc -p packages
657657
- run: yarn tsc -p modules
658658
- run: yarn bazel build //packages/zone.js:npm_package
659+
# Build test fixtures for a test that rely on Bazel-generated fixtures. Note that disabling
660+
# specific tests which are reliant on such generated fixtures is not an option as SystemJS
661+
# in the Saucelabs legacy job always fetches referenced files, even if the imports would be
662+
# guarded by an check to skip in the Saucelabs legacy job. We should be good running such
663+
# test in all supported browsers on Saucelabs anyway until this job can be removed.
664+
- run:
665+
name: Preparing Bazel-generated fixtures required in legacy tests
666+
command: |
667+
yarn bazel build //packages/core/test:downleveled_es5_fixture
668+
# Needed for the ES5 downlevel reflector test in `packages/core/test/reflection`.
669+
cp dist/bin/packages/core/test/reflection/es5_downleveled_inheritance_fixture.js \
670+
dist/all/@angular/core/test/reflection/es5_downleveled_inheritance_fixture.js
659671
- run:
660672
# Waiting on ready ensures that we don't run tests too early without Saucelabs not being ready.
661673
name: Waiting for Saucelabs tunnel to connect

karma-js.conf.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ module.exports = function(config) {
3737

3838
'node_modules/core-js/client/core.js',
3939
'node_modules/jasmine-ajax/lib/mock-ajax.js',
40+
41+
// Dependencies built by Bazel. See `config.yml` for steps running before
42+
// the legacy Saucelabs tests run.
4043
'dist/bin/packages/zone.js/npm_package/bundles/zone.umd.js',
4144
'dist/bin/packages/zone.js/npm_package/bundles/zone-testing.umd.js',
4245
'dist/bin/packages/zone.js/npm_package/bundles/task-tracking.umd.js',

packages/core/src/reflection/reflection_capabilities.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,45 @@ import {GetterFn, MethodFn, SetterFn} from './types';
1717

1818

1919

20+
/*
21+
* #########################
22+
* Attention: These Regular expressions have to hold even if the code is minified!
23+
* ##########################
24+
*/
25+
2026
/**
21-
* Attention: These regex has to hold even if the code is minified!
27+
* Regular expression that detects pass-through constructors for ES5 output. This Regex
28+
* intends to capture the common delegation pattern emitted by TypeScript and Babel. Also
29+
* it intends to capture the pattern where existing constructors have been downleveled from
30+
* ES2015 to ES5 using TypeScript w/ downlevel iteration. e.g.
31+
*
32+
* * ```
33+
* function MyClass() {
34+
* var _this = _super.apply(this, arguments) || this;
35+
* ```
36+
*
37+
* ```
38+
* function MyClass() {
39+
* var _this = _super.apply(this, __spread(arguments)) || this;
40+
* ```
41+
*
42+
* More details can be found in: https://github.com/angular/angular/issues/38453.
2243
*/
23-
export const DELEGATE_CTOR = /^function\s+\S+\(\)\s*{[\s\S]+\.apply\(this,\s*arguments\)/;
24-
export const INHERITED_CLASS = /^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{/;
25-
export const INHERITED_CLASS_WITH_CTOR =
44+
export const ES5_DELEGATE_CTOR =
45+
/^function\s+\S+\(\)\s*{[\s\S]+\.apply\(this,\s*(arguments|[^()]+\(arguments\))\)/;
46+
/** Regular expression that detects ES2015 classes which extend from other classes. */
47+
export const ES2015_INHERITED_CLASS = /^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{/;
48+
/**
49+
* Regular expression that detects ES2015 classes which extend from other classes and
50+
* have an explicit constructor defined.
51+
*/
52+
export const ES2015_INHERITED_CLASS_WITH_CTOR =
2653
/^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{[\s\S]*constructor\s*\(/;
27-
export const INHERITED_CLASS_WITH_DELEGATE_CTOR =
54+
/**
55+
* Regular expression that detects ES2015 classes which extend from other classes
56+
* and inherit a constructor.
57+
*/
58+
export const ES2015_INHERITED_CLASS_WITH_DELEGATE_CTOR =
2859
/^class\s+[A-Za-z\d$_]*\s*extends\s+[^{]+{[\s\S]*constructor\s*\(\)\s*{\s*super\(\.\.\.arguments\)/;
2960

3061
/**
@@ -36,8 +67,9 @@ export const INHERITED_CLASS_WITH_DELEGATE_CTOR =
3667
* an initialized instance property.
3768
*/
3869
export function isDelegateCtor(typeStr: string): boolean {
39-
return DELEGATE_CTOR.test(typeStr) || INHERITED_CLASS_WITH_DELEGATE_CTOR.test(typeStr) ||
40-
(INHERITED_CLASS.test(typeStr) && !INHERITED_CLASS_WITH_CTOR.test(typeStr));
70+
return ES5_DELEGATE_CTOR.test(typeStr) ||
71+
ES2015_INHERITED_CLASS_WITH_DELEGATE_CTOR.test(typeStr) ||
72+
(ES2015_INHERITED_CLASS.test(typeStr) && !ES2015_INHERITED_CLASS_WITH_CTOR.test(typeStr));
4173
}
4274

4375
export class ReflectionCapabilities implements PlatformReflectionCapabilities {

packages/core/test/BUILD.bazel

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,31 @@ circular_dependency_test(
1515
deps = ["//packages/core/testing"],
1616
)
1717

18+
genrule(
19+
name = "downleveled_es5_fixture",
20+
srcs = ["reflection/es2015_inheritance_fixture.ts"],
21+
outs = ["reflection/es5_downleveled_inheritance_fixture.js"],
22+
cmd = """
23+
es2015_out_dir="/tmp/downleveled_es5_fixture/"
24+
es2015_out_file="$$es2015_out_dir/es2015_inheritance_fixture.js"
25+
26+
# Build the ES2015 output and then downlevel it to ES5.
27+
$(execpath @npm//typescript/bin:tsc) $< --outDir $$es2015_out_dir --target ES2015 \
28+
--types --module umd
29+
$(execpath @npm//typescript/bin:tsc) --outFile $@ $$es2015_out_file --allowJs \
30+
--types --target ES5
31+
""",
32+
tools = ["@npm//typescript/bin:tsc"],
33+
)
34+
1835
ts_library(
1936
name = "test_lib",
2037
testonly = True,
2138
srcs = glob(
2239
["**/*.ts"],
2340
exclude = [
2441
"**/*_node_only_spec.ts",
42+
"reflection/es2015_inheritance_fixture.ts",
2543
],
2644
),
2745
# Visible to //:saucelabs_unit_tests_poc target
@@ -73,6 +91,9 @@ ts_library(
7391
jasmine_node_test(
7492
name = "test",
7593
bootstrap = ["//tools/testing:node_es5"],
94+
data = [
95+
":downleveled_es5_fixture",
96+
],
7697
shard_count = 4,
7798
deps = [
7899
":test_lib",
@@ -87,6 +108,7 @@ jasmine_node_test(
87108

88109
karma_web_test_suite(
89110
name = "test_web",
111+
runtime_deps = [":downleveled_es5_fixture"],
90112
deps = [
91113
":test_lib",
92114
],
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
// AMD module name is required so that this file can be loaded in the Karma tests.
10+
/// <amd-module name="angular/packages/core/test/reflection/es5_downleveled_inheritance_fixture" />
11+
12+
class Parent {}
13+
14+
export class ChildNoCtor extends Parent {}
15+
export class ChildWithCtor extends Parent {
16+
constructor() {
17+
super();
18+
}
19+
}
20+
export class ChildNoCtorPrivateProps extends Parent {
21+
x = 10;
22+
}

packages/core/test/reflection/reflector_spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,16 @@ class TestObj {
201201
expect(isDelegateCtor(ChildWithCtor.toString())).toBe(false);
202202
});
203203

204+
// See: https://github.com/angular/angular/issues/38453
205+
it('should support ES2015 downleveled classes', () => {
206+
const {ChildNoCtor, ChildNoCtorPrivateProps, ChildWithCtor} =
207+
require('./es5_downleveled_inheritance_fixture');
208+
209+
expect(isDelegateCtor(ChildNoCtor.toString())).toBe(true);
210+
expect(isDelegateCtor(ChildNoCtorPrivateProps.toString())).toBe(true);
211+
expect(isDelegateCtor(ChildWithCtor.toString())).toBe(false);
212+
});
213+
204214
it('should support ES2015 classes when minified', () => {
205215
// These classes are ES2015 in minified form
206216
const ChildNoCtorMinified = 'class ChildNoCtor extends Parent{}';

0 commit comments

Comments
 (0)