fix(ivy): generate type references to a default import (#29146)

This commit refactors and expands ngtsc's support for generating imports of
values from imports of types (this is used for example when importing a
class referenced in a type annotation in a constructor).

Previously, this logic handled "import {Foo} from" and "import * as foo
from" style imports, but failed on imports of default values ("import
Foo from"). This commit moves the type-to-value logic to a separate file and
expands it to cover the default import case. Doing this also required
augmenting the ImportManager to track default as well as non-default import
generation. The APIs were made a little cleaner at the same time.

PR Close #29146
This commit is contained in:
Alex Rickabaugh
2019-03-06 16:35:08 -08:00
committed by Kara Erickson
parent 37c5a26421
commit b6f6b1178f
14 changed files with 349 additions and 156 deletions

View File

@ -7,4 +7,5 @@
*/
export * from './src/host';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration, typeNodeToValueExpr} from './src/typescript';
export {DEFAULT_EXPORT_NAME, typeNodeToValueExpr} from './src/type_to_value';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/typescript';

View File

@ -0,0 +1,185 @@
/**
* @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 * as ts from 'typescript';
import {TypeValueReference} from './host';
export const DEFAULT_EXPORT_NAME = '*';
/**
* Potentially convert a `ts.TypeNode` to a `TypeValueReference`, which indicates how to use the
* type given in the `ts.TypeNode` in a value position.
*
* This can return `null` if the `typeNode` is `null`, if it does not refer to a symbol with a value
* declaration, or if it is not possible to statically understand.
*/
export function typeToValue(
typeNode: ts.TypeNode | null, checker: ts.TypeChecker): TypeValueReference|null {
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode === null || !ts.isTypeReferenceNode(typeNode)) {
return null;
}
const symbols = resolveTypeSymbols(typeNode, checker);
if (symbols === null) {
return null;
}
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.
if (decl.valueDeclaration === undefined) {
return null;
}
// 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 && isImportSource(firstDecl)) {
const origin = extractModuleAndNameFromImport(firstDecl, symbols.importName);
return {local: false, valueDeclaration: decl.valueDeclaration, ...origin};
} else {
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
return {
local: true,
expression,
};
} else {
return null;
}
}
}
/**
* Attempt to extract a `ts.Expression` that's equivalent to a `ts.TypeNode`, as the two have
* different AST shapes but can reference the same symbols.
*
* This will return `null` if an equivalent expression cannot be constructed.
*/
export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
/**
* Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source.
*
* In the event that the `TypeReference` refers to a locally declared symbol, these will be the
* same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which
* points to the import statement by which the symbol was imported.
*
* In the event `typeRef` refers to a default import, an `importName` will also be returned to
* give the identifier name within the current file by which the import is known.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null {
const typeName = typeRef.typeName;
// typeRefSymbol is the ts.Symbol of the entire type reference.
const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName);
if (typeRefSymbol === undefined) {
return null;
}
// local is the ts.Symbol for the local ts.Identifier for the type.
// If the type is actually locally declared or is imported by name, for example:
// import {Foo} from './foo';
// then it'll be the same as top. If the type is imported via a namespace import, for example:
// import * as foo from './foo';
// and then referenced as:
// constructor(f: foo.Foo)
// then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`.
// This allows tracking of the import behind whatever type reference exists.
let local = typeRefSymbol;
let importName: string|null = null;
// TODO(alxhub): this is technically not correct. The user could have any import type with any
// amount of qualification following the imported type:
//
// import * as foo from 'foo'
// constructor(inject: foo.X.Y.Z)
//
// What we really want is the ability to express the arbitrary operation of `.X.Y.Z` on top of
// whatever import we generate for 'foo'. This logic is sufficient for now, though.
if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) &&
ts.isIdentifier(typeName.right)) {
const localTmp = checker.getSymbolAtLocation(typeName.left);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}
// De-alias the top-level type reference symbol to get the symbol of the actual declaration.
let decl = typeRefSymbol;
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function isImportSource(node: ts.Declaration): node is(
ts.ImportSpecifier | ts.NamespaceImport | ts.ImportClause) {
return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node);
}
function extractModuleAndNameFromImport(
node: ts.ImportSpecifier | ts.NamespaceImport | ts.ImportClause,
localName: string | null): {name: string, moduleName: string} {
let name: string;
let moduleSpecifier: ts.Expression;
switch (node.kind) {
case ts.SyntaxKind.ImportSpecifier:
// The symbol was imported by name, in a ts.ImportSpecifier.
name = (node.propertyName || node.name).text;
moduleSpecifier = node.parent.parent.parent.moduleSpecifier;
break;
case ts.SyntaxKind.NamespaceImport:
// The symbol was imported via a namespace import. In this case, the name to use when
// importing it was extracted by resolveTypeSymbols.
if (localName === null) {
// resolveTypeSymbols() should have extracted the correct local name for the import.
throw new Error(`Debug failure: no local name provided for NamespaceImport`);
}
name = localName;
moduleSpecifier = node.parent.parent.moduleSpecifier;
break;
case ts.SyntaxKind.ImportClause:
name = DEFAULT_EXPORT_NAME;
moduleSpecifier = node.parent.moduleSpecifier;
break;
default:
throw new Error(`Unreachable: ${ts.SyntaxKind[(node as ts.Node).kind]}`);
}
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
return {moduleName, name};
}

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TypeValueReference} from './host';
import {typeToValue} from './type_to_value';
/**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
@ -48,7 +49,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: TypeValueReference|null = null;
let originalTypeNode = node.type || null;
let typeNode = originalTypeNode;
@ -67,68 +68,11 @@ export class TypeScriptReflectionHost implements ReflectionHost {
}
}
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode && ts.isTypeReferenceNode(typeNode)) {
const symbols = resolveTypeSymbols(typeNode, this.checker);
if (symbols !== null) {
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.
if (decl.valueDeclaration !== undefined) {
// 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 && ts.isImportSpecifier(firstDecl)) {
// The symbol was imported by name, in a ts.ImportSpecifier.
const name = (firstDecl.propertyName || firstDecl.name).text;
const moduleSpecifier = firstDecl.parent.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else if (
firstDecl && ts.isNamespaceImport(firstDecl) && symbols.importName !== null) {
// The symbol was imported via a namespace import. In this case, the name to use when
// importing it was extracted by resolveTypeSymbols.
const name = symbols.importName;
const moduleSpecifier = firstDecl.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else {
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
typeValueExpr = {
local: true,
expression,
};
}
}
}
}
}
const typeValueReference = typeToValue(typeNode, this.checker);
return {
name,
nameNode: node.name,
typeValueReference: typeValueExpr,
nameNode: node.name, typeValueReference,
typeNode: originalTypeNode, decorators,
};
});
@ -490,25 +434,6 @@ function parameterName(name: ts.BindingName): string|null {
}
}
export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function propertyNameToString(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
@ -516,48 +441,3 @@ function propertyNameToString(node: ts.PropertyName): string|null {
return null;
}
}
/**
* Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source.
*
* In the event that the `TypeReference` refers to a locally declared symbol, these will be the
* same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which
* points to the import statement by which the symbol was imported.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null {
const typeName = typeRef.typeName;
// typeRefSymbol is the ts.Symbol of the entire type reference.
const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName);
if (typeRefSymbol === undefined) {
return null;
}
// local is the ts.Symbol for the local ts.Identifier for the type.
// If the type is actually locally declared or is imported by name, for example:
// import {Foo} from './foo';
// then it'll be the same as top. If the type is imported via a namespace import, for example:
// import * as foo from './foo';
// and then referenced as:
// constructor(f: foo.Foo)
// then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`.
// This allows tracking of the import behind whatever type reference exists.
let local = typeRefSymbol;
let importName: string|null = null;
if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) &&
ts.isIdentifier(typeName.right)) {
const localTmp = checker.getSymbolAtLocation(typeName.left);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}
// De-alias the top-level type reference symbol to get the symbol of the actual declaration.
let decl = typeRefSymbol;
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
}

View File

@ -147,6 +147,33 @@ describe('reflector', () => {
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
});
it('should reflect an argument from a default import', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
contents: `
export default class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import Bar from './bar';
class Foo {
constructor(bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', {moduleName: './bar', name: '*'});
});
it('should reflect a nullable argument', () => {
const {program} = makeProgram([
{