Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(typeQuery): add support for typeof of an imported module #128

Merged
merged 4 commits into from
Jan 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/transformer/descriptor/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
17 changes: 10 additions & 7 deletions src/transformer/descriptor/mock/mockProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ts.Signature>, 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;
});
}) as SignatureLike[];

return GetMockPropertiesFromDeclarations(properties, signaturesDeclarations, scope);
}

export function GetMockPropertiesFromDeclarations(list: ReadonlyArray<ts.Declaration>, signatures: ReadonlyArray<ts.Declaration>, scope: Scope): ts.CallExpression {
const propertiesFilter: ts.Declaration[] = list.filter((member: ts.PropertySignature) => {
export function GetMockPropertiesFromDeclarations(list: ReadonlyArray<PropertyLike>, signatures: ReadonlyArray<SignatureLike>, 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
Expand All @@ -34,7 +37,7 @@ export function GetMockPropertiesFromDeclarations(list: ReadonlyArray<ts.Declara
});

const accessorDeclaration: ts.PropertyAssignment[] = propertiesFilter.map(
(member: ts.PropertySignature): ts.PropertyAssignment => {
(member: PropertyLike): ts.PropertyAssignment => {
return GetMockProperty(member, scope);
},
);
Expand Down
3 changes: 2 additions & 1 deletion src/transformer/descriptor/mock/mockProperty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/transformer/descriptor/mock/propertyLike.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/transformer/descriptor/mock/signatureLike.ts
Original file line number Diff line number Diff line change
@@ -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;
}
35 changes: 35 additions & 0 deletions src/transformer/descriptor/module/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 } 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();

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 GetTypeQueryDescriptorFromDeclaration(externalModuleDeclaration, scope);
}
21 changes: 14 additions & 7 deletions src/transformer/descriptor/properties/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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<ts.NamedDeclaration> = node.members;
const signatures: Array<ts.Declaration> = [];
const properties: Array<ts.Declaration> = [];
const signatures: Array<SignatureLike> = [];
const properties: Array<PropertyLike> = [];

// 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);
}
}

Expand Down
1 change: 0 additions & 1 deletion src/transformer/descriptor/typeQuery/enumTypeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
120 changes: 83 additions & 37 deletions src/transformer/descriptor/typeQuery/typeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,91 @@ 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 { GetType } from '../type/type';
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<typeof myVar>();
```
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);

switch (declaration.kind) {
case ts.SyntaxKind.ClassDeclaration:
return TypescriptCreator.createFunctionExpressionReturn(
GetTypeReferenceDescriptor(
ts.createTypeReferenceNode(node.exprName as ts.Identifier, undefined),
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:
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();
}
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<typeof myVar>();
```
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;

return getTypeQueryDeclarationFromSymbol(symbol);
}

function getTypeQueryDeclarationFromSymbol(symbol: ts.Symbol): ts.NamedDeclaration {
const declaration: ts.Declaration = symbol.declarations[0];

if (ts.isImportEqualsDeclaration(declaration)) {
return declaration;
}

return TypescriptHelper.GetDeclarationFromSymbol(symbol);
}
3 changes: 2 additions & 1 deletion src/transformer/helper/creator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PropertyName } from 'typescript';
import * as ts from 'typescript';

export namespace TypescriptCreator {
Expand Down Expand Up @@ -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);
}

Expand Down
Loading