Skip to content

Commit

Permalink
feat(typeQuery): add support for typeof of an imported module (#128)
Browse files Browse the repository at this point in the history
* add support for typeof of an imported module

* fix

* make sure type query of module works for inferred variable type
  • Loading branch information
Pmyl committed Jan 5, 2020
1 parent 196ed2d commit a9e25a3
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 193 deletions.
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

0 comments on commit a9e25a3

Please sign in to comment.