fix(ngcc): fix compilation of ChangeDetectorRef in pipe constructors (#38892)

In #38666 we changed how ngcc deals with type expressions, where it
would now always emit the original type expression into the generated
code as a "local" type value reference instead of synthesizing new
imports using an "imported" type value reference. This was done as a fix
to properly deal with renamed symbols, however it turns out that the
compiler has special handling for certain imported symbols, e.g.
`ChangeDetectorRef` from `@angular/core`. The "local" type value
reference prevented this special logic from being hit, resulting in
incorrect compilation of pipe factories.

This commit fixes the issue by manually inspecting the import of the
type expression, in order to return an "imported" type value reference.
By manually inspecting the import we continue to handle renamed symbols.

Fixes #38883

PR Close #38892
This commit is contained in:
JoostK 2020-09-17 20:40:35 +02:00 committed by Misko Hevery
parent d795a00137
commit e4424863c2
7 changed files with 92 additions and 19 deletions

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system'; import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {Logger} from '../../../src/ngtsc/logging'; import {Logger} from '../../../src/ngtsc/logging';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection'; import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, Import, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference, TypeValueReferenceKind, ValueUnavailableKind} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util'; import {isWithinPackage} from '../analysis/util';
import {BundleProgram} from '../packages/bundle_program'; import {BundleProgram} from '../packages/bundle_program';
import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils'; import {findAll, getNameText, hasNameIdentifier, isDefined, stripDollarSuffix} from '../utils';
@ -1608,10 +1608,11 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
/** /**
* Compute the `TypeValueReference` for the given `typeExpression`. * Compute the `TypeValueReference` for the given `typeExpression`.
* *
* In ngcc, all the `typeExpression` are guaranteed to be "values" because it is working in JS and * Although `typeExpression` is a valid `ts.Expression` that could be emitted directly into the
* not TS. This means that the TS compiler is not going to remove the "type" import and so we can * generated code, ngcc still needs to resolve the declaration and create an `IMPORTED` type
* always use a LOCAL `TypeValueReference` kind, rather than trying to force an additional import * value reference as the compiler has specialized handling for some symbols, for example
* for non-local expressions. * `ChangeDetectorRef` from `@angular/core`. Such an `IMPORTED` type value reference will result
* in a newly generated namespace import, instead of emitting the original `typeExpression` as is.
*/ */
private typeToValue(typeExpression: ts.Expression|null): TypeValueReference { private typeToValue(typeExpression: ts.Expression|null): TypeValueReference {
if (typeExpression === null) { if (typeExpression === null) {
@ -1621,6 +1622,9 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}; };
} }
const imp = this.getImportOfExpression(typeExpression);
const decl = this.getDeclarationOfExpression(typeExpression);
if (imp === null || decl === null || decl.node === null) {
return { return {
kind: TypeValueReferenceKind.LOCAL, kind: TypeValueReferenceKind.LOCAL,
expression: typeExpression, expression: typeExpression,
@ -1628,6 +1632,32 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
}; };
} }
return {
kind: TypeValueReferenceKind.IMPORTED,
valueDeclaration: decl.node,
moduleName: imp.from,
importedName: imp.name,
nestedPath: null,
};
}
/**
* Determines where the `expression` is imported from.
*
* @param expression the expression to determine the import details for.
* @returns the `Import` for the expression, or `null` if the expression is not imported or the
* expression syntax is not supported.
*/
private getImportOfExpression(expression: ts.Expression): Import|null {
if (ts.isIdentifier(expression)) {
return this.getImportOfIdentifier(expression);
} else if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.name)) {
return this.getImportOfIdentifier(expression.name);
} else {
return null;
}
}
/** /**
* Get the parameter type and decorators for the constructor of a class, * Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static property of the class. * where the information is stored on a static property of the class.

View File

@ -1211,7 +1211,7 @@ exports.MissingClass2 = MissingClass2;
}); });
describe('getConstructorParameters', () => { describe('getConstructorParameters', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types', it('should retain imported name for type value references for decorated constructor parameter types',
() => { () => {
const files = [ const files = [
{ {
@ -1271,7 +1271,7 @@ exports.MissingClass2 = MissingClass2;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']); expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters( expectTypeValueReferencesForParameters(
parameters, ['shared.Baz', 'local.External', 'SameFile']); parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
}); });
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {

View File

@ -1140,7 +1140,7 @@ runInEachFileSystem(() => {
}); });
describe('getConstructorParameters()', () => { describe('getConstructorParameters()', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types', it('should retain imported name for type value references for decorated constructor parameter types',
() => { () => {
const files = [ const files = [
{ {
@ -1188,7 +1188,8 @@ runInEachFileSystem(() => {
const parameters = host.getConstructorParameters(classNode)!; const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']); expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']); expectTypeValueReferencesForParameters(
parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
}); });
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {
@ -1205,7 +1206,8 @@ runInEachFileSystem(() => {
'_viewContainer', '_template', 'injected' '_viewContainer', '_template', 'injected'
]); ]);
expectTypeValueReferencesForParameters( expectTypeValueReferencesForParameters(
parameters, ['ViewContainerRef', 'TemplateRef', null]); parameters, ['ViewContainerRef', 'TemplateRef', null],
['@angular/core', '@angular/core', null]);
}); });
it('should accept `ctorParameters` as an array', () => { it('should accept `ctorParameters` as an array', () => {

View File

@ -1252,7 +1252,7 @@ runInEachFileSystem(() => {
}); });
describe('getConstructorParameters()', () => { describe('getConstructorParameters()', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types', it('should retain imported name for type value references for decorated constructor parameter types',
() => { () => {
const files = [ const files = [
{ {
@ -1310,7 +1310,8 @@ runInEachFileSystem(() => {
const parameters = host.getConstructorParameters(classNode)!; const parameters = host.getConstructorParameters(classNode)!;
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']); expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters(parameters, ['Baz', 'External', 'SameFile']); expectTypeValueReferencesForParameters(
parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
}); });
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {

View File

@ -1332,7 +1332,7 @@ runInEachFileSystem(() => {
}); });
describe('getConstructorParameters', () => { describe('getConstructorParameters', () => {
it('should always specify LOCAL type value references for decorated constructor parameter types', it('should retain imported name for type value references for decorated constructor parameter types',
() => { () => {
const files = [ const files = [
{ {
@ -1369,7 +1369,7 @@ runInEachFileSystem(() => {
name: _('/main.js'), name: _('/main.js'),
contents: ` contents: `
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('shared-lib), require('./local')) : typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('shared-lib'), require('./local')) :
typeof define === 'function' && define.amd ? define('main', ['exports', 'shared-lib', './local'], factory) : typeof define === 'function' && define.amd ? define('main', ['exports', 'shared-lib', './local'], factory) :
(factory(global.main, global.shared, global.local)); (factory(global.main, global.shared, global.local));
}(this, (function (exports, shared, local) { 'use strict'; }(this, (function (exports, shared, local) { 'use strict';
@ -1401,7 +1401,7 @@ runInEachFileSystem(() => {
expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']); expect(parameters.map(p => p.name)).toEqual(['arg1', 'arg2', 'arg3']);
expectTypeValueReferencesForParameters( expectTypeValueReferencesForParameters(
parameters, ['shared.Baz', 'local.External', 'SameFile']); parameters, ['Baz', 'External', 'SameFile'], ['shared-lib', './local', null]);
}); });
it('should find the decorated constructor parameters', () => { it('should find the decorated constructor parameters', () => {

View File

@ -576,6 +576,35 @@ runInEachFileSystem(() => {
`TestClass.ɵprov = ɵngcc0.ɵɵdefineInjectable({`); `TestClass.ɵprov = ɵngcc0.ɵɵdefineInjectable({`);
}); });
// https://github.com/angular/angular/issues/38883
it('should recognize ChangeDetectorRef as special symbol for pipes', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `
import {ChangeDetectorRef, Pipe, PipeTransform} from '@angular/core';
@Pipe({
name: 'myTestPipe'
})
export class TestClass implements PipeTransform {
constructor(cdr: ChangeDetectorRef) {}
transform(value: any) { return value; }
}
`,
});
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015'],
});
const jsContents = fs.readFile(_(`/node_modules/test-package/index.js`));
expect(jsContents)
.toContain(
`TestClass.ɵfac = function TestClass_Factory(t) { ` +
`return new (t || TestClass)(ɵngcc0.ɵɵinjectPipeChangeDetectorRef()); };`);
});
it('should use the correct type name in typings files when an export has a different name in source files', it('should use the correct type name in typings files when an export has a different name in source files',
() => { () => {
// We need to make sure that changes to the typings files use the correct name // We need to make sure that changes to the typings files use the correct name

View File

@ -99,7 +99,14 @@ function compileIntoFlatPackage(
program.emit(); program.emit();
}; };
emit({declaration: true, module: options.module, target: options.target, lib: []}); emit({
declaration: true,
emitDecoratorMetadata: true,
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: options.module,
target: options.target,
lib: [],
});
// Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file. // Copy over the JS and .d.ts files, and add a .metadata.json for each .d.ts file.
for (const file of rootNames) { for (const file of rootNames) {
@ -152,7 +159,9 @@ export function compileIntoApf(
compileFs.ensureDir(compileFs.resolve('esm2015')); compileFs.ensureDir(compileFs.resolve('esm2015'));
emit({ emit({
declaration: true, declaration: true,
emitDecoratorMetadata: true,
outDir: './esm2015', outDir: './esm2015',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.ESNext, module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ES2015, target: ts.ScriptTarget.ES2015,
lib: [], lib: [],
@ -178,7 +187,9 @@ export function compileIntoApf(
compileFs.ensureDir(compileFs.resolve('esm5')); compileFs.ensureDir(compileFs.resolve('esm5'));
emit({ emit({
declaration: false, declaration: false,
emitDecoratorMetadata: true,
outDir: './esm5', outDir: './esm5',
moduleResolution: ts.ModuleResolutionKind.NodeJs,
module: ts.ModuleKind.ESNext, module: ts.ModuleKind.ESNext,
target: ts.ScriptTarget.ES5, target: ts.ScriptTarget.ES5,
lib: [], lib: [],