Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler): handle type references to namespace imports correctly #36106

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Expand Up @@ -1436,7 +1436,8 @@ export class Esm2015ReflectionHost extends TypeScriptReflectionHost implements N
local: false,
valueDeclaration: decl.node,
moduleName: decl.viaModule,
name: decl.node.name.text,
importedName: decl.node.name.text,
nestedPath: null,
};
} else {
typeValueReference = {
Expand Down
2 changes: 1 addition & 1 deletion packages/compiler-cli/ngcc/test/host/util.ts
Expand Up @@ -32,7 +32,7 @@ export function expectTypeValueReferencesForParameters(
}
} else if (param.typeValueReference !== null) {
expect(param.typeValueReference.moduleName).toBe(fromModule!);
expect(param.typeValueReference.name).toBe(expected);
expect(param.typeValueReference.importedName).toBe(expected);
}
}
});
Expand Down
16 changes: 14 additions & 2 deletions packages/compiler-cli/src/ngtsc/annotations/src/util.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Expression, ExternalExpr, LiteralExpr, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import {Expression, ExternalExpr, LiteralExpr, ParseLocation, ParseSourceFile, ParseSourceSpan, R3DependencyMetadata, R3Reference, R3ResolvedDependencyType, ReadPropExpr, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';

import {ErrorCode, FatalDiagnosticError, makeDiagnostic} from '../../diagnostics';
Expand Down Expand Up @@ -138,7 +138,19 @@ export function valueReferenceToExpression(
return new WrappedNodeExpr(valueRef.expression);
} else {
// TODO(alxhub): this cast is necessary because the g3 typescript version doesn't narrow here.
return new ExternalExpr(valueRef as {moduleName: string, name: string});
const ref = valueRef as {
moduleName: string;
importedName: string;
nestedPath: string[]|null;
};
let importExpr: Expression =
new ExternalExpr({moduleName: ref.moduleName, name: ref.importedName});
if (ref.nestedPath !== null) {
for (const property of ref.nestedPath) {
importExpr = new ReadPropExpr(importExpr, property);
}
}
return importExpr;
}
}

Expand Down
18 changes: 17 additions & 1 deletion packages/compiler-cli/src/ngtsc/reflection/src/host.ts
Expand Up @@ -243,8 +243,24 @@ export type TypeValueReference = {
local: true; expression: ts.Expression; defaultImportStatement: ts.ImportDeclaration | null;
}|{
local: false;
name: string;

/**
* The module specifier from which the `importedName` symbol should be imported.
*/
moduleName: string;

/**
* The name of the top-level symbol that is imported from `moduleName`. If `nestedPath` is also
* present, a nested object is being referenced from the top-level symbol.
*/
importedName: string;

/**
* If present, represents the symbol names that are referenced from the top-level import.
* When `null` or empty, the `importedName` itself is the symbol being referenced.
*/
nestedPath: string[]|null;

valueDeclaration: ts.Declaration;
};

Expand Down
168 changes: 94 additions & 74 deletions packages/compiler-cli/src/ngtsc/reflection/src/type_to_value.ts
Expand Up @@ -42,31 +42,76 @@ export function typeToValue(
// 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 && ts.isImportClause(firstDecl) && firstDecl.name !== undefined) {
// This is a default import.
return {
local: true,
// Copying the name here ensures the generated references will be correctly transformed along
// with the import.
expression: ts.updateIdentifier(firstDecl.name),
defaultImportStatement: firstDecl.parent,
};
} else 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,
defaultImportStatement: null,
// Copying the name here ensures the generated references will be correctly transformed
// along with the import.
expression: ts.updateIdentifier(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';

// 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 {
local: false,
valueDeclaration: decl.valueDeclaration,
moduleName,
importedName,
nestedPath
};
} else if (ts.isNamespaceImport(firstDecl)) {
// The import is a namespace import
// import * as Foo from 'foo';

if (symbols.symbolNames.length === 1) {
// The type refers to the namespace itself, which cannot be represented as a value.
return null;
}

// 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 {
local: false,
valueDeclaration: decl.valueDeclaration,
moduleName,
importedName,
nestedPath
};
} else {
return null;
}
}

// 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 {
local: true,
expression,
defaultImportStatement: null,
};
} else {
return null;
}
}

/**
Expand All @@ -88,47 +133,51 @@ export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
*
* 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.
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifier`
* 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.
* All symbol names that make up the type reference are returned left-to-right into the
* `symbolNames` array, which is guaranteed to include at least one entry.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string|null}|null {
{local: ts.Symbol, decl: ts.Symbol, symbolNames: string[]}|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.
// `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:
// then it'll be the same as `typeRefSymbol`.
//
// 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.
// then `local` will be the `ts.Symbol` of `foo`, whereas `typeRefSymbol` 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);
// Destructure a name like `foo.X.Y.Z` as follows:
// - in `leftMost`, the `ts.Identifier` of the left-most name (`foo`) in the qualified name.
// This identifier is used to resolve the `ts.Symbol` for `local`.
// - in `symbolNames`, all names involved in the qualified path, or a single symbol name if the
// type is not qualified.
let leftMost = typeName;
const symbolNames: string[] = [];
while (ts.isQualifiedName(leftMost)) {
symbolNames.unshift(leftMost.right.text);
leftMost = leftMost.left;
}
symbolNames.unshift(leftMost.text);

if (leftMost !== typeName) {
const localTmp = checker.getSymbolAtLocation(leftMost);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}

Expand All @@ -137,7 +186,7 @@ function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeCheck
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
return {local, decl, symbolNames};
}

function entityNameToValue(node: ts.EntityName): ts.Expression|null {
Expand All @@ -151,38 +200,9 @@ function entityNameToValue(node: ts.EntityName): ts.Expression|null {
}
}

function isImportSource(node: ts.Declaration): node is(ts.ImportSpecifier | ts.NamespaceImport) {
return ts.isImportSpecifier(node) || ts.isNamespaceImport(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;
default:
throw new Error(`Unreachable: ${ts.SyntaxKind[(node as ts.Node).kind]}`);
}

if (!ts.isStringLiteral(moduleSpecifier)) {
function extractModuleName(node: ts.ImportDeclaration): string {
if (!ts.isStringLiteral(node.moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
return {moduleName, name};
return node.moduleSpecifier.text;
}
Expand Up @@ -464,7 +464,7 @@ runInEachFileSystem(() => {
expect(argExpressionToString(param.typeValueReference.expression)).toEqual(type);
} else if (!param.typeValueReference.local && typeof type !== 'string') {
expect(param.typeValueReference.moduleName).toEqual(type.moduleName);
expect(param.typeValueReference.name).toEqual(type.name);
expect(param.typeValueReference.importedName).toEqual(type.name);
} else {
return fail(`Mismatch between typeValueReference and expected type: ${param.name} / ${
param.typeValueReference.local}`);
Expand Down