fix(ngcc): do not output duplicate ɵprov properties (#34085)

Previously, the Angular AOT compiler would always add a
`ɵprov` to injectables. But in ngcc this resulted in duplicate `ɵprov`
properties since published libraries already have this property.

Now in ngtsc, trying to add a duplicate `ɵprov` property is an error,
while in ngcc the additional property is silently not added.

// FW-1750

PR Close #34085
This commit is contained in:
Pete Bacon Darwin
2019-11-27 14:17:57 +00:00
committed by Miško Hevery
parent 658087be7e
commit 2fb9b7ff1b
7 changed files with 155 additions and 10 deletions

View File

@ -32,7 +32,14 @@ export class InjectableDecoratorHandler implements
DecoratorHandler<InjectableHandlerData, Decorator> {
constructor(
private reflector: ReflectionHost, private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private strictCtorDeps: boolean) {}
private isCore: boolean, private strictCtorDeps: boolean,
/**
* What to do if the injectable already contains a ɵprov property.
*
* If true then an error diagnostic is reported.
* If false then there is no error and a new ɵprov property is not added.
*/
private errorOnDuplicateProv = true) {}
readonly precedence = HandlerPrecedence.SHARED;
@ -93,11 +100,18 @@ export class InjectableDecoratorHandler implements
results.push(factoryRes);
}
results.push({
name: 'ɵprov',
initializer: res.expression, statements,
type: res.type,
});
const ɵprov = this.reflector.getMembersOfClass(node).find(member => member.name === 'ɵprov');
if (ɵprov !== undefined && this.errorOnDuplicateProv) {
throw new FatalDiagnosticError(
ErrorCode.INJECTABLE_DUPLICATE_PROV, ɵprov.nameNode || ɵprov.node || node,
'Injectables cannot contain a static ɵprov property, because the compiler is going to generate one.');
}
if (ɵprov === undefined) {
// Only add a new ɵprov if there is not one already
results.push({name: 'ɵprov', initializer: res.expression, statements, type: res.type});
}
return results;
}

View File

@ -0,0 +1,87 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ErrorCode, FatalDiagnosticError, ngErrorCode} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NOOP_DEFAULT_IMPORT_RECORDER} from '../../imports';
import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {getDeclaration, makeProgram} from '../../testing';
import {InjectableDecoratorHandler} from '../src/injectable';
runInEachFileSystem(() => {
describe('InjectableDecoratorHandler', () => {
describe('compile()', () => {
it('should produce a diagnostic when injectable already has a static ɵprov property (with errorOnDuplicateProv true)',
() => {
const {handler, TestClass, ɵprov, analysis} =
setupHandler(/* errorOnDuplicateProv */ true);
try {
handler.compile(TestClass, analysis);
return fail('Compilation should have failed');
} catch (err) {
if (!(err instanceof FatalDiagnosticError)) {
return fail('Error should be a FatalDiagnosticError');
}
const diag = err.toDiagnostic();
expect(diag.code).toEqual(ngErrorCode(ErrorCode.INJECTABLE_DUPLICATE_PROV));
expect(diag.file.fileName.endsWith('entry.ts')).toBe(true);
expect(diag.start).toBe(ɵprov.nameNode !.getStart());
}
});
it('should not add new ɵprov property when injectable already has one (with errorOnDuplicateProv false)',
() => {
const {handler, TestClass, ɵprov, analysis} =
setupHandler(/* errorOnDuplicateProv */ false);
const res = handler.compile(TestClass, analysis);
expect(res).not.toContain(jasmine.objectContaining({name: 'ɵprov'}));
});
});
});
});
function setupHandler(errorOnDuplicateProv: boolean) {
const ENTRY_FILE = absoluteFrom('/entry.ts');
const ANGULAR_CORE = absoluteFrom('/node_modules/@angular/core/index.d.ts');
const {program} = makeProgram([
{
name: ANGULAR_CORE,
contents: 'export const Injectable: any; export const ɵɵdefineInjectable: any',
},
{
name: ENTRY_FILE,
contents: `
import {Injectable, ɵɵdefineInjectable} from '@angular/core';
export const TestClassToken = 'TestClassToken';
@Injectable({providedIn: 'module'})
export class TestClass {
static ɵprov = ɵɵdefineInjectable({ factory: () => {}, token: TestClassToken, providedIn: "module" });
}`
},
]);
const checker = program.getTypeChecker();
const reflectionHost = new TypeScriptReflectionHost(checker);
const handler = new InjectableDecoratorHandler(
reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, /* isCore */ false,
/* strictCtorDeps */ false, errorOnDuplicateProv);
const TestClass = getDeclaration(program, ENTRY_FILE, 'TestClass', isNamedClassDeclaration);
const ɵprov = reflectionHost.getMembersOfClass(TestClass).find(member => member.name === 'ɵprov');
if (ɵprov === undefined) {
throw new Error('TestClass did not contain a `ɵprov` member');
}
const detected = handler.detect(TestClass, reflectionHost.getDecoratorsOfDeclaration(TestClass));
if (detected === undefined) {
throw new Error('Failed to recognize TestClass');
}
const {analysis} = handler.analyze(TestClass, detected.metadata);
if (analysis === undefined) {
throw new Error('Failed to analyze TestClass');
}
return {handler, TestClass, ɵprov, analysis};
}

View File

@ -94,6 +94,11 @@ export enum ErrorCode {
* No matching pipe was found for a
*/
MISSING_PIPE = 8004,
/**
* An injectable already has a `ɵprov` property.
*/
INJECTABLE_DUPLICATE_PROV = 9001
}
export function ngErrorCode(code: ErrorCode): number {