From 7e32e0e444838332e4c85b0ca5faddfa6952c258 Mon Sep 17 00:00:00 2001 From: Pmyl Date: Thu, 2 Jan 2020 18:17:49 +0000 Subject: [PATCH 1/3] add support for typeof of an imported module --- src/transformer/descriptor/helper/helper.ts | 9 ++ .../descriptor/mock/mockProperties.ts | 15 ++-- .../descriptor/mock/mockProperty.ts | 3 +- .../descriptor/mock/propertyLike.ts | 7 ++ .../descriptor/mock/signatureLike.ts | 7 ++ src/transformer/descriptor/module/module.ts | 33 +++++++ .../descriptor/properties/properties.ts | 21 +++-- .../descriptor/typeQuery/enumTypeQuery.ts | 1 - .../descriptor/typeQuery/typeQuery.ts | 48 +++++++--- src/transformer/helper/creator.ts | 3 +- .../descriptor/typeQuery/typeQuery.test.ts | 87 ++++++++++++++++--- .../interfaces/exportDefaultDeclaration.ts | 3 + .../utils/interfaces/exportEqualObject.ts | 5 ++ 13 files changed, 200 insertions(+), 42 deletions(-) create mode 100644 src/transformer/descriptor/mock/propertyLike.ts create mode 100644 src/transformer/descriptor/mock/signatureLike.ts create mode 100644 src/transformer/descriptor/module/module.ts create mode 100644 test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts create mode 100644 test/transformer/descriptor/utils/interfaces/exportEqualObject.ts diff --git a/src/transformer/descriptor/helper/helper.ts b/src/transformer/descriptor/helper/helper.ts index 08eb237e6..6448b6adc 100644 --- a/src/transformer/descriptor/helper/helper.ts +++ b/src/transformer/descriptor/helper/helper.ts @@ -67,9 +67,18 @@ export namespace TypescriptHelper { return symbol.escapedName.toString(); } + export function GetAliasedSymbolSafe(alias: ts.Symbol): ts.Symbol { + return isAlias(alias) ? TypeChecker().getAliasedSymbol(alias) : alias; + } + function GetFirstValidDeclaration(declarations: ts.Declaration[]): ts.Declaration { return declarations.find((declaration: ts.Declaration) => { return !ts.isVariableDeclaration(declaration); }) || declarations[0]; } + + function isAlias(symbol: ts.Symbol): boolean { + // tslint:disable-next-line no-bitwise + return !!((symbol.flags & ts.SymbolFlags.Alias) || (symbol.flags & ts.SymbolFlags.AliasExcludes)); + } } diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index b99730fdc..afd045917 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -4,20 +4,23 @@ import { GetDescriptor } from '../descriptor'; import { IsTypescriptType } from '../tsLibs/typecriptLibs'; import { GetMockCall } from './mockCall'; import { GetMockProperty } from './mockProperty'; +import { PropertyLike } from './propertyLike'; +import { SignatureLike } from './signatureLike'; export function GetMockPropertiesFromSymbol(propertiesSymbol: ts.Symbol[], signatures: ReadonlyArray, scope: Scope): ts.Expression { - const properties: ts.Declaration[] = propertiesSymbol.map((prop: ts.Symbol) => { + const properties: PropertyLike[] = propertiesSymbol.map((prop: ts.Symbol) => { return prop.declarations[0]; - }); - const signaturesDeclarations: ts.Declaration[] = signatures.map((signature: ts.Signature) => { + }) as PropertyLike[]; + + const signaturesDeclarations: SignatureLike[] = signatures.map((signature: ts.Signature) => { return signature.declaration; }); return GetMockPropertiesFromDeclarations(properties, signaturesDeclarations, scope); } -export function GetMockPropertiesFromDeclarations(list: ReadonlyArray, signatures: ReadonlyArray, scope: Scope): ts.CallExpression { - const propertiesFilter: ts.Declaration[] = list.filter((member: ts.PropertySignature) => { +export function GetMockPropertiesFromDeclarations(list: ReadonlyArray, signatures: ReadonlyArray, scope: Scope): ts.CallExpression { + const propertiesFilter: PropertyLike[] = list.filter((member: PropertyLike) => { const hasModifiers: boolean = !!member.modifiers; if (IsTypescriptType(member)) { // This is a current workaround to safe fail extends of TypescriptLibs @@ -34,7 +37,7 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray { + (member: PropertyLike): ts.PropertyAssignment => { return GetMockProperty(member, scope); }, ); diff --git a/src/transformer/descriptor/mock/mockProperty.ts b/src/transformer/descriptor/mock/mockProperty.ts index b5ab15788..856bcebea 100644 --- a/src/transformer/descriptor/mock/mockProperty.ts +++ b/src/transformer/descriptor/mock/mockProperty.ts @@ -4,8 +4,9 @@ import { Scope } from '../../scope/scope'; import { GetDescriptor } from '../descriptor'; import { TypescriptHelper } from '../helper/helper'; import { GetMockInternalValuesName, GetMockSetParameterName } from './mockDeclarationName'; +import { PropertyLike } from './propertyLike'; -export function GetMockProperty(member: ts.PropertySignature, scope: Scope): ts.PropertyAssignment { +export function GetMockProperty(member: PropertyLike, scope: Scope): ts.PropertyAssignment { const descriptor: ts.Expression = GetDescriptor(member, scope); const propertyName: string = TypescriptHelper.GetStringPropertyName(member.name); diff --git a/src/transformer/descriptor/mock/propertyLike.ts b/src/transformer/descriptor/mock/propertyLike.ts new file mode 100644 index 000000000..3700f6195 --- /dev/null +++ b/src/transformer/descriptor/mock/propertyLike.ts @@ -0,0 +1,7 @@ +import * as ts from 'typescript'; + +export type PropertyLike = ts.PropertyDeclaration | ts.PropertySignature | ts.MethodSignature; + +export function isPropertyLike(prop: ts.Node): prop is PropertyLike { + return prop.kind === ts.SyntaxKind.PropertyDeclaration || prop.kind === ts.SyntaxKind.PropertySignature || prop.kind === ts.SyntaxKind.MethodSignature; +} diff --git a/src/transformer/descriptor/mock/signatureLike.ts b/src/transformer/descriptor/mock/signatureLike.ts new file mode 100644 index 000000000..196a29696 --- /dev/null +++ b/src/transformer/descriptor/mock/signatureLike.ts @@ -0,0 +1,7 @@ +import * as ts from 'typescript'; + +export type SignatureLike = ts.CallSignatureDeclaration | ts.ConstructSignatureDeclaration; + +export function isSignatureLike(prop: ts.Node): prop is SignatureLike { + return prop.kind === ts.SyntaxKind.CallSignature || prop.kind === ts.SyntaxKind.ConstructSignature; +} diff --git a/src/transformer/descriptor/module/module.ts b/src/transformer/descriptor/module/module.ts new file mode 100644 index 000000000..1eecb8c0f --- /dev/null +++ b/src/transformer/descriptor/module/module.ts @@ -0,0 +1,33 @@ +import * as ts from 'typescript'; +import { TypescriptCreator } from '../../helper/creator'; +import { Scope } from '../../scope/scope'; +import { TypeChecker } from '../../typeChecker/typeChecker'; +import { GetDescriptor } from '../descriptor'; +import { TypescriptHelper } from '../helper/helper'; +import { GetMockPropertiesFromDeclarations, PropertyLike } from '../mock/mockProperties'; + +export function GetModuleDescriptor(node: ts.NamedDeclaration, scope: Scope): ts.Expression { + const typeChecker: ts.TypeChecker = TypeChecker(); + + const symbolAlias: ts.Symbol = typeChecker.getSymbolAtLocation(node.name); + const symbol: ts.Symbol = typeChecker.getAliasedSymbol(symbolAlias); + const externalModuleDeclaration: ts.NamedDeclaration = symbol.declarations[0]; + + if (ts.isSourceFile(externalModuleDeclaration) || ts.isModuleDeclaration(externalModuleDeclaration)) { + const moduleExports: ts.Symbol[] = typeChecker.getExportsOfModule(symbol); + + const properties: PropertyLike[] = moduleExports.map((prop: ts.Symbol): PropertyLike => { + const originalSymbol: ts.Symbol = TypescriptHelper.GetAliasedSymbolSafe(prop); + const originalDeclaration: ts.NamedDeclaration = originalSymbol.declarations[0]; + const declaration: ts.Declaration = prop.declarations[0]; + if (ts.isExportAssignment(declaration)) { + return TypescriptCreator.createProperty('default', ts.createTypeQueryNode(originalDeclaration.name as ts.Identifier)); + } + return TypescriptCreator.createProperty(originalDeclaration.name as ts.Identifier, ts.createTypeQueryNode(originalDeclaration.name as ts.Identifier)); + }); + + return GetMockPropertiesFromDeclarations(properties, [], scope); + } + + return GetDescriptor(ts.createTypeQueryNode(externalModuleDeclaration.name as ts.Identifier), scope); +} diff --git a/src/transformer/descriptor/properties/properties.ts b/src/transformer/descriptor/properties/properties.ts index 2dd7b821a..832496e36 100644 --- a/src/transformer/descriptor/properties/properties.ts +++ b/src/transformer/descriptor/properties/properties.ts @@ -2,7 +2,12 @@ import { SignatureKind } from 'typescript'; import * as ts from 'typescript'; import { Scope } from '../../scope/scope'; import { TypeChecker } from '../../typeChecker/typeChecker'; -import { GetMockPropertiesFromDeclarations, GetMockPropertiesFromSymbol } from '../mock/mockProperties'; +import { + GetMockPropertiesFromDeclarations, + GetMockPropertiesFromSymbol, +} from '../mock/mockProperties'; +import { isPropertyLike, PropertyLike } from '../mock/propertyLike'; +import { isSignatureLike, SignatureLike } from '../mock/signatureLike'; export function GetProperties(node: ts.Node, scope: Scope): ts.Expression { const typeChecker: ts.TypeChecker = TypeChecker(); @@ -23,15 +28,17 @@ export function GetProperties(node: ts.Node, scope: Scope): ts.Expression { export function GetPropertiesFromMembers(node: ts.TypeLiteralNode, scope: Scope): ts.Expression { const members: ts.NodeArray = node.members; - const signatures: Array = []; - const properties: Array = []; + const signatures: Array = []; + const properties: Array = []; // tslint:disable-next-line for (let i: number = 0; i < members.length; i++) { - if (members[i].kind === ts.SyntaxKind.CallSignature || members[i].kind === ts.SyntaxKind.ConstructSignature) { - signatures.push(members[i]); - } else if (members[i].kind === ts.SyntaxKind.PropertyDeclaration || members[i].kind === ts.SyntaxKind.PropertySignature || members[i].kind === ts.SyntaxKind.MethodSignature) { - properties.push(members[i]); + const declaration: ts.NamedDeclaration = members[i]; + + if (isSignatureLike(declaration)) { + signatures.push(declaration); + } else if (isPropertyLike(declaration)) { + properties.push(declaration); } } diff --git a/src/transformer/descriptor/typeQuery/enumTypeQuery.ts b/src/transformer/descriptor/typeQuery/enumTypeQuery.ts index ce6626eae..d2616a253 100644 --- a/src/transformer/descriptor/typeQuery/enumTypeQuery.ts +++ b/src/transformer/descriptor/typeQuery/enumTypeQuery.ts @@ -3,7 +3,6 @@ import { Scope } from '../../scope/scope'; export function GetTypeofEnumDescriptor(enumDeclaration: ts.EnumDeclaration, scope: Scope): ts.Expression { enumDeclaration.modifiers = undefined; - enumDeclaration.name = ts.createFileLevelUniqueName(enumDeclaration.name.text); return ts.createArrowFunction( undefined, diff --git a/src/transformer/descriptor/typeQuery/typeQuery.ts b/src/transformer/descriptor/typeQuery/typeQuery.ts index b7de4a290..a04977aa1 100644 --- a/src/transformer/descriptor/typeQuery/typeQuery.ts +++ b/src/transformer/descriptor/typeQuery/typeQuery.ts @@ -7,24 +7,13 @@ import { TypeChecker } from '../../typeChecker/typeChecker'; import { GetDescriptor } from '../descriptor'; import { TypescriptHelper } from '../helper/helper'; import { GetMethodDeclarationDescriptor } from '../method/methodDeclaration'; +import { GetModuleDescriptor } from '../module/module'; import { GetNullDescriptor } from '../null/null'; import { GetTypeReferenceDescriptor } from '../typeReference/typeReference'; export function GetTypeQueryDescriptor(node: ts.TypeQueryNode, scope: Scope): ts.Expression { const typeChecker: ts.TypeChecker = TypeChecker(); - /* - TODO: Find different workaround without casting to any - Cast to any is been done because getSymbolAtLocation doesn't work when the node is an inferred identifier of a type query of a type query - Use case is: - ``` - const myVar = MyEnum; - createMock(); - ``` - here `typeof myVar` is inferred `typeof MyEnum` and the `MyEnum` identifier doesn't play well with getSymbolAtLocation and it returns undefined. - */ - // tslint:disable-next-line no-any - const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node.exprName) || (node.exprName as any).symbol; - const declaration: ts.Declaration = TypescriptHelper.GetDeclarationFromSymbol(symbol); + const declaration: ts.Declaration = getTypeQueryDeclaration(node); switch (declaration.kind) { case ts.SyntaxKind.ClassDeclaration: @@ -34,12 +23,22 @@ export function GetTypeQueryDescriptor(node: ts.TypeQueryNode, scope: Scope): ts scope, ), ); + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + return GetTypeReferenceDescriptor( + ts.createTypeReferenceNode(node.exprName as ts.Identifier, undefined), + scope, + ); + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportEqualsDeclaration: + return GetModuleDescriptor(declaration, scope); case ts.SyntaxKind.EnumDeclaration: // TODO: Use following two lines when issue #17552 on typescript github is resolved (https://github.com/microsoft/TypeScript/issues/17552) // TheNewEmitResolver.ensureEmitOf(GetImportDeclarationOf(node.eprName as ts.Identifier); // return node.exprName as ts.Identifier; return GetMockFactoryCallTypeofEnum(declaration as ts.EnumDeclaration); case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodSignature: return GetMethodDeclarationDescriptor(declaration as ts.FunctionDeclaration, scope); case ts.SyntaxKind.VariableDeclaration: const typeNode: ts.TypeNode = (declaration as ts.VariableDeclaration).type || typeChecker.typeToTypeNode(typeChecker.getTypeFromTypeNode(node)); @@ -49,3 +48,26 @@ export function GetTypeQueryDescriptor(node: ts.TypeQueryNode, scope: Scope): ts return GetNullDescriptor(); } } + +function getTypeQueryDeclaration(node: ts.TypeQueryNode): ts.Declaration { + const typeChecker: ts.TypeChecker = TypeChecker(); + /* + TODO: Find different workaround without casting to any + Cast to any is been done because getSymbolAtLocation doesn't work when the node is an inferred identifier of a type query of a type query + Use case is: + ``` + const myVar = MyEnum; + createMock(); + ``` + here `typeof myVar` is inferred `typeof MyEnum` and the `MyEnum` identifier doesn't play well with getSymbolAtLocation and it returns undefined. + */ + // tslint:disable-next-line no-any + const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node.exprName as ts.Identifier) || (node.exprName as any).symbol; + const declaration: ts.Declaration = symbol.declarations[0]; + + if (ts.isImportEqualsDeclaration(declaration)) { + return declaration; + } + + return TypescriptHelper.GetDeclarationFromSymbol(symbol); +} diff --git a/src/transformer/helper/creator.ts b/src/transformer/helper/creator.ts index 1ca0fb9d2..d2d2ca8a2 100644 --- a/src/transformer/helper/creator.ts +++ b/src/transformer/helper/creator.ts @@ -1,3 +1,4 @@ +import { PropertyName } from 'typescript'; import * as ts from 'typescript'; export namespace TypescriptCreator { @@ -35,7 +36,7 @@ export namespace TypescriptCreator { return createProperty('', undefined); } - export function createProperty(propertyName: string, type: ts.TypeNode): ts.PropertyDeclaration { + export function createProperty(propertyName: string | PropertyName, type: ts.TypeNode): ts.PropertyDeclaration { return ts.createProperty([], [], propertyName, undefined, type, undefined); } diff --git a/test/transformer/descriptor/typeQuery/typeQuery.test.ts b/test/transformer/descriptor/typeQuery/typeQuery.test.ts index 02066ffdd..63a3bfc43 100644 --- a/test/transformer/descriptor/typeQuery/typeQuery.test.ts +++ b/test/transformer/descriptor/typeQuery/typeQuery.test.ts @@ -1,11 +1,18 @@ import { createMock } from 'ts-auto-mock'; -import { MyEnum } from '../../../playground/enums'; import { ImportInterface } from '../utils/interfaces/importInterface'; +import REQUIRE = require('../utils/typeQuery/typeQueryUtils'); +import REQUIRE_DEFAULT = require('../utils/interfaces/exportDefaultDeclaration'); +import REQUIRE_EQUAL = require('../utils/interfaces/exportEqualObject'); +import * as STAR from '../utils/typeQuery/typeQueryUtils'; +import * as STAR_DEFAULT from '../utils/interfaces/exportDefaultDeclaration'; import { ExportedClass, ExportedDeclaredClass, - exportedDeclaredFunction, ExportedEnum, - exportedFunction, WrapExportedClass, WrapExportedEnum, + exportedDeclaredFunction, + ExportedEnum, + exportedFunction, + WrapExportedClass, + WrapExportedEnum, } from '../utils/typeQuery/typeQueryUtils'; declare function functionDeclaration(): number; @@ -44,9 +51,9 @@ describe('typeQuery', () => { function func(): string { return 'ok'; } - + type Intersection = {} & typeof func; - + const functionMock: Intersection = createMock(); expect(functionMock).toBeUndefined(); @@ -63,7 +70,7 @@ describe('typeQuery', () => { expect(new classMock().prop).toEqual(''); }); - + it('should create a newable class for an imported class declaration', () => { const classMock: typeof ExportedDeclaredClass = createMock(); @@ -100,8 +107,8 @@ describe('typeQuery', () => { const enumMock: typeof Enum = createMock(); - expect(enumMock.A).toEqual(0); - expect(enumMock.B).toEqual('some'); + expect(enumMock.A).toEqual(Enum.A); + expect(enumMock.B).toEqual(Enum.B); }); it('should assign the imported enum to the mock', () => { @@ -156,9 +163,23 @@ describe('typeQuery', () => { expect(mock.A).toEqual(0); }); + it('should work for a method in an object', () => { + let aVariable: { + a(): string; + } = { + a: function(): string { + return "wow"; + } + }; + + const mock: typeof aVariable.a = createMock(); + + expect(mock()).toEqual(''); + }); + it('should return undefined for an intersection', () => { let aVariable: WrapExportedEnum; - + type Intersection = {} & typeof aVariable; const functionMock: Intersection = createMock(); @@ -166,6 +187,46 @@ describe('typeQuery', () => { expect(functionMock).toBeUndefined(); }); + describe('import star', () => { + it('should mock every materialisable export (no types or interfaces)', () => { + const mock: typeof STAR = createMock(); + + expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); + expect(new mock.ExportedClass().prop).toEqual(0); + expect(new mock.ExportedDeclaredClass().prop).toEqual(''); + expect(mock.exportedDeclaredFunction()).toEqual(''); + }); + + it('should mock the default', () => { + const mock: typeof STAR_DEFAULT = createMock(); + + expect(mock.default('input')).toBe(false); + }); + }); + + describe('import require', () => { + it('should mock every materialisable export (no types or interfaces)', () => { + const mock: typeof REQUIRE = createMock(); + + expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); + expect(new mock.ExportedClass().prop).toEqual(0); + expect(new mock.ExportedDeclaredClass().prop).toEqual(''); + expect(mock.exportedDeclaredFunction()).toEqual(''); + }); + + it('should mock the default', () => { + const mock: typeof REQUIRE_DEFAULT = createMock(); + + expect(mock.default('input')).toBe(false); + }); + + it('should mock the `export =`', () => { + const mock: typeof REQUIRE_EQUAL = createMock(); + + expect(new mock().prop).toBe(0); + }); + }); + describe('inferred type', () => { it('should work for inferred object', () => { const aVariable = { prop: 'asd' }; @@ -174,21 +235,21 @@ describe('typeQuery', () => { expect(mock.prop).toEqual(''); }); - + it('should work for enum', () => { - const aVariable = MyEnum; + const aVariable = ExportedEnum; const mock: typeof aVariable = createMock(); expect(mock.A).toEqual(0); }); - + it('should work for function call', () => { function test(value) { if(value) { return { prop: 'asd' }; } - + return { second: 7 }; } diff --git a/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts b/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts new file mode 100644 index 000000000..2d877e28f --- /dev/null +++ b/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts @@ -0,0 +1,3 @@ +declare function functionDeclarationDefault(path: string): boolean; + +export default functionDeclarationDefault; diff --git a/test/transformer/descriptor/utils/interfaces/exportEqualObject.ts b/test/transformer/descriptor/utils/interfaces/exportEqualObject.ts new file mode 100644 index 000000000..9b23dfb08 --- /dev/null +++ b/test/transformer/descriptor/utils/interfaces/exportEqualObject.ts @@ -0,0 +1,5 @@ +class IAmAClassExportedWithEqual { + prop: number; +} + +export = IAmAClassExportedWithEqual; From 1ed876b41043328cc25835656e1f289d23659923 Mon Sep 17 00:00:00 2001 From: Pmyl Date: Thu, 2 Jan 2020 18:34:07 +0000 Subject: [PATCH 2/3] fix --- src/transformer/descriptor/mock/mockProperties.ts | 2 +- src/transformer/descriptor/module/module.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transformer/descriptor/mock/mockProperties.ts b/src/transformer/descriptor/mock/mockProperties.ts index afd045917..1dbe897c3 100644 --- a/src/transformer/descriptor/mock/mockProperties.ts +++ b/src/transformer/descriptor/mock/mockProperties.ts @@ -14,7 +14,7 @@ export function GetMockPropertiesFromSymbol(propertiesSymbol: ts.Symbol[], signa const signaturesDeclarations: SignatureLike[] = signatures.map((signature: ts.Signature) => { return signature.declaration; - }); + }) as SignatureLike[]; return GetMockPropertiesFromDeclarations(properties, signaturesDeclarations, scope); } diff --git a/src/transformer/descriptor/module/module.ts b/src/transformer/descriptor/module/module.ts index 1eecb8c0f..4c356eaba 100644 --- a/src/transformer/descriptor/module/module.ts +++ b/src/transformer/descriptor/module/module.ts @@ -4,7 +4,8 @@ import { Scope } from '../../scope/scope'; import { TypeChecker } from '../../typeChecker/typeChecker'; import { GetDescriptor } from '../descriptor'; import { TypescriptHelper } from '../helper/helper'; -import { GetMockPropertiesFromDeclarations, PropertyLike } from '../mock/mockProperties'; +import { GetMockPropertiesFromDeclarations } from '../mock/mockProperties'; +import { PropertyLike } from '../mock/propertyLike'; export function GetModuleDescriptor(node: ts.NamedDeclaration, scope: Scope): ts.Expression { const typeChecker: ts.TypeChecker = TypeChecker(); From ebfcdf92230884e6e1f7a86b850f26cf8befc24e Mon Sep 17 00:00:00 2001 From: Pmyl Date: Fri, 3 Jan 2020 23:02:06 +0000 Subject: [PATCH 3/3] make sure type query of module works for inferred variable type --- src/transformer/descriptor/module/module.ts | 3 +- .../descriptor/typeQuery/typeQuery.ts | 130 +++--- .../descriptor/typeQuery/typeQuery.test.ts | 370 +++++++++--------- .../interfaces/exportDefaultDeclaration.ts | 7 +- 4 files changed, 275 insertions(+), 235 deletions(-) diff --git a/src/transformer/descriptor/module/module.ts b/src/transformer/descriptor/module/module.ts index 4c356eaba..897f1c1fe 100644 --- a/src/transformer/descriptor/module/module.ts +++ b/src/transformer/descriptor/module/module.ts @@ -6,6 +6,7 @@ import { GetDescriptor } from '../descriptor'; import { TypescriptHelper } from '../helper/helper'; import { GetMockPropertiesFromDeclarations } from '../mock/mockProperties'; import { PropertyLike } from '../mock/propertyLike'; +import { GetTypeQueryDescriptorFromDeclaration } from '../typeQuery/typeQuery'; export function GetModuleDescriptor(node: ts.NamedDeclaration, scope: Scope): ts.Expression { const typeChecker: ts.TypeChecker = TypeChecker(); @@ -30,5 +31,5 @@ export function GetModuleDescriptor(node: ts.NamedDeclaration, scope: Scope): ts return GetMockPropertiesFromDeclarations(properties, [], scope); } - return GetDescriptor(ts.createTypeQueryNode(externalModuleDeclaration.name as ts.Identifier), scope); + return GetTypeQueryDescriptorFromDeclaration(externalModuleDeclaration, scope); } diff --git a/src/transformer/descriptor/typeQuery/typeQuery.ts b/src/transformer/descriptor/typeQuery/typeQuery.ts index a04977aa1..cd7632283 100644 --- a/src/transformer/descriptor/typeQuery/typeQuery.ts +++ b/src/transformer/descriptor/typeQuery/typeQuery.ts @@ -9,65 +9,89 @@ import { TypescriptHelper } from '../helper/helper'; import { GetMethodDeclarationDescriptor } from '../method/methodDeclaration'; import { GetModuleDescriptor } from '../module/module'; import { GetNullDescriptor } from '../null/null'; +import { GetType } from '../type/type'; import { GetTypeReferenceDescriptor } from '../typeReference/typeReference'; export function GetTypeQueryDescriptor(node: ts.TypeQueryNode, scope: Scope): ts.Expression { - const typeChecker: ts.TypeChecker = TypeChecker(); - const declaration: ts.Declaration = getTypeQueryDeclaration(node); + const declaration: ts.NamedDeclaration = getTypeQueryDeclaration(node); + return GetTypeQueryDescriptorFromDeclaration(declaration, scope); +} + +export function GetTypeQueryDescriptorFromDeclaration(declaration: ts.NamedDeclaration, scope: Scope): ts.Expression { + const typeChecker: ts.TypeChecker = TypeChecker(); + + switch (declaration.kind) { + case ts.SyntaxKind.ClassDeclaration: + return TypescriptCreator.createFunctionExpressionReturn( + GetTypeReferenceDescriptor( + ts.createTypeReferenceNode(declaration.name as ts.Identifier, undefined), + scope, + ), + ); + case ts.SyntaxKind.TypeAliasDeclaration: + case ts.SyntaxKind.InterfaceDeclaration: + return GetTypeReferenceDescriptor( + ts.createTypeReferenceNode(declaration.name as ts.Identifier, undefined), + scope, + ); + case ts.SyntaxKind.NamespaceImport: + case ts.SyntaxKind.ImportEqualsDeclaration: + return GetModuleDescriptor(declaration, scope); + case ts.SyntaxKind.EnumDeclaration: + // TODO: Use following two lines when issue #17552 on typescript github is resolved (https://github.com/microsoft/TypeScript/issues/17552) + // TheNewEmitResolver.ensureEmitOf(GetImportDeclarationOf(node.eprName as ts.Identifier); + // return node.exprName as ts.Identifier; + return GetMockFactoryCallTypeofEnum(declaration as ts.EnumDeclaration); + case ts.SyntaxKind.FunctionDeclaration: + case ts.SyntaxKind.MethodSignature: + return GetMethodDeclarationDescriptor(declaration as ts.FunctionDeclaration, scope); + case ts.SyntaxKind.VariableDeclaration: + const variable: ts.VariableDeclaration = declaration as ts.VariableDeclaration; + + if (variable.type) { + return GetDescriptor(variable.type, scope); + } + + const inferredType: ts.Node = GetType(variable.initializer, scope); + const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(inferredType); + + if (symbol) { + const inferredTypeDeclaration: ts.NamedDeclaration = getTypeQueryDeclarationFromSymbol(symbol); + + return GetTypeQueryDescriptorFromDeclaration(inferredTypeDeclaration, scope); + } else { + return GetDescriptor(inferredType, scope); + } + default: + TransformerLogger().typeNotSupported(`TypeQuery of ${ts.SyntaxKind[declaration.kind]}`); + return GetNullDescriptor(); + } +} + +function getTypeQueryDeclaration(node: ts.TypeQueryNode): ts.NamedDeclaration { + const typeChecker: ts.TypeChecker = TypeChecker(); + /* + TODO: Find different workaround without casting to any + Cast to any is been done because getSymbolAtLocation doesn't work when the node is an inferred identifier of a type query of a type query + Use case is: + ``` + const myVar = MyEnum; + createMock(); + ``` + here `typeof myVar` is inferred `typeof MyEnum` and the `MyEnum` identifier doesn't play well with getSymbolAtLocation and it returns undefined. + */ + // tslint:disable-next-line no-any + const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node.exprName) || (node.exprName as any).symbol; - switch (declaration.kind) { - case ts.SyntaxKind.ClassDeclaration: - return TypescriptCreator.createFunctionExpressionReturn( - GetTypeReferenceDescriptor( - ts.createTypeReferenceNode(node.exprName as ts.Identifier, undefined), - scope, - ), - ); - case ts.SyntaxKind.TypeAliasDeclaration: - case ts.SyntaxKind.InterfaceDeclaration: - return GetTypeReferenceDescriptor( - ts.createTypeReferenceNode(node.exprName as ts.Identifier, undefined), - scope, - ); - case ts.SyntaxKind.NamespaceImport: - case ts.SyntaxKind.ImportEqualsDeclaration: - return GetModuleDescriptor(declaration, scope); - case ts.SyntaxKind.EnumDeclaration: - // TODO: Use following two lines when issue #17552 on typescript github is resolved (https://github.com/microsoft/TypeScript/issues/17552) - // TheNewEmitResolver.ensureEmitOf(GetImportDeclarationOf(node.eprName as ts.Identifier); - // return node.exprName as ts.Identifier; - return GetMockFactoryCallTypeofEnum(declaration as ts.EnumDeclaration); - case ts.SyntaxKind.FunctionDeclaration: - case ts.SyntaxKind.MethodSignature: - return GetMethodDeclarationDescriptor(declaration as ts.FunctionDeclaration, scope); - case ts.SyntaxKind.VariableDeclaration: - const typeNode: ts.TypeNode = (declaration as ts.VariableDeclaration).type || typeChecker.typeToTypeNode(typeChecker.getTypeFromTypeNode(node)); - return GetDescriptor(typeNode, scope); - default: - TransformerLogger().typeNotSupported(`TypeQuery of ${ts.SyntaxKind[declaration.kind]}`); - return GetNullDescriptor(); - } + return getTypeQueryDeclarationFromSymbol(symbol); } -function getTypeQueryDeclaration(node: ts.TypeQueryNode): ts.Declaration { - const typeChecker: ts.TypeChecker = TypeChecker(); - /* - TODO: Find different workaround without casting to any - Cast to any is been done because getSymbolAtLocation doesn't work when the node is an inferred identifier of a type query of a type query - Use case is: - ``` - const myVar = MyEnum; - createMock(); - ``` - here `typeof myVar` is inferred `typeof MyEnum` and the `MyEnum` identifier doesn't play well with getSymbolAtLocation and it returns undefined. - */ - // tslint:disable-next-line no-any - const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node.exprName as ts.Identifier) || (node.exprName as any).symbol; - const declaration: ts.Declaration = symbol.declarations[0]; +function getTypeQueryDeclarationFromSymbol(symbol: ts.Symbol): ts.NamedDeclaration { + const declaration: ts.Declaration = symbol.declarations[0]; - if (ts.isImportEqualsDeclaration(declaration)) { - return declaration; - } + if (ts.isImportEqualsDeclaration(declaration)) { + return declaration; + } - return TypescriptHelper.GetDeclarationFromSymbol(symbol); + return TypescriptHelper.GetDeclarationFromSymbol(symbol); } diff --git a/test/transformer/descriptor/typeQuery/typeQuery.test.ts b/test/transformer/descriptor/typeQuery/typeQuery.test.ts index 63a3bfc43..6675761c7 100644 --- a/test/transformer/descriptor/typeQuery/typeQuery.test.ts +++ b/test/transformer/descriptor/typeQuery/typeQuery.test.ts @@ -1,264 +1,276 @@ import { createMock } from 'ts-auto-mock'; +import * as STAR_DEFAULT from '../utils/interfaces/exportDefaultDeclaration'; import { ImportInterface } from '../utils/interfaces/importInterface'; -import REQUIRE = require('../utils/typeQuery/typeQueryUtils'); -import REQUIRE_DEFAULT = require('../utils/interfaces/exportDefaultDeclaration'); -import REQUIRE_EQUAL = require('../utils/interfaces/exportEqualObject'); import * as STAR from '../utils/typeQuery/typeQueryUtils'; -import * as STAR_DEFAULT from '../utils/interfaces/exportDefaultDeclaration'; import { - ExportedClass, - ExportedDeclaredClass, - exportedDeclaredFunction, - ExportedEnum, - exportedFunction, - WrapExportedClass, - WrapExportedEnum, + ExportedClass, + ExportedDeclaredClass, + exportedDeclaredFunction, + ExportedEnum, + exportedFunction, + WrapExportedClass, + WrapExportedEnum, } from '../utils/typeQuery/typeQueryUtils'; +import REQUIRE_DEFAULT = require('../utils/interfaces/exportDefaultDeclaration'); +import REQUIRE_EQUAL = require('../utils/interfaces/exportEqualObject'); +import REQUIRE = require('../utils/typeQuery/typeQueryUtils'); declare function functionDeclaration(): number; describe('typeQuery', () => { - describe('for function', () => { - it('should assign the function mock for a function declaration', () => { - const functionMock: typeof functionDeclaration = createMock(); + describe('for function', () => { + it('should assign the function mock for a function declaration', () => { + const functionMock: typeof functionDeclaration = createMock(); - expect(functionMock()).toEqual(0); - }); + expect(functionMock()).toEqual(0); + }); - it('should assign the function mock for an function declaration with body', () => { - function func(): string { - return 'ok'; - } + it('should assign the function mock for an function declaration with body', () => { + function func(): string { + return 'ok'; + } - const functionMock: typeof func = createMock(); + const functionMock: typeof func = createMock(); - expect(functionMock()).toEqual(''); - }); + expect(functionMock()).toEqual(''); + }); - it('should assign the function mock for an imported function declaration', () => { - const functionMock: typeof exportedDeclaredFunction = createMock(); + it('should assign the function mock for an imported function declaration', () => { + const functionMock: typeof exportedDeclaredFunction = createMock(); - expect(functionMock()).toEqual(''); - }); + expect(functionMock()).toEqual(''); + }); - it('should assign the function mock for an imported function declaration with body', () => { - const functionMock: typeof exportedFunction = createMock(); + it('should assign the function mock for an imported function declaration with body', () => { + const functionMock: typeof exportedFunction = createMock(); - expect(functionMock()).toEqual(0); - }); + expect(functionMock()).toEqual(0); + }); - it('should return undefined for an intersection', () => { - function func(): string { - return 'ok'; - } + it('should return undefined for an intersection', () => { + function func(): string { + return 'ok'; + } - type Intersection = {} & typeof func; + type Intersection = {} & typeof func; - const functionMock: Intersection = createMock(); + const functionMock: Intersection = createMock(); - expect(functionMock).toBeUndefined(); + expect(functionMock).toBeUndefined(); + }); }); - }); - describe('for class', () => { - it('should create a newable class for a class declaration in file', () => { - class MyClass { - prop: string; - } + describe('for class', () => { + it('should create a newable class for a class declaration in file', () => { + class MyClass { + prop: string; + } - const classMock: typeof MyClass = createMock(); + const classMock: typeof MyClass = createMock(); - expect(new classMock().prop).toEqual(''); - }); + expect(new classMock().prop).toEqual(''); + }); - it('should create a newable class for an imported class declaration', () => { - const classMock: typeof ExportedDeclaredClass = createMock(); + it('should create a newable class for an imported class declaration', () => { + const classMock: typeof ExportedDeclaredClass = createMock(); - expect(new classMock().prop).toEqual(''); - }); + expect(new classMock().prop).toEqual(''); + }); - it('should create a newable class for an imported class', () => { - const classMock: typeof ExportedClass = createMock(); + it('should create a newable class for an imported class', () => { + const classMock: typeof ExportedClass = createMock(); - expect(new classMock().prop).toEqual(0); - }); + expect(new classMock().prop).toEqual(0); + }); - it('should create a newable class for an wrapped imported typeof class', () => { - const classMock: WrapExportedClass = createMock(); + it('should create a newable class for an wrapped imported typeof class', () => { + const classMock: WrapExportedClass = createMock(); - expect(new classMock().prop).toEqual(0); - }); + expect(new classMock().prop).toEqual(0); + }); - it('should return undefined for an intersection', () => { - type Intersection = {} & WrapExportedClass; + it('should return undefined for an intersection', () => { + type Intersection = {} & WrapExportedClass; - const functionMock: Intersection = createMock(); + const functionMock: Intersection = createMock(); - expect(functionMock).toBeUndefined(); + expect(functionMock).toBeUndefined(); + }); }); - }); - describe('for enum', () => { - it('should assign the enum to the mock', () => { - enum Enum { - A, - B = 'some' - } + describe('for enum', () => { + it('should assign the enum to the mock', () => { + enum Enum { + A, + B = 'some' + } - const enumMock: typeof Enum = createMock(); + const enumMock: typeof Enum = createMock(); - expect(enumMock.A).toEqual(Enum.A); - expect(enumMock.B).toEqual(Enum.B); - }); + expect(enumMock.A).toEqual(Enum.A); + expect(enumMock.B).toEqual(Enum.B); + }); - it('should assign the imported enum to the mock', () => { - const enumMock: typeof ExportedEnum = createMock(); + it('should assign the imported enum to the mock', () => { + const enumMock: typeof ExportedEnum = createMock(); - expect(enumMock.A).toEqual(0); - expect(enumMock.B).toEqual('B'); - expect(enumMock.C).toEqual('MaybeC'); - }); + expect(enumMock.A).toEqual(0); + expect(enumMock.B).toEqual('B'); + expect(enumMock.C).toEqual('MaybeC'); + }); - it('should assign the imported enum to the mock when typeof wrapped in a type', () => { - type WrapEnum = typeof ExportedEnum; + it('should assign the imported enum to the mock when typeof wrapped in a type', () => { + type WrapEnum = typeof ExportedEnum; - const enumMock: WrapEnum = createMock(); + const enumMock: WrapEnum = createMock(); - expect(enumMock.A).toEqual(0); - expect(enumMock.B).toEqual('B'); - expect(enumMock.C).toEqual('MaybeC'); - }); + expect(enumMock.A).toEqual(0); + expect(enumMock.B).toEqual('B'); + expect(enumMock.C).toEqual('MaybeC'); + }); - it('should assign the enum to the mock when importing enum wrapper', () => { - const enumMock: WrapExportedEnum = createMock(); + it('should assign the enum to the mock when importing enum wrapper', () => { + const enumMock: WrapExportedEnum = createMock(); - expect(enumMock.A).toEqual(0); - expect(enumMock.B).toEqual('B'); - expect(enumMock.C).toEqual('MaybeC'); - }); + expect(enumMock.A).toEqual(0); + expect(enumMock.B).toEqual('B'); + expect(enumMock.C).toEqual('MaybeC'); + }); - it('should return undefined for an intersection', () => { - type Intersection = {} & WrapExportedEnum; + it('should return undefined for an intersection', () => { + type Intersection = {} & WrapExportedEnum; - const functionMock: Intersection = createMock(); + const functionMock: Intersection = createMock(); - expect(functionMock).toBeUndefined(); + expect(functionMock).toBeUndefined(); + }); }); - }); - describe('for variable', () => { - it('should create the imported interface mock from the type of a variable', () => { - let aVariable: ImportInterface; + describe('for variable', () => { + it('should create the imported interface mock from the type of a variable', () => { + let aVariable: ImportInterface; - const mock: typeof aVariable = createMock(); + const mock: typeof aVariable = createMock(); - expect(mock.a.b).toEqual(''); - }); + expect(mock.a.b).toEqual(''); + }); - it('should create the imported typeof enum mock from the type of a variable', () => { - let aVariable: WrapExportedEnum; + it('should create the imported typeof enum mock from the type of a variable', () => { + let aVariable: WrapExportedEnum; - const mock: typeof aVariable = createMock(); + const mock: typeof aVariable = createMock(); - expect(mock.A).toEqual(0); - }); + expect(mock.A).toEqual(0); + }); - it('should work for a method in an object', () => { - let aVariable: { - a(): string; - } = { - a: function(): string { - return "wow"; - } - }; + it('should work for a method in an object', () => { + let aVariable: { + a(): string; + } = { + a: function (): string { + return "wow"; + }, + }; - const mock: typeof aVariable.a = createMock(); + const mock: typeof aVariable.a = createMock(); - expect(mock()).toEqual(''); - }); + expect(mock()).toEqual(''); + }); - it('should return undefined for an intersection', () => { - let aVariable: WrapExportedEnum; + it('should return undefined for an intersection', () => { + let aVariable: WrapExportedEnum; - type Intersection = {} & typeof aVariable; + type Intersection = {} & typeof aVariable; - const functionMock: Intersection = createMock(); + const functionMock: Intersection = createMock(); - expect(functionMock).toBeUndefined(); - }); + expect(functionMock).toBeUndefined(); + }); - describe('import star', () => { - it('should mock every materialisable export (no types or interfaces)', () => { - const mock: typeof STAR = createMock(); + describe('import star', () => { + it('should mock every materialisable export (no types or interfaces)', () => { + const mock: typeof STAR = createMock(); - expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); - expect(new mock.ExportedClass().prop).toEqual(0); - expect(new mock.ExportedDeclaredClass().prop).toEqual(''); - expect(mock.exportedDeclaredFunction()).toEqual(''); - }); + expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); + expect(new mock.ExportedClass().prop).toEqual(0); + expect(new mock.ExportedDeclaredClass().prop).toEqual(''); + expect(mock.exportedDeclaredFunction()).toEqual(''); + }); - it('should mock the default', () => { - const mock: typeof STAR_DEFAULT = createMock(); + it('should mock the default', () => { + const mock: typeof STAR_DEFAULT = createMock(); - expect(mock.default('input')).toBe(false); - }); - }); + expect(mock.functionDefault('input')).toBe(false); + expect(mock.default('input')).toBe(false); + expect(mock.functionNotDefault('input')).toBe(false); + }); + }); - describe('import require', () => { - it('should mock every materialisable export (no types or interfaces)', () => { - const mock: typeof REQUIRE = createMock(); + describe('import require', () => { + it('should mock every materialisable export (no types or interfaces)', () => { + const mock: typeof REQUIRE = createMock(); - expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); - expect(new mock.ExportedClass().prop).toEqual(0); - expect(new mock.ExportedDeclaredClass().prop).toEqual(''); - expect(mock.exportedDeclaredFunction()).toEqual(''); - }); + expect(mock.ExportedEnum.A).toBe(ExportedEnum.A); + expect(new mock.ExportedClass().prop).toEqual(0); + expect(new mock.ExportedDeclaredClass().prop).toEqual(''); + expect(mock.exportedDeclaredFunction()).toEqual(''); + }); - it('should mock the default', () => { - const mock: typeof REQUIRE_DEFAULT = createMock(); + it('should mock the default', () => { + const mock: typeof REQUIRE_DEFAULT = createMock(); - expect(mock.default('input')).toBe(false); - }); + expect(mock.functionDefault('input')).toBe(false); + expect(mock.default('input')).toBe(false); + expect(mock.functionNotDefault('input')).toBe(false); + }); - it('should mock the `export =`', () => { - const mock: typeof REQUIRE_EQUAL = createMock(); + it('should mock the `export =`', () => { + const mock: typeof REQUIRE_EQUAL = createMock(); - expect(new mock().prop).toBe(0); - }); - }); + expect(new mock().prop).toBe(0); + }); + }); + + describe('inferred type', () => { + it('should work for inferred object', () => { + const aVariable = {prop: 'asd'}; + + const mock: typeof aVariable = createMock(); + + expect(mock.prop).toEqual('asd'); + }); - describe('inferred type', () => { - it('should work for inferred object', () => { - const aVariable = { prop: 'asd' }; + it('should work for enum', () => { + const aVariable = ExportedEnum; - const mock: typeof aVariable = createMock(); + const mock: typeof aVariable = createMock(); - expect(mock.prop).toEqual(''); - }); + expect(mock.A).toEqual(0); + }); - it('should work for enum', () => { - const aVariable = ExportedEnum; + it('should work for function call', () => { + function test(value) { + if (value) { + return {prop: 'asd'}; + } - const mock: typeof aVariable = createMock(); + return {second: 7}; + } - expect(mock.A).toEqual(0); - }); + const aVariable = test(true); - it('should work for function call', () => { - function test(value) { - if(value) { - return { prop: 'asd' }; - } + const mock: typeof aVariable = createMock(); - return { second: 7 }; - } + expect(mock.prop).toEqual('asd'); + }); - const aVariable = test(true); + it('should work for module', () => { + const aVariable = STAR; - const mock: typeof aVariable = createMock(); + const mock: typeof aVariable = createMock(); - expect(mock.prop).toEqual(''); - }); + expect(mock.ExportedEnum.A).toEqual(ExportedEnum.A); + }); + }); }); - }); }); diff --git a/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts b/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts index 2d877e28f..684061c05 100644 --- a/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts +++ b/test/transformer/descriptor/utils/interfaces/exportDefaultDeclaration.ts @@ -1,3 +1,6 @@ -declare function functionDeclarationDefault(path: string): boolean; +export function functionDefault(path: string): boolean { + return false; +} -export default functionDeclarationDefault; +export const functionNotDefault = functionDefault; +export default functionDefault;