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
@ -9,7 +9,7 @@
|
||||
import {Expression, WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteReference, Reference, ResolvedReference} from '../../imports';
|
||||
import {AbsoluteReference, Reference, ReferenceResolver, ResolvedReference} from '../../imports';
|
||||
import {ReflectionHost, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectTypeEntityToDeclaration} from '../../reflection';
|
||||
import {TypeCheckableDirectiveMeta} from '../../typecheck';
|
||||
|
||||
@ -89,7 +89,9 @@ export class SelectorScopeRegistry {
|
||||
*/
|
||||
private _declararedTypeToModule = new Map<ts.Declaration, ts.Declaration>();
|
||||
|
||||
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {}
|
||||
constructor(
|
||||
private checker: ts.TypeChecker, private reflector: ReflectionHost,
|
||||
private resolver: ReferenceResolver) {}
|
||||
|
||||
/**
|
||||
* Register a module's metadata with the registry.
|
||||
@ -161,7 +163,9 @@ export class SelectorScopeRegistry {
|
||||
// Process the declaration scope of the module, and lookup the selector of every declared type.
|
||||
// The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
|
||||
// was not imported from a .d.ts source.
|
||||
for (const ref of this.lookupScopesOrDie(module !, /* ngModuleImportedFrom */ null)
|
||||
for (const ref of this
|
||||
.lookupScopesOrDie(
|
||||
module !, /* ngModuleImportedFrom */ null, node.getSourceFile().fileName)
|
||||
.compilation) {
|
||||
const node = ts.getOriginalNode(ref.node) as ts.Declaration;
|
||||
|
||||
@ -203,9 +207,10 @@ export class SelectorScopeRegistry {
|
||||
return scope !== null ? convertScopeToExpressions(scope, node) : null;
|
||||
}
|
||||
|
||||
private lookupScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null):
|
||||
SelectorScopes {
|
||||
const result = this.lookupScopes(node, ngModuleImportedFrom);
|
||||
private lookupScopesOrDie(
|
||||
node: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): SelectorScopes {
|
||||
const result = this.lookupScopes(node, ngModuleImportedFrom, resolutionContext);
|
||||
if (result === null) {
|
||||
throw new Error(`Module not found: ${reflectNameOfDeclaration(node)}`);
|
||||
}
|
||||
@ -219,8 +224,9 @@ export class SelectorScopeRegistry {
|
||||
* (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well
|
||||
* as imports and exports from other modules that are relatively imported.
|
||||
*/
|
||||
private lookupScopes(node: ts.Declaration, ngModuleImportedFrom: string|null): SelectorScopes
|
||||
|null {
|
||||
private lookupScopes(
|
||||
node: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): SelectorScopes|null {
|
||||
let data: ModuleData|null = null;
|
||||
|
||||
// Either this module was analyzed directly, or has a precompiled ngModuleDef.
|
||||
@ -230,7 +236,7 @@ export class SelectorScopeRegistry {
|
||||
} else {
|
||||
// The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type
|
||||
// annotation that specifies the needed metadata.
|
||||
data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom);
|
||||
data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom, resolutionContext);
|
||||
// Note that data here could still be null, if the class didn't have a precompiled
|
||||
// ngModuleDef.
|
||||
}
|
||||
@ -239,22 +245,28 @@ export class SelectorScopeRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
const context = node.getSourceFile().fileName;
|
||||
|
||||
return {
|
||||
compilation: [
|
||||
...data.declarations,
|
||||
// Expand imports to the exported scope of those imports.
|
||||
...flatten(data.imports.map(
|
||||
ref => this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref))
|
||||
.exported)),
|
||||
ref =>
|
||||
this.lookupScopesOrDie(ref.node as ts.Declaration, absoluteModuleName(ref), context)
|
||||
.exported)),
|
||||
// And include the compilation scope of exported modules.
|
||||
...flatten(
|
||||
data.exports
|
||||
.map(ref => this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)))
|
||||
.map(
|
||||
ref => this.lookupScopes(
|
||||
ref.node as ts.Declaration, absoluteModuleName(ref), context))
|
||||
.filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null)
|
||||
.map(scope => scope.exported))
|
||||
],
|
||||
exported: flatten(data.exports.map(ref => {
|
||||
const scope = this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref));
|
||||
const scope =
|
||||
this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref), context);
|
||||
if (scope !== null) {
|
||||
return scope.exported;
|
||||
} else {
|
||||
@ -297,7 +309,8 @@ export class SelectorScopeRegistry {
|
||||
* stemming from this module.
|
||||
*/
|
||||
private _readModuleDataFromCompiledClass(
|
||||
clazz: ts.Declaration, ngModuleImportedFrom: string|null): ModuleData|null {
|
||||
clazz: ts.Declaration, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): ModuleData|null {
|
||||
// This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`.
|
||||
// TODO(alxhub): investigate caching of .d.ts module metadata.
|
||||
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
|
||||
@ -315,9 +328,12 @@ export class SelectorScopeRegistry {
|
||||
// Read the ModuleData out of the type arguments.
|
||||
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
|
||||
return {
|
||||
declarations: this._extractReferencesFromType(declarationMetadata, ngModuleImportedFrom),
|
||||
exports: this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom),
|
||||
imports: this._extractReferencesFromType(importMetadata, ngModuleImportedFrom),
|
||||
declarations: this._extractReferencesFromType(
|
||||
declarationMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
exports:
|
||||
this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
imports:
|
||||
this._extractReferencesFromType(importMetadata, ngModuleImportedFrom, resolutionContext),
|
||||
};
|
||||
}
|
||||
|
||||
@ -389,8 +405,9 @@ export class SelectorScopeRegistry {
|
||||
* This operation assumes that these types should be imported from `ngModuleImportedFrom` unless
|
||||
* they themselves were imported from another absolute path.
|
||||
*/
|
||||
private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string|null):
|
||||
Reference<ts.Declaration>[] {
|
||||
private _extractReferencesFromType(
|
||||
def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
||||
resolutionContext: string): Reference<ts.Declaration>[] {
|
||||
if (!ts.isTupleTypeNode(def)) {
|
||||
return [];
|
||||
}
|
||||
@ -402,12 +419,10 @@ export class SelectorScopeRegistry {
|
||||
if (ngModuleImportedFrom !== null) {
|
||||
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
|
||||
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
||||
const id = reflectIdentifierOfDeclaration(node);
|
||||
return new AbsoluteReference(node, id !, moduleName, id !.text);
|
||||
return this.resolver.resolve(node, moduleName, resolutionContext);
|
||||
} else {
|
||||
const {node} = reflectTypeEntityToDeclaration(type, this.checker);
|
||||
const id = reflectIdentifierOfDeclaration(node);
|
||||
return new ResolvedReference(node, id !);
|
||||
return this.resolver.resolve(node, null, resolutionContext);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -9,9 +9,9 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {TsReferenceResolver} from '../../imports';
|
||||
import {PartialEvaluator} from '../../partial_evaluator';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
import {ResourceLoader} from '../src/api';
|
||||
import {ComponentDecoratorHandler} from '../src/component';
|
||||
@ -23,7 +23,7 @@ export class NoopResourceLoader implements ResourceLoader {
|
||||
|
||||
describe('ComponentDecoratorHandler', () => {
|
||||
it('should produce a diagnostic when @Component has non-literal argument', () => {
|
||||
const {program} = makeProgram([
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.ts',
|
||||
contents: 'export const Component: any;',
|
||||
@ -39,13 +39,14 @@ describe('ComponentDecoratorHandler', () => {
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const evaluator = new PartialEvaluator(host, checker);
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
|
||||
const handler = new ComponentDecoratorHandler(
|
||||
host, evaluator, new SelectorScopeRegistry(checker, host), false, new NoopResourceLoader(),
|
||||
[''], false, true);
|
||||
reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver),
|
||||
false, new NoopResourceLoader(), [''], false, true);
|
||||
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
|
||||
const detected = handler.detect(TestCmp, host.getDecoratorsOfDeclaration(TestCmp));
|
||||
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
|
||||
if (detected === undefined) {
|
||||
return fail('Failed to recognize @Component');
|
||||
}
|
||||
|
@ -8,15 +8,14 @@
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteReference, ResolvedReference} from '../../imports';
|
||||
import {AbsoluteReference, ResolvedReference, TsReferenceResolver} from '../../imports';
|
||||
import {TypeScriptReflectionHost} from '../../reflection';
|
||||
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
|
||||
|
||||
import {SelectorScopeRegistry} from '../src/selector_scope';
|
||||
|
||||
describe('SelectorScopeRegistry', () => {
|
||||
it('absolute imports work', () => {
|
||||
const {program} = makeProgram([
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.ts',
|
||||
contents: `
|
||||
@ -29,6 +28,7 @@ describe('SelectorScopeRegistry', () => {
|
||||
contents: `
|
||||
import {NgModuleDef} from '@angular/core';
|
||||
import * as i0 from './component';
|
||||
export {SomeCmp} from './component';
|
||||
|
||||
export declare class SomeModule {
|
||||
static ngModuleDef: NgModuleDef<SomeModule, [typeof i0.SomeCmp], never, [typeof i0.SomeCmp]>;
|
||||
@ -54,7 +54,7 @@ describe('SelectorScopeRegistry', () => {
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const ProgramModule =
|
||||
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
|
||||
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
|
||||
@ -65,7 +65,8 @@ describe('SelectorScopeRegistry', () => {
|
||||
|
||||
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
|
||||
|
||||
const registry = new SelectorScopeRegistry(checker, host);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
|
||||
|
||||
registry.registerModule(ProgramModule, {
|
||||
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
|
||||
@ -95,7 +96,7 @@ describe('SelectorScopeRegistry', () => {
|
||||
});
|
||||
|
||||
it('exports of third-party libs work', () => {
|
||||
const {program} = makeProgram([
|
||||
const {program, options, host} = makeProgram([
|
||||
{
|
||||
name: 'node_modules/@angular/core/index.d.ts',
|
||||
contents: `
|
||||
@ -126,7 +127,7 @@ describe('SelectorScopeRegistry', () => {
|
||||
},
|
||||
]);
|
||||
const checker = program.getTypeChecker();
|
||||
const host = new TypeScriptReflectionHost(checker);
|
||||
const reflectionHost = new TypeScriptReflectionHost(checker);
|
||||
const ProgramModule =
|
||||
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
|
||||
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
|
||||
@ -137,7 +138,8 @@ describe('SelectorScopeRegistry', () => {
|
||||
|
||||
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
|
||||
|
||||
const registry = new SelectorScopeRegistry(checker, host);
|
||||
const resolver = new TsReferenceResolver(program, checker, options, host);
|
||||
const registry = new SelectorScopeRegistry(checker, reflectionHost, resolver);
|
||||
|
||||
registry.registerModule(ProgramModule, {
|
||||
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],
|
||||
|
Reference in New Issue
Block a user