feat(ivy): reference external classes by their exported name (#27743)
Previously, ngtsc would assume that a given directive/pipe being imported from an external package was importable using the same name by which it was declared. This isn't always true; sometimes a package will export a directive under a different name. For example, Angular frequently prefixes directive names with the 'ɵ' character to indicate that they're part of the package's private API, and not for public consumption. This commit introduces the TsReferenceResolver class which, given a declaration to import and a module name to import it from, can determine the exported name of the declared class within the module. This allows ngtsc to pick the correct name by which to import the class instead of making assumptions about how it was exported. This resolver is used to select a correct symbol name when creating an AbsoluteReference. FW-517 #resolve FW-536 #resolve PR Close #27743
This commit is contained in:

committed by
Kara Erickson

parent
0b9094ec63
commit
1c39ad38d3
@ -8,7 +8,7 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Reference} from '../../imports';
|
||||
import {Reference, ReferenceResolver} from '../../imports';
|
||||
import {ReflectionHost} from '../../reflection';
|
||||
|
||||
import {StaticInterpreter} from './interpreter';
|
||||
@ -19,12 +19,15 @@ export type ForeignFunctionResolver =
|
||||
ts.Expression | null;
|
||||
|
||||
export class PartialEvaluator {
|
||||
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
|
||||
constructor(
|
||||
private host: ReflectionHost, private checker: ts.TypeChecker,
|
||||
private refResolver: ReferenceResolver) {}
|
||||
|
||||
evaluate(expr: ts.Expression, foreignFunctionResolver?: ForeignFunctionResolver): ResolvedValue {
|
||||
const interpreter = new StaticInterpreter(this.host, this.checker);
|
||||
const interpreter = new StaticInterpreter(this.host, this.checker, this.refResolver);
|
||||
return interpreter.visit(expr, {
|
||||
absoluteModuleName: null,
|
||||
resolutionContext: expr.getSourceFile().fileName,
|
||||
scope: new Map<ts.ParameterDeclaration, ResolvedValue>(), foreignFunctionResolver,
|
||||
});
|
||||
}
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteReference, NodeReference, Reference, ResolvedReference} from '../../imports';
|
||||
import {ReflectionHost} from '../../reflection';
|
||||
import {AbsoluteReference, NodeReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
|
||||
import {Declaration, ReflectionHost} from '../../reflection';
|
||||
|
||||
import {ArraySliceBuiltinFn} from './builtin';
|
||||
import {BuiltinFn, DYNAMIC_VALUE, EnumValue, ResolvedValue, ResolvedValueArray, ResolvedValueMap, isDynamicValue} from './result';
|
||||
@ -61,7 +61,16 @@ const UNARY_OPERATORS = new Map<ts.SyntaxKind, (a: any) => any>([
|
||||
]);
|
||||
|
||||
interface Context {
|
||||
/**
|
||||
* The module name (if any) which was used to reach the currently resolving symbols.
|
||||
*/
|
||||
absoluteModuleName: string|null;
|
||||
|
||||
/**
|
||||
* A file name representing the context in which the current `absoluteModuleName`, if any, was
|
||||
* resolved.
|
||||
*/
|
||||
resolutionContext: string;
|
||||
scope: Scope;
|
||||
foreignFunctionResolver?
|
||||
(ref: Reference<ts.FunctionDeclaration|ts.MethodDeclaration|ts.FunctionExpression>,
|
||||
@ -69,7 +78,9 @@ interface Context {
|
||||
}
|
||||
|
||||
export class StaticInterpreter {
|
||||
constructor(private host: ReflectionHost, private checker: ts.TypeChecker) {}
|
||||
constructor(
|
||||
private host: ReflectionHost, private checker: ts.TypeChecker,
|
||||
private refResolver: ReferenceResolver) {}
|
||||
|
||||
visit(node: ts.Expression, context: Context): ResolvedValue {
|
||||
return this.visitExpression(node, context);
|
||||
@ -203,8 +214,8 @@ export class StaticInterpreter {
|
||||
if (decl === null) {
|
||||
return DYNAMIC_VALUE;
|
||||
}
|
||||
const result = this.visitDeclaration(
|
||||
decl.node, {...context, absoluteModuleName: decl.viaModule || context.absoluteModuleName});
|
||||
const result =
|
||||
this.visitDeclaration(decl.node, {...context, ...joinModuleContext(context, node, decl)});
|
||||
if (result instanceof Reference) {
|
||||
result.addIdentifier(node);
|
||||
}
|
||||
@ -292,10 +303,10 @@ export class StaticInterpreter {
|
||||
}
|
||||
const map = new Map<string, ResolvedValue>();
|
||||
declarations.forEach((decl, name) => {
|
||||
const value = this.visitDeclaration(decl.node, {
|
||||
...context,
|
||||
absoluteModuleName: decl.viaModule || context.absoluteModuleName,
|
||||
});
|
||||
const value = this.visitDeclaration(
|
||||
decl.node, {
|
||||
...context, ...joinModuleContext(context, node, decl),
|
||||
});
|
||||
map.set(name, value);
|
||||
});
|
||||
return map;
|
||||
@ -381,12 +392,16 @@ export class StaticInterpreter {
|
||||
|
||||
// If the function is declared in a different file, resolve the foreign function expression
|
||||
// using the absolute module name of that file (if any).
|
||||
let absoluteModuleName: string|null = context.absoluteModuleName;
|
||||
if (lhs instanceof NodeReference || lhs instanceof AbsoluteReference) {
|
||||
absoluteModuleName = lhs.moduleName || absoluteModuleName;
|
||||
if ((lhs instanceof NodeReference || lhs instanceof AbsoluteReference) &&
|
||||
lhs.moduleName !== null) {
|
||||
context = {
|
||||
...context,
|
||||
absoluteModuleName: lhs.moduleName,
|
||||
resolutionContext: node.getSourceFile().fileName,
|
||||
};
|
||||
}
|
||||
|
||||
return this.visitExpression(expr, {...context, absoluteModuleName});
|
||||
return this.visitExpression(expr, context);
|
||||
}
|
||||
|
||||
const body = fn.body;
|
||||
@ -473,17 +488,7 @@ export class StaticInterpreter {
|
||||
}
|
||||
|
||||
private getReference(node: ts.Declaration, context: Context): Reference {
|
||||
const id = identifierOfDeclaration(node);
|
||||
if (id === undefined) {
|
||||
throw new Error(`Don't know how to refer to ${ts.SyntaxKind[node.kind]}`);
|
||||
}
|
||||
if (context.absoluteModuleName !== null) {
|
||||
// TODO(alxhub): investigate whether this can get symbol names wrong in the event of
|
||||
// re-exports under different names.
|
||||
return new AbsoluteReference(node, id, context.absoluteModuleName, id.text);
|
||||
} else {
|
||||
return new ResolvedReference(node, id);
|
||||
}
|
||||
return this.refResolver.resolve(node, context.absoluteModuleName, context.resolutionContext);
|
||||
}
|
||||
}
|
||||
|
||||
@ -504,22 +509,6 @@ function literal(value: ResolvedValue): any {
|
||||
throw new Error(`Value ${value} is not literal and cannot be used in this context.`);
|
||||
}
|
||||
|
||||
function identifierOfDeclaration(decl: ts.Declaration): ts.Identifier|undefined {
|
||||
if (ts.isClassDeclaration(decl)) {
|
||||
return decl.name;
|
||||
} else if (ts.isEnumDeclaration(decl)) {
|
||||
return decl.name;
|
||||
} else if (ts.isFunctionDeclaration(decl)) {
|
||||
return decl.name;
|
||||
} else if (ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name)) {
|
||||
return decl.name;
|
||||
} else if (ts.isShorthandPropertyAssignment(decl)) {
|
||||
return decl.name;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {
|
||||
if (node.parent === undefined || !ts.isVariableDeclarationList(node.parent)) {
|
||||
return false;
|
||||
@ -532,3 +521,19 @@ function isVariableDeclarationDeclared(node: ts.VariableDeclaration): boolean {
|
||||
return varStmt.modifiers !== undefined &&
|
||||
varStmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword);
|
||||
}
|
||||
|
||||
const EMPTY = {};
|
||||
|
||||
function joinModuleContext(existing: Context, node: ts.Node, decl: Declaration): {
|
||||
absoluteModuleName?: string,
|
||||
resolutionContext?: string,
|
||||
} {
|
||||
if (decl.viaModule !== null && decl.viaModule !== existing.absoluteModuleName) {
|
||||
return {
|
||||
absoluteModuleName: decl.viaModule,
|
||||
resolutionContext: node.getSourceFile().fileName,
|
||||
};
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@
|
||||
import {WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteReference, Reference} from '../../imports';
|
||||
import {AbsoluteReference, Reference, TsReferenceResolver} from '../../imports';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {PartialEvaluator} from '../src/interface';
|
||||
@ -42,9 +42,10 @@ function makeExpression(
|
||||
|
||||
function evaluate<T extends ResolvedValue>(
|
||||
code: string, expr: string, supportingFiles: {name: string, contents: string}[] = []): T {
|
||||
const {expression, checker} = makeExpression(code, expr, supportingFiles);
|
||||
const {expression, checker, program, options, host} = makeExpression(code, expr, supportingFiles);
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
|
||||
return evaluator.evaluate(expression) as T;
|
||||
}
|
||||
|
||||
@ -135,7 +136,7 @@ describe('ngtsc metadata', () => {
|
||||
});
|
||||
|
||||
it('imports work', () => {
|
||||
const {program} = makeProgram([
|
||||
const {program, options, host} = makeProgram([
|
||||
{name: 'second.ts', contents: 'export function foo(bar) { return bar; }'},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
@ -149,7 +150,8 @@ describe('ngtsc metadata', () => {
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
|
||||
const expr = result.initializer !;
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
|
||||
const resolved = evaluator.evaluate(expr);
|
||||
if (!(resolved instanceof Reference)) {
|
||||
return fail('Expected expression to resolve to a reference');
|
||||
@ -167,7 +169,7 @@ describe('ngtsc metadata', () => {
|
||||
});
|
||||
|
||||
it('absolute imports work', () => {
|
||||
const {program} = makeProgram([
|
||||
const {program, options, host} = makeProgram([
|
||||
{name: 'node_modules/some_library/index.d.ts', contents: 'export declare function foo(bar);'},
|
||||
{
|
||||
name: 'entry.ts',
|
||||
@ -181,7 +183,8 @@ describe('ngtsc metadata', () => {
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const result = getDeclaration(program, 'entry.ts', 'target$', ts.isVariableDeclaration);
|
||||
const expr = result.initializer !;
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
|
||||
const resolved = evaluator.evaluate(expr);
|
||||
if (!(resolved instanceof AbsoluteReference)) {
|
||||
return fail('Expected expression to resolve to an absolute reference');
|
||||
|
Reference in New Issue
Block a user