-
Notifications
You must be signed in to change notification settings - Fork 26.9k
Closed
Labels
area: compilerIssues related to `ngc`, Angular's template compilerIssues related to `ngc`, Angular's template compilerrefactoringIssue that involves refactoring or code-cleanupIssue that involves refactoring or code-cleanupstate: has PR
Milestone
Description
For upcoming TS 4.5 our typeToValue logic needs to account for microsoft/TypeScript#45998, where we should report an error if an import specifier is used that has a type-only modifier for a DI token.
angular/packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts
Lines 20 to 137 in a32a317
| export function typeToValue( | |
| typeNode: ts.TypeNode|null, checker: ts.TypeChecker): TypeValueReference { | |
| // It's not possible to get a value expression if the parameter doesn't even have a type. | |
| if (typeNode === null) { | |
| return missingType(); | |
| } | |
| if (!ts.isTypeReferenceNode(typeNode)) { | |
| return unsupportedType(typeNode); | |
| } | |
| const symbols = resolveTypeSymbols(typeNode, checker); | |
| if (symbols === null) { | |
| return unknownReference(typeNode); | |
| } | |
| const {local, decl} = symbols; | |
| // It's only valid to convert a type reference to a value reference if the type actually | |
| // has a value declaration associated with it. Note that const enums are an exception, | |
| // because while they do have a value declaration, they don't exist at runtime. | |
| if (decl.valueDeclaration === undefined || decl.flags & ts.SymbolFlags.ConstEnum) { | |
| let typeOnlyDecl: ts.Declaration|null = null; | |
| if (decl.declarations !== undefined && decl.declarations.length > 0) { | |
| typeOnlyDecl = decl.declarations[0]; | |
| } | |
| return noValueDeclaration(typeNode, typeOnlyDecl); | |
| } | |
| // The type points to a valid value declaration. Rewrite the TypeReference into an | |
| // Expression which references the value pointed to by the TypeReference, if possible. | |
| // Look at the local `ts.Symbol`'s declarations and see if it comes from an import | |
| // statement. If so, extract the module specifier and the name of the imported type. | |
| const firstDecl = local.declarations && local.declarations[0]; | |
| if (firstDecl !== undefined) { | |
| if (ts.isImportClause(firstDecl) && firstDecl.name !== undefined) { | |
| // This is a default import. | |
| // import Foo from 'foo'; | |
| if (firstDecl.isTypeOnly) { | |
| // Type-only imports cannot be represented as value. | |
| return typeOnlyImport(typeNode, firstDecl); | |
| } | |
| return { | |
| kind: TypeValueReferenceKind.LOCAL, | |
| expression: firstDecl.name, | |
| defaultImportStatement: firstDecl.parent, | |
| }; | |
| } else if (ts.isImportSpecifier(firstDecl)) { | |
| // The symbol was imported by name | |
| // import {Foo} from 'foo'; | |
| // or | |
| // import {Foo as Bar} from 'foo'; | |
| if (firstDecl.parent.parent.isTypeOnly) { | |
| // Type-only imports cannot be represented as value. | |
| return typeOnlyImport(typeNode, firstDecl.parent.parent); | |
| } | |
| // Determine the name to import (`Foo`) from the import specifier, as the symbol names of | |
| // the imported type could refer to a local alias (like `Bar` in the example above). | |
| const importedName = (firstDecl.propertyName || firstDecl.name).text; | |
| // The first symbol name refers to the local name, which is replaced by `importedName` above. | |
| // Any remaining symbol names make up the complete path to the value. | |
| const [_localName, ...nestedPath] = symbols.symbolNames; | |
| const moduleName = extractModuleName(firstDecl.parent.parent.parent); | |
| return { | |
| kind: TypeValueReferenceKind.IMPORTED, | |
| valueDeclaration: decl.valueDeclaration, | |
| moduleName, | |
| importedName, | |
| nestedPath | |
| }; | |
| } else if (ts.isNamespaceImport(firstDecl)) { | |
| // The import is a namespace import | |
| // import * as Foo from 'foo'; | |
| if (firstDecl.parent.isTypeOnly) { | |
| // Type-only imports cannot be represented as value. | |
| return typeOnlyImport(typeNode, firstDecl.parent); | |
| } | |
| if (symbols.symbolNames.length === 1) { | |
| // The type refers to the namespace itself, which cannot be represented as a value. | |
| return namespaceImport(typeNode, firstDecl.parent); | |
| } | |
| // The first symbol name refers to the local name of the namespace, which is is discarded | |
| // as a new namespace import will be generated. This is followed by the symbol name that needs | |
| // to be imported and any remaining names that constitute the complete path to the value. | |
| const [_ns, importedName, ...nestedPath] = symbols.symbolNames; | |
| const moduleName = extractModuleName(firstDecl.parent.parent); | |
| return { | |
| kind: TypeValueReferenceKind.IMPORTED, | |
| valueDeclaration: decl.valueDeclaration, | |
| moduleName, | |
| importedName, | |
| nestedPath | |
| }; | |
| } | |
| } | |
| // If the type is not imported, the type reference can be converted into an expression as is. | |
| const expression = typeNodeToValueExpr(typeNode); | |
| if (expression !== null) { | |
| return { | |
| kind: TypeValueReferenceKind.LOCAL, | |
| expression, | |
| defaultImportStatement: null, | |
| }; | |
| } else { | |
| return unsupportedType(typeNode); | |
| } | |
| } |
For example the following should be invalid:
import { type MyService } from './service.ts';
@Injectable()
export class OtherService {
constructor(private service: MyService) {}
}The compiler is already capable of reporting errors for type-only import statements, it just needs to be expanded for type-only import specifiers.
Metadata
Metadata
Assignees
Labels
area: compilerIssues related to `ngc`, Angular's template compilerIssues related to `ngc`, Angular's template compilerrefactoringIssue that involves refactoring or code-cleanupIssue that involves refactoring or code-cleanupstate: has PR