feat(ivy): compile @Injectable on classes not meant for DI (#28523)
In the past, @Injectable had no side effects and existing Angular code is therefore littered with @Injectable usage on classes which are not intended to be injected. A common example is: @Injectable() class Foo { constructor(private notInjectable: string) {} } and somewhere else: providers: [{provide: Foo, useFactory: ...}) Here, there is no need for Foo to be injectable - indeed, it's impossible for the DI system to create an instance of it, as it has a non-injectable constructor. The provider configures a factory for the DI system to be able to create instances of Foo. Adding @Injectable in Ivy signifies that the class's own constructor, and not a provider, determines how the class will be created. This commit adds logic to compile classes which are marked with @Injectable but are otherwise not injectable, and create an ngInjectableDef field with a factory function that throws an error. This way, existing code in the wild continues to compile, but if someone attempts to use the injectable it will fail with a useful error message. In the case where strictInjectionParameters is set to true, a compile-time error is thrown instead of the runtime error, as ngtsc has enough information to determine when injection couldn't possibly be valid. PR Close #28523
This commit is contained in:

committed by
Misko Hevery

parent
f8b67712bc
commit
d2742cf473
@ -624,6 +624,99 @@ describe('ngtsc behavioral tests', () => {
|
||||
'i0.ɵNgModuleDefWithMeta<TestModule, [typeof TestPipe, typeof TestCmp], never, never>');
|
||||
});
|
||||
|
||||
describe('compiling invalid @Injectables', () => {
|
||||
describe('with strictInjectionParameters = true', () => {
|
||||
it('should give a compile-time error if an invalid @Injectable is used with no arguments',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].messageText).toContain('No suitable injection token for parameter');
|
||||
});
|
||||
|
||||
it('should give a compile-time error if an invalid @Injectable is used with an argument',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].messageText).toContain('No suitable injection token for parameter');
|
||||
});
|
||||
|
||||
it('should not give a compile-time error if an invalid @Injectable is used with useValue',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
useValue: '42',
|
||||
})
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toMatch(/if \(t\).*throw new Error.* else .* '42'/ms);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with strictInjectionParameters = false', () => {
|
||||
it('should compile an @Injectable on a class with a non-injectable constructor', () => {
|
||||
env.tsconfig({strictInjectionParameters: false});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error(');
|
||||
});
|
||||
|
||||
it('should compile an @Injectable provided in the root on a class with a non-injectable constructor',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: false});
|
||||
env.write('test.ts', `
|
||||
import {Injectable} from '@angular/core';
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class Test {
|
||||
constructor(private notInjectable: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = env.getContents('test.js');
|
||||
expect(jsContents).toContain('factory: function Test_Factory(t) { throw new Error(');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('former View Engine AST transform bugs', () => {
|
||||
it('should compile array literals behind conditionals', () => {
|
||||
env.tsconfig();
|
||||
|
Reference in New Issue
Block a user