fix(ivy): support emitting a reference to interface declarations (#34849)
In #34021 the ngtsc compiler gained the ability to emit type parameter constraints, which would generate imports for any type reference that is used within the constraint. However, the `AbsoluteModuleStrategy` reference emitter strategy did not consider interface declarations as a valid declaration it can generate an import for, throwing an error instead. This commit fixes the issue by including interface declarations in the logic that determines whether something is a declaration. Fixes #34837 PR Close #34849
This commit is contained in:
parent
5cada5cce1
commit
6ddf5508db
@ -190,7 +190,8 @@ export function toR3Reference(
|
|||||||
valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile,
|
valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile,
|
||||||
typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference {
|
typeContext: ts.SourceFile, refEmitter: ReferenceEmitter): R3Reference {
|
||||||
const value = refEmitter.emit(valueRef, valueContext);
|
const value = refEmitter.emit(valueRef, valueContext);
|
||||||
const type = refEmitter.emit(typeRef, typeContext, ImportFlags.ForceNewImport);
|
const type = refEmitter.emit(
|
||||||
|
typeRef, typeContext, ImportFlags.ForceNewImport | ImportFlags.AllowTypeImports);
|
||||||
if (value === null || type === null) {
|
if (value === null || type === null) {
|
||||||
throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`);
|
throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {UnifiedModulesHost} from '../../core/api';
|
|||||||
import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFromSourceFile, dirname, relative} from '../../file_system';
|
import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFromSourceFile, dirname, relative} from '../../file_system';
|
||||||
import {stripExtension} from '../../file_system/src/util';
|
import {stripExtension} from '../../file_system/src/util';
|
||||||
import {ReflectionHost} from '../../reflection';
|
import {ReflectionHost} from '../../reflection';
|
||||||
import {getSourceFile, isDeclaration, nodeNameForError} from '../../util/src/typescript';
|
import {getSourceFile, isDeclaration, isTypeDeclaration, nodeNameForError} from '../../util/src/typescript';
|
||||||
|
|
||||||
import {findExportedNameOfNode} from './find_export';
|
import {findExportedNameOfNode} from './find_export';
|
||||||
import {Reference} from './references';
|
import {Reference} from './references';
|
||||||
@ -40,6 +40,15 @@ export enum ImportFlags {
|
|||||||
* into TypeScript and type-checked (such as in template type-checking).
|
* into TypeScript and type-checked (such as in template type-checking).
|
||||||
*/
|
*/
|
||||||
NoAliasing = 0x02,
|
NoAliasing = 0x02,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that an import to a type-only declaration is allowed.
|
||||||
|
*
|
||||||
|
* For references that occur in type-positions, the referred declaration may be a type-only
|
||||||
|
* declaration that is not retained during emit. Including this flag allows to emit references to
|
||||||
|
* type-only declarations as used in e.g. template type-checking.
|
||||||
|
*/
|
||||||
|
AllowTypeImports = 0x04,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +70,7 @@ export interface ReferenceEmitStrategy {
|
|||||||
*
|
*
|
||||||
* @param ref the `Reference` for which to generate an expression
|
* @param ref the `Reference` for which to generate an expression
|
||||||
* @param context the source file in which the `Expression` must be valid
|
* @param context the source file in which the `Expression` must be valid
|
||||||
* @param importMode a flag which controls whether imports should be generated or not
|
* @param importFlags a flag which controls whether imports should be generated or not
|
||||||
* @returns an `Expression` which refers to the `Reference`, or `null` if none can be generated
|
* @returns an `Expression` which refers to the `Reference`, or `null` if none can be generated
|
||||||
*/
|
*/
|
||||||
emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): Expression|null;
|
emit(ref: Reference, context: ts.SourceFile, importFlags: ImportFlags): Expression|null;
|
||||||
@ -133,14 +142,18 @@ export class AbsoluteModuleStrategy implements ReferenceEmitStrategy {
|
|||||||
protected program: ts.Program, protected checker: ts.TypeChecker,
|
protected program: ts.Program, protected checker: ts.TypeChecker,
|
||||||
protected moduleResolver: ModuleResolver, private reflectionHost: ReflectionHost) {}
|
protected moduleResolver: ModuleResolver, private reflectionHost: ReflectionHost) {}
|
||||||
|
|
||||||
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
|
emit(ref: Reference<ts.Node>, context: ts.SourceFile, importFlags: ImportFlags): Expression|null {
|
||||||
if (ref.bestGuessOwningModule === null) {
|
if (ref.bestGuessOwningModule === null) {
|
||||||
// There is no module name available for this Reference, meaning it was arrived at via a
|
// There is no module name available for this Reference, meaning it was arrived at via a
|
||||||
// relative path.
|
// relative path.
|
||||||
return null;
|
return null;
|
||||||
} else if (!isDeclaration(ref.node)) {
|
} else if (!isDeclaration(ref.node)) {
|
||||||
// It's not possible to import something which isn't a declaration.
|
// It's not possible to import something which isn't a declaration.
|
||||||
throw new Error('Debug assert: importing a Reference to non-declaration?');
|
throw new Error(
|
||||||
|
`Debug assert: unable to import a Reference to non-declaration of type ${ts.SyntaxKind[ref.node.kind]}.`);
|
||||||
|
} else if ((importFlags & ImportFlags.AllowTypeImports) === 0 && isTypeDeclaration(ref.node)) {
|
||||||
|
throw new Error(
|
||||||
|
`Importing a type-only declaration of type ${ts.SyntaxKind[ref.node.kind]} in a value position is not allowed.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find the exported name of the declaration, if one is available.
|
// Try to find the exported name of the declaration, if one is available.
|
||||||
|
@ -8,18 +8,128 @@
|
|||||||
import {ExternalExpr} from '@angular/compiler';
|
import {ExternalExpr} from '@angular/compiler';
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {LogicalFileSystem, absoluteFrom} from '../../file_system';
|
import {LogicalFileSystem, absoluteFrom as _} from '../../file_system';
|
||||||
import {runInEachFileSystem} from '../../file_system/testing';
|
import {TestFile, runInEachFileSystem} from '../../file_system/testing';
|
||||||
import {Declaration, TypeScriptReflectionHost} from '../../reflection';
|
import {Declaration, TypeScriptReflectionHost} from '../../reflection';
|
||||||
import {getDeclaration, makeProgram} from '../../testing';
|
import {getDeclaration, makeProgram} from '../../testing';
|
||||||
import {LogicalProjectStrategy} from '../src/emitter';
|
import {AbsoluteModuleStrategy, ImportFlags, LogicalProjectStrategy} from '../src/emitter';
|
||||||
import {Reference} from '../src/references';
|
import {Reference} from '../src/references';
|
||||||
|
import {ModuleResolver} from '../src/resolver';
|
||||||
|
|
||||||
runInEachFileSystem(() => {
|
runInEachFileSystem(() => {
|
||||||
describe('LogicalProjectStrategy', () => {
|
describe('AbsoluteModuleStrategy', () => {
|
||||||
let _: typeof absoluteFrom;
|
function makeStrategy(files: TestFile[]) {
|
||||||
beforeEach(() => _ = absoluteFrom);
|
const {program, host} = makeProgram(files);
|
||||||
|
const checker = program.getTypeChecker();
|
||||||
|
const moduleResolver = new ModuleResolver(
|
||||||
|
program, program.getCompilerOptions(), host, /* moduleResolutionCache */ null);
|
||||||
|
const strategy = new AbsoluteModuleStrategy(
|
||||||
|
program, checker, moduleResolver, new TypeScriptReflectionHost(checker));
|
||||||
|
|
||||||
|
return {strategy, program};
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should not generate an import for a reference without owning module', () => {
|
||||||
|
const {strategy, program} = makeStrategy([
|
||||||
|
{
|
||||||
|
name: _('/node_modules/external.d.ts'),
|
||||||
|
contents: `export declare class Foo {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/context.ts'),
|
||||||
|
contents: 'export class Context {}',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decl =
|
||||||
|
getDeclaration(program, _('/node_modules/external.d.ts'), 'Foo', ts.isClassDeclaration);
|
||||||
|
const context = program.getSourceFile(_('/context.ts')) !;
|
||||||
|
|
||||||
|
const reference = new Reference(decl);
|
||||||
|
const emitted = strategy.emit(reference, context, ImportFlags.None);
|
||||||
|
expect(emitted).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate an import using the exported name of the declaration', () => {
|
||||||
|
const {strategy, program} = makeStrategy([
|
||||||
|
{
|
||||||
|
name: _('/node_modules/external.d.ts'),
|
||||||
|
contents: `
|
||||||
|
declare class Foo {}
|
||||||
|
export {Foo as Bar};
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/context.ts'),
|
||||||
|
contents: 'export class Context {}',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decl =
|
||||||
|
getDeclaration(program, _('/node_modules/external.d.ts'), 'Foo', ts.isClassDeclaration);
|
||||||
|
const context = program.getSourceFile(_('/context.ts')) !;
|
||||||
|
|
||||||
|
const reference = new Reference(decl, {
|
||||||
|
specifier: 'external',
|
||||||
|
resolutionContext: context.fileName,
|
||||||
|
});
|
||||||
|
const emitted = strategy.emit(reference, context, ImportFlags.None);
|
||||||
|
if (!(emitted instanceof ExternalExpr)) {
|
||||||
|
return fail('Reference should be emitted as ExternalExpr');
|
||||||
|
}
|
||||||
|
expect(emitted.value.name).toEqual('Bar');
|
||||||
|
expect(emitted.value.moduleName).toEqual('external');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw when generating an import to a type-only declaration when not allowed', () => {
|
||||||
|
const {strategy, program} = makeStrategy([
|
||||||
|
{
|
||||||
|
name: _('/node_modules/external.d.ts'),
|
||||||
|
contents: `export declare interface Foo {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/context.ts'),
|
||||||
|
contents: 'export class Context {}',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decl = getDeclaration(
|
||||||
|
program, _('/node_modules/external.d.ts'), 'Foo', ts.isInterfaceDeclaration);
|
||||||
|
const context = program.getSourceFile(_('/context.ts')) !;
|
||||||
|
|
||||||
|
const reference = new Reference(decl, {
|
||||||
|
specifier: 'external',
|
||||||
|
resolutionContext: context.fileName,
|
||||||
|
});
|
||||||
|
expect(() => strategy.emit(reference, context, ImportFlags.None))
|
||||||
|
.toThrowError(
|
||||||
|
'Importing a type-only declaration of type InterfaceDeclaration in a value position is not allowed.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should generate an import to a type-only declaration when allowed', () => {
|
||||||
|
const {strategy, program} = makeStrategy([
|
||||||
|
{
|
||||||
|
name: _('/node_modules/external.d.ts'),
|
||||||
|
contents: `export declare interface Foo {}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: _('/context.ts'),
|
||||||
|
contents: 'export class Context {}',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const decl = getDeclaration(
|
||||||
|
program, _('/node_modules/external.d.ts'), 'Foo', ts.isInterfaceDeclaration);
|
||||||
|
const context = program.getSourceFile(_('/context.ts')) !;
|
||||||
|
|
||||||
|
const reference =
|
||||||
|
new Reference(decl, {specifier: 'external', resolutionContext: context.fileName});
|
||||||
|
const emitted = strategy.emit(reference, context, ImportFlags.AllowTypeImports);
|
||||||
|
if (!(emitted instanceof ExternalExpr)) {
|
||||||
|
return fail('Reference should be emitted as ExternalExpr');
|
||||||
|
}
|
||||||
|
expect(emitted.value.name).toEqual('Foo');
|
||||||
|
expect(emitted.value.moduleName).toEqual('external');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('LogicalProjectStrategy', () => {
|
||||||
it('should enumerate exports with the ReflectionHost', () => {
|
it('should enumerate exports with the ReflectionHost', () => {
|
||||||
// Use a modified ReflectionHost that prefixes all export names that it enumerates.
|
// Use a modified ReflectionHost that prefixes all export names that it enumerates.
|
||||||
class TestHost extends TypeScriptReflectionHost {
|
class TestHost extends TypeScriptReflectionHost {
|
||||||
|
@ -76,7 +76,9 @@ export function getDeclaration<T extends ts.Declaration>(
|
|||||||
chosenDecl = decl;
|
chosenDecl = decl;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) {
|
} else if (
|
||||||
|
ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) ||
|
||||||
|
ts.isInterfaceDeclaration(node)) {
|
||||||
if (node.name !== undefined && node.name.text === name) {
|
if (node.name !== undefined && node.name.text === name) {
|
||||||
chosenDecl = node;
|
chosenDecl = node;
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,8 @@ export class Environment {
|
|||||||
* This may involve importing the node into the file if it's not declared there already.
|
* This may involve importing the node into the file if it's not declared there already.
|
||||||
*/
|
*/
|
||||||
referenceType(ref: Reference): ts.TypeNode {
|
referenceType(ref: Reference): ts.TypeNode {
|
||||||
const ngExpr = this.refEmitter.emit(ref, this.contextFile, ImportFlags.NoAliasing);
|
const ngExpr = this.refEmitter.emit(
|
||||||
|
ref, this.contextFile, ImportFlags.NoAliasing | ImportFlags.AllowTypeImports);
|
||||||
|
|
||||||
// Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
|
// Create an `ExpressionType` from the `Expression` and translate it via `translateType`.
|
||||||
// TODO(alxhub): support references to types with generic arguments in a clean way.
|
// TODO(alxhub): support references to types with generic arguments in a clean way.
|
||||||
|
@ -162,5 +162,53 @@ runInEachFileSystem(() => {
|
|||||||
.toThrowError('A type reference to emit must be imported from an absolute module');
|
.toThrowError('A type reference to emit must be imported from an absolute module');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('can emit references to interfaces', () => {
|
||||||
|
const additionalFiles: TestFile[] = [{
|
||||||
|
name: absoluteFrom('/node_modules/types/index.d.ts'),
|
||||||
|
contents: `export declare interface MyInterface {}`,
|
||||||
|
}];
|
||||||
|
const emitter = createEmitter(
|
||||||
|
`
|
||||||
|
import {MyInterface} from 'types';
|
||||||
|
|
||||||
|
export class TestClass<T extends MyInterface> {}`,
|
||||||
|
additionalFiles);
|
||||||
|
|
||||||
|
expect(emitter.canEmit()).toBe(true);
|
||||||
|
expect(emit(emitter)).toEqual('<T extends test.MyInterface>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can emit references to enums', () => {
|
||||||
|
const additionalFiles: TestFile[] = [{
|
||||||
|
name: absoluteFrom('/node_modules/types/index.d.ts'),
|
||||||
|
contents: `export declare enum MyEnum {}`,
|
||||||
|
}];
|
||||||
|
const emitter = createEmitter(
|
||||||
|
`
|
||||||
|
import {MyEnum} from 'types';
|
||||||
|
|
||||||
|
export class TestClass<T extends MyEnum> {}`,
|
||||||
|
additionalFiles);
|
||||||
|
|
||||||
|
expect(emitter.canEmit()).toBe(true);
|
||||||
|
expect(emit(emitter)).toEqual('<T extends test.MyEnum>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can emit references to type aliases', () => {
|
||||||
|
const additionalFiles: TestFile[] = [{
|
||||||
|
name: absoluteFrom('/node_modules/types/index.d.ts'),
|
||||||
|
contents: `export declare type MyType = string;`,
|
||||||
|
}];
|
||||||
|
const emitter = createEmitter(
|
||||||
|
`
|
||||||
|
import {MyType} from 'types';
|
||||||
|
|
||||||
|
export class TestClass<T extends MyType> {}`,
|
||||||
|
additionalFiles);
|
||||||
|
|
||||||
|
expect(emitter.canEmit()).toBe(true);
|
||||||
|
expect(emit(emitter)).toEqual('<T extends test.MyType>');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -67,9 +67,19 @@ export function identifierOfNode(decl: ts.Node & {name?: ts.Node}): ts.Identifie
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isDeclaration(node: ts.Node): node is ts.Declaration {
|
export function isDeclaration(node: ts.Node): node is ts.Declaration {
|
||||||
return ts.isEnumDeclaration(node) || ts.isClassDeclaration(node) ||
|
return isValueDeclaration(node) || isTypeDeclaration(node);
|
||||||
ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) ||
|
}
|
||||||
ts.isTypeAliasDeclaration(node);
|
|
||||||
|
export function isValueDeclaration(node: ts.Node): node is ts.ClassDeclaration|
|
||||||
|
ts.FunctionDeclaration|ts.VariableDeclaration {
|
||||||
|
return ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node) ||
|
||||||
|
ts.isVariableDeclaration(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTypeDeclaration(node: ts.Node): node is ts.EnumDeclaration|
|
||||||
|
ts.TypeAliasDeclaration|ts.InterfaceDeclaration {
|
||||||
|
return ts.isEnumDeclaration(node) || ts.isTypeAliasDeclaration(node) ||
|
||||||
|
ts.isInterfaceDeclaration(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isExported(node: ts.Declaration): boolean {
|
export function isExported(node: ts.Declaration): boolean {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user