fix(compiler-cli): use correct module resolution context for absolute imports in .d.ts files (#42879)
The compiler keeps track of how a declaration has been referenced using absolute module imports and from which path the absolute module should be resolved from. There was a bug in how the .d.ts metadata extraction would incorrectly use the .d.ts file itself as resolution context for symbols that had been imported using a relative module specifier. This could result in module resolution failures. For example, when extracting NgModule metadata from `/node_modules/lib/index.d.ts` that looks like ``` import {LibDirective} from './dir'; @NgModule({ declarations: [LibDirective], exports: [LibDirective], }) export class LibModule {} ``` and `/app.module.ts` that contains ``` import {LibModule} from 'lib'; @NgModule({ imports: [LibModule], }) export class AppModule {} ``` then `AppModule` would have recorded a reference to `LibModule` using the `'lib'` module specifier. When extracting the NgModule metadata from the `/node_modules/lib/index.d.ts` file the relative import into `./dir` should also be assumed to be importable from `'lib'` (according to APF where symbols need to be exported from a single entry-point) so the reference to `LibDirective` should have `'lib'` as absolute module specifier, but it would incorrectly have `/node_modules/lib/index.d.ts` as resolution context path. The latter is incorrect as `'lib'` needs to be resolved from `/app.module.ts` and not from within the library itself. Fixes #42810 PR Close #42879
This commit is contained in:
parent
dbae00195e
commit
ed9cfb674f
|
@ -30,7 +30,7 @@ export class DtsMetadataReader implements MetadataReader {
|
||||||
*/
|
*/
|
||||||
getNgModuleMetadata(ref: Reference<ClassDeclaration>): NgModuleMeta|null {
|
getNgModuleMetadata(ref: Reference<ClassDeclaration>): NgModuleMeta|null {
|
||||||
const clazz = ref.node;
|
const clazz = ref.node;
|
||||||
const resolutionContext = clazz.getSourceFile().fileName;
|
|
||||||
// This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`.
|
// This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`.
|
||||||
// TODO(alxhub): investigate caching of .d.ts module metadata.
|
// TODO(alxhub): investigate caching of .d.ts module metadata.
|
||||||
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
|
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
|
||||||
|
@ -49,12 +49,10 @@ export class DtsMetadataReader implements MetadataReader {
|
||||||
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
|
const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments;
|
||||||
return {
|
return {
|
||||||
ref,
|
ref,
|
||||||
declarations: extractReferencesFromType(
|
declarations:
|
||||||
this.checker, declarationMetadata, ref.ownedByModuleGuess, resolutionContext),
|
extractReferencesFromType(this.checker, declarationMetadata, ref.bestGuessOwningModule),
|
||||||
exports: extractReferencesFromType(
|
exports: extractReferencesFromType(this.checker, exportMetadata, ref.bestGuessOwningModule),
|
||||||
this.checker, exportMetadata, ref.ownedByModuleGuess, resolutionContext),
|
imports: extractReferencesFromType(this.checker, importMetadata, ref.bestGuessOwningModule),
|
||||||
imports: extractReferencesFromType(
|
|
||||||
this.checker, importMetadata, ref.ownedByModuleGuess, resolutionContext),
|
|
||||||
schemas: [],
|
schemas: [],
|
||||||
rawDeclarations: null,
|
rawDeclarations: null,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {Reference} from '../../imports';
|
import {OwningModule, Reference} from '../../imports';
|
||||||
import {ClassDeclaration, ClassMember, ClassMemberKind, isNamedClassDeclaration, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection';
|
import {ClassDeclaration, ClassMember, ClassMemberKind, isNamedClassDeclaration, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection';
|
||||||
import {nodeDebugInfo} from '../../util/src/typescript';
|
import {nodeDebugInfo} from '../../util/src/typescript';
|
||||||
|
|
||||||
|
@ -16,8 +16,8 @@ import {DirectiveMeta, DirectiveTypeCheckMeta, MetadataReader, NgModuleMeta, Pip
|
||||||
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
|
import {ClassPropertyMapping, ClassPropertyName} from './property_mapping';
|
||||||
|
|
||||||
export function extractReferencesFromType(
|
export function extractReferencesFromType(
|
||||||
checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string|null,
|
checker: ts.TypeChecker, def: ts.TypeNode,
|
||||||
resolutionContext: string): Reference<ClassDeclaration>[] {
|
bestGuessOwningModule: OwningModule|null): Reference<ClassDeclaration>[] {
|
||||||
if (!ts.isTupleTypeNode(def)) {
|
if (!ts.isTupleTypeNode(def)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
@ -31,11 +31,15 @@ export function extractReferencesFromType(
|
||||||
if (!isNamedClassDeclaration(node)) {
|
if (!isNamedClassDeclaration(node)) {
|
||||||
throw new Error(`Expected named ClassDeclaration: ${nodeDebugInfo(node)}`);
|
throw new Error(`Expected named ClassDeclaration: ${nodeDebugInfo(node)}`);
|
||||||
}
|
}
|
||||||
const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
|
if (from !== null && !from.startsWith('.')) {
|
||||||
if (specifier !== null) {
|
// The symbol was imported using an absolute module specifier so return a reference that
|
||||||
return new Reference(node, {specifier, resolutionContext});
|
// uses that absolute module specifier as its best guess owning module.
|
||||||
|
return new Reference(
|
||||||
|
node, {specifier: from, resolutionContext: def.getSourceFile().fileName});
|
||||||
} else {
|
} else {
|
||||||
return new Reference(node);
|
// For local symbols or symbols that were imported using a relative module import it is
|
||||||
|
// assumed that the symbol is exported from the provided best guess owning module.
|
||||||
|
return new Reference(node, bestGuessOwningModule);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as ts from 'typescript';
|
||||||
|
|
||||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
|
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
|
||||||
import {runInEachFileSystem} from '../../file_system/testing';
|
import {runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {Reference} from '../../imports';
|
import {OwningModule, Reference} from '../../imports';
|
||||||
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
|
||||||
import {loadFakeCore, makeProgram} from '../../testing';
|
import {loadFakeCore, makeProgram} from '../../testing';
|
||||||
|
|
||||||
|
@ -89,5 +89,93 @@ runInEachFileSystem(() => {
|
||||||
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
|
const meta = dtsReader.getDirectiveMetadata(new Reference(clazz))!;
|
||||||
expect(meta.isStructural).toBeTrue();
|
expect(meta.isStructural).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should retain an absolute owning module for relative imports', () => {
|
||||||
|
const externalPath = absoluteFrom('/external.d.ts');
|
||||||
|
const {program} = makeProgram(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: externalPath,
|
||||||
|
contents: `
|
||||||
|
import * as i0 from '@angular/core';
|
||||||
|
import * as i1 from 'absolute';
|
||||||
|
import * as i2 from './relative';
|
||||||
|
|
||||||
|
export declare class ExternalModule {
|
||||||
|
static ɵmod: i0.ɵɵNgModuleDeclaration<RelativeModule, [typeof i2.RelativeDir], never, [typeof i1.AbsoluteDir, typeof i2.RelativeDir]>;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/relative.d.ts'),
|
||||||
|
contents: `
|
||||||
|
import * as i0 from '@angular/core';
|
||||||
|
|
||||||
|
export declare class RelativeDir {
|
||||||
|
static ɵdir: i0.ɵɵDirectiveDeclaration<RelativeDir, '[dir]', never, never, never, never>;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: absoluteFrom('/node_modules/absolute.d.ts'),
|
||||||
|
contents: `
|
||||||
|
import * as i0 from '@angular/core';
|
||||||
|
|
||||||
|
export declare class AbsoluteDir {
|
||||||
|
static ɵdir: i0.ɵɵDirectiveDeclaration<ExternalDir, '[dir]', never, never, never, never>;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
{
|
||||||
|
skipLibCheck: true,
|
||||||
|
lib: ['es6', 'dom'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const externalSf = getSourceFileOrError(program, externalPath);
|
||||||
|
const clazz = externalSf.statements[3];
|
||||||
|
if (!isNamedClassDeclaration(clazz)) {
|
||||||
|
return fail('Expected class declaration');
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const dtsReader =
|
||||||
|
new DtsMetadataReader(typeChecker, new TypeScriptReflectionHost(typeChecker));
|
||||||
|
|
||||||
|
const withoutOwningModule = dtsReader.getNgModuleMetadata(new Reference(clazz))!;
|
||||||
|
expect(withoutOwningModule.exports.length).toBe(2);
|
||||||
|
|
||||||
|
// `AbsoluteDir` was imported from an absolute module so the export Reference should have
|
||||||
|
// a corresponding best guess owning module.
|
||||||
|
expect(withoutOwningModule.exports[0].bestGuessOwningModule).toEqual({
|
||||||
|
specifier: 'absolute',
|
||||||
|
resolutionContext: externalSf.fileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// `RelativeDir` was imported from a relative module specifier so the original reference's
|
||||||
|
// best guess owning module should have been retained, which was null.
|
||||||
|
expect(withoutOwningModule.exports[1].bestGuessOwningModule).toBeNull();
|
||||||
|
|
||||||
|
const owningModule: OwningModule = {
|
||||||
|
specifier: 'module',
|
||||||
|
resolutionContext: absoluteFrom('/context.ts'),
|
||||||
|
};
|
||||||
|
const withOwningModule = dtsReader.getNgModuleMetadata(new Reference(clazz, owningModule))!;
|
||||||
|
expect(withOwningModule.exports.length).toBe(2);
|
||||||
|
|
||||||
|
// Again, `AbsoluteDir` was imported from an absolute module so the export Reference should
|
||||||
|
// have a corresponding best guess owning module; the owning module of the incoming reference
|
||||||
|
// is irrelevant here.
|
||||||
|
expect(withOwningModule.exports[0].bestGuessOwningModule).toEqual({
|
||||||
|
specifier: 'absolute',
|
||||||
|
resolutionContext: externalSf.fileName,
|
||||||
|
});
|
||||||
|
|
||||||
|
// As `RelativeDir` was imported from a relative module specifier, the export Reference should
|
||||||
|
// continue to have the owning module of the incoming reference as the relatively imported
|
||||||
|
// symbol is assumed to also be exported from the absolute module specifier as captured in the
|
||||||
|
// best guess owning module.
|
||||||
|
expect(withOwningModule.exports[1].bestGuessOwningModule).toEqual(owningModule);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -68,8 +68,7 @@ function makeTestEnv(
|
||||||
const exportedSymbol = symbol.exports!.get(name as ts.__String);
|
const exportedSymbol = symbol.exports!.get(name as ts.__String);
|
||||||
if (exportedSymbol !== undefined) {
|
if (exportedSymbol !== undefined) {
|
||||||
const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration;
|
const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration;
|
||||||
const specifier = MODULE_FROM_NODE_MODULES_PATH.exec(sf.fileName)![1];
|
return new Reference(decl);
|
||||||
return new Reference(decl, {specifier, resolutionContext: sf.fileName});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('Class not found: ' + name);
|
throw new Error('Class not found: ' + name);
|
||||||
|
@ -143,10 +142,13 @@ runInEachFileSystem(() => {
|
||||||
});
|
});
|
||||||
const {Dir, ModuleB} = refs;
|
const {Dir, ModuleB} = refs;
|
||||||
const scope = resolver.resolve(ModuleB)!;
|
const scope = resolver.resolve(ModuleB)!;
|
||||||
expect(scopeToRefs(scope)).toEqual([Dir]);
|
expect(scopeToRefs(scope).map(ref => ref.node)).toEqual([Dir.node]);
|
||||||
|
|
||||||
// Explicitly verify that the directive has the correct owning module.
|
// Explicitly verify that the directive has the correct owning module.
|
||||||
expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration');
|
expect(scope.exported.directives[0].ref.bestGuessOwningModule).toEqual({
|
||||||
|
specifier: 'declaration',
|
||||||
|
resolutionContext: ModuleB.node.getSourceFile().fileName,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should write correct aliases for deep dependencies', () => {
|
it('should write correct aliases for deep dependencies', () => {
|
||||||
|
|
Loading…
Reference in New Issue