fix(compiler-cli): avoid creating value expressions for symbols from type-only imports (#37912)
In TypeScript 3.8 support was added for type-only imports, which only brings in the symbol as a type, not their value. The Angular compiler did not yet take the type-only keyword into account when representing symbols in type positions as value expressions. The class metadata that the compiler emits would include the value expression for its parameter types, generating actual imports as necessary. For type-only imports this should not be done, as it introduces an actual import of the module that was originally just a type-only import. This commit lets the compiler deal with type-only imports specially, preventing a value expression from being created. Fixes #37900 PR Close #37912
This commit is contained in:
@ -2079,7 +2079,13 @@ runInEachFileSystem(os => {
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].messageText).toContain('No suitable injection token for parameter');
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(errors[0].relatedInformation!.length).toBe(1);
|
||||
expect(errors[0].relatedInformation![0].messageText)
|
||||
.toBe('This type is not supported as injection token.');
|
||||
});
|
||||
|
||||
it('should give a compile-time error if an invalid @Injectable is used with an argument',
|
||||
@ -2096,9 +2102,139 @@ runInEachFileSystem(os => {
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].messageText).toContain('No suitable injection token for parameter');
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'notInjectable' of class 'Test'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(errors[0].relatedInformation!.length).toBe(1);
|
||||
expect(errors[0].relatedInformation![0].messageText)
|
||||
.toBe('This type is not supported as injection token.');
|
||||
});
|
||||
|
||||
it('should report an error when using a type-only import as injection token', () => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write(`types.ts`, `
|
||||
export class TypeOnly {}
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable} from '@angular/core';
|
||||
import type {TypeOnly} from './types';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(param: TypeOnly) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'param' of class 'MyService'.\n` +
|
||||
` Consider changing the type-only import to a regular import, ` +
|
||||
`or use the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation!.length).toBe(2);
|
||||
expect(diags[0].relatedInformation![0].messageText)
|
||||
.toBe(
|
||||
'This type is imported using a type-only import, ' +
|
||||
'which prevents it from being usable as an injection token.');
|
||||
expect(diags[0].relatedInformation![1].messageText)
|
||||
.toBe('The type-only import occurs here.');
|
||||
});
|
||||
|
||||
it('should report an error when using a primitive type as injection token', () => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(param: string) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'param' of class 'MyService'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation!.length).toBe(1);
|
||||
expect(diags[0].relatedInformation![0].messageText)
|
||||
.toBe('This type is not supported as injection token.');
|
||||
});
|
||||
|
||||
it('should report an error when using a union type as injection token', () => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
export class ClassA {}
|
||||
export class ClassB {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(param: ClassA|ClassB) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'param' of class 'MyService'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation!.length).toBe(1);
|
||||
expect(diags[0].relatedInformation![0].messageText)
|
||||
.toBe('This type is not supported as injection token.');
|
||||
});
|
||||
|
||||
it('should report an error when using an interface as injection token', () => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
export interface Interface {}
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(param: Interface) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'param' of class 'MyService'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation!.length).toBe(2);
|
||||
expect(diags[0].relatedInformation![0].messageText)
|
||||
.toBe('This type does not have a value, so it cannot be used as injection token.');
|
||||
expect(diags[0].relatedInformation![1].messageText).toBe('The type is declared here.');
|
||||
});
|
||||
|
||||
it('should report an error when no type is present', () => {
|
||||
env.tsconfig({strictInjectionParameters: true, noImplicitAny: false});
|
||||
env.write(`test.ts`, `
|
||||
import {Injectable} from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MyService {
|
||||
constructor(param) {}
|
||||
}
|
||||
`);
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'param' of class 'MyService'.\n` +
|
||||
` Consider adding a type to the parameter or ` +
|
||||
`use the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not give a compile-time error if an invalid @Injectable is used with useValue',
|
||||
() => {
|
||||
env.tsconfig({strictInjectionParameters: true});
|
||||
@ -2240,7 +2376,8 @@ runInEachFileSystem(os => {
|
||||
|
||||
const errors = env.driveDiagnostics();
|
||||
expect(errors.length).toBe(1);
|
||||
expect(errors[0].messageText).toContain('No suitable injection token for parameter');
|
||||
expect(ts.flattenDiagnosticMessageText(errors[0].messageText, '\n'))
|
||||
.toContain('No suitable injection token for parameter');
|
||||
});
|
||||
});
|
||||
|
||||
@ -4194,9 +4331,16 @@ runInEachFileSystem(os => {
|
||||
|
||||
const diags = env.driveDiagnostics();
|
||||
expect(diags.length).toBe(1);
|
||||
expect(diags[0].messageText)
|
||||
expect(ts.flattenDiagnosticMessageText(diags[0].messageText, '\n'))
|
||||
.toBe(
|
||||
`No suitable injection token for parameter 'foo' of class 'MyService'.\nFound Foo`);
|
||||
`No suitable injection token for parameter 'foo' of class 'MyService'.\n` +
|
||||
` Consider using the @Inject decorator to specify an injection token.`);
|
||||
expect(diags[0].relatedInformation!.length).toBe(2);
|
||||
expect(diags[0].relatedInformation![0].messageText)
|
||||
.toBe(
|
||||
'This type corresponds with a namespace, which cannot be used as injection token.');
|
||||
expect(diags[0].relatedInformation![1].messageText)
|
||||
.toBe('The namespace import occurs here.');
|
||||
});
|
||||
});
|
||||
|
||||
@ -4225,6 +4369,43 @@ runInEachFileSystem(os => {
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should use `undefined` in setClassMetadata if types originate from type-only imports',
|
||||
() => {
|
||||
env.write(`types.ts`, `
|
||||
export default class {}
|
||||
export class TypeOnly {}
|
||||
`);
|
||||
env.write(`test.ts`, `
|
||||
import {Component, Inject, Injectable} from '@angular/core';
|
||||
import type DefaultImport from './types';
|
||||
import type {TypeOnly} from './types';
|
||||
import type * as types from './types';
|
||||
|
||||
@Component({
|
||||
selector: 'some-comp',
|
||||
template: '...',
|
||||
})
|
||||
export class SomeComp {
|
||||
constructor(
|
||||
@Inject('token') namedImport: TypeOnly,
|
||||
@Inject('token') defaultImport: DefaultImport,
|
||||
@Inject('token') namespacedImport: types.TypeOnly,
|
||||
) {}
|
||||
}
|
||||
`);
|
||||
|
||||
env.driveMain();
|
||||
const jsContents = trim(env.getContents('test.js'));
|
||||
// Module specifier for type-only import should not be emitted
|
||||
expect(jsContents).not.toContain('./types');
|
||||
// Default type-only import should not be emitted
|
||||
expect(jsContents).not.toContain('DefaultImport');
|
||||
// Named type-only import should not be emitted
|
||||
expect(jsContents).not.toContain('TypeOnly');
|
||||
// The parameter type in class metadata should be undefined
|
||||
expect(jsContents).toMatch(setClassMetadataRegExp('type: undefined'));
|
||||
});
|
||||
|
||||
it('should not throw in case whitespaces and HTML comments are present inside <ng-content>',
|
||||
() => {
|
||||
env.write('test.ts', `
|
||||
|
Reference in New Issue
Block a user