Skip to content

Commit

Permalink
feat(typeQuery): add support to typeQuery (typeof) to fix #91 (#117)
Browse files Browse the repository at this point in the history
* some work on mockDefiner to speed up process and write some code for typeQuery

* feat(typeQuery): add support to typeQuery (typeof) to fix #91

* add documentation

* #91 add support for typeof variable
  • Loading branch information
Pmyl authored Dec 9, 2019
1 parent f23039d commit 84d1663
Show file tree
Hide file tree
Showing 18 changed files with 395 additions and 70 deletions.
27 changes: 26 additions & 1 deletion docs/DETAILS.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,4 +245,29 @@ const mockType = createMock<new () => Test>();
const mock = new mockType();
mock = // { a: "" }
```
```
## TypeQuery
```ts
enum AnEnum {
a,
b = 'something',
}
const mock = createMock<typeof AnEnum>();
mock.a // 0
mock.b // 'something'
mock[0] // 'a'
class AClass {
a: string
}
const mockClass = createMock<typeof AClass>();
new mockClass().a // ''
function AFunction(): number;
const mockFunction = createMock<typeof AFunction>();
mockFunction() // 0
```
19 changes: 0 additions & 19 deletions docs/NOT_SUPPORTED.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,3 @@ const mock = createMock<Test>();

mock.conditional // should be string. It will be null
```

## TypeQuery
[bug](https://github.com/uittorio/ts-auto-mock/issues/91)

This scenario needs to be investigated
```ts
enum AnEnum {
a,
b,
}
interface Test {
type: typeof AnEnum
}

const mock = createMock<Test>();
mock.type // will be null
```


5 changes: 5 additions & 0 deletions src/transformer/descriptor/descriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { GetStringDescriptor } from './string/string';
import { GetTypeAliasDescriptor } from './typeAlias/typeAlias';
import { GetTypeLiteralDescriptor } from './typeLiteral/typeLiteral';
import { GetTypeParameterDescriptor } from './typeParameter/typeParameter';
import { GetTypeQueryDescriptor } from './typeQuery/typeQuery';
import { GetTypeReferenceDescriptor } from './typeReference/typeReference';
import { GetUndefinedDescriptor } from './undefined/undefined';
import { GetUnionDescriptor } from './union/union';
Expand Down Expand Up @@ -67,6 +68,8 @@ export function GetDescriptor(node: ts.Node, scope: Scope): ts.Expression {
return GetImportDescriptor(node as ts.ImportClause, scope);
case ts.SyntaxKind.MethodSignature:
return GetMethodSignatureDescriptor(node as ts.MethodSignature, scope);
case ts.SyntaxKind.FunctionDeclaration:
return GetMethodDeclarationDescriptor(node as ts.FunctionDeclaration, scope);
case ts.SyntaxKind.MethodDeclaration:
return GetMethodDeclarationDescriptor(node as ts.MethodDeclaration, scope);
case ts.SyntaxKind.FunctionType:
Expand All @@ -80,6 +83,8 @@ export function GetDescriptor(node: ts.Node, scope: Scope): ts.Expression {
return GetFunctionAssignmentDescriptor(node as ts.ArrowFunction, scope);
case ts.SyntaxKind.ConstructorType:
return GetConstructorTypeDescriptor(node as ts.ConstructorTypeNode, scope);
case ts.SyntaxKind.TypeQuery:
return GetTypeQueryDescriptor(node as ts.TypeQueryNode, scope);
case ts.SyntaxKind.UnionType:
return GetUnionDescriptor(node as ts.UnionTypeNode, scope);
case ts.SyntaxKind.IntersectionType:
Expand Down
21 changes: 8 additions & 13 deletions src/transformer/descriptor/helper/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export namespace TypescriptHelper {
export function GetDeclarationFromNode(node: ts.Node): ts.Declaration {
const typeChecker: ts.TypeChecker = TypeChecker();
const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node);
return GetDeclarationFromSymbol(symbol);
}

export function GetDeclarationFromSymbol(symbol: ts.Symbol): ts.Declaration {
const declaration: ts.Declaration = GetFirstValidDeclaration(symbol.declarations);

if (ts.isImportSpecifier(declaration)) {
Expand All @@ -31,8 +35,9 @@ export namespace TypescriptHelper {
export function GetDeclarationForImport(node: ts.ImportClause | ts.ImportSpecifier): ts.TypeNode | ts.Declaration {
const typeChecker: ts.TypeChecker = TypeChecker();
const symbol: ts.Symbol = typeChecker.getSymbolAtLocation(node.name);
const declaredType: ts.Type = typeChecker.getDeclaredTypeOfSymbol(symbol);
return GetDeclarationFromType(declaredType);
const originalSymbol: ts.Symbol = typeChecker.getAliasedSymbol(symbol);

return GetFirstValidDeclaration(originalSymbol.declarations);
}

export function GetParameterOfNode(node: ts.EntityName): ts.NodeArray<ts.TypeParameterDeclaration> {
Expand All @@ -41,16 +46,6 @@ export namespace TypescriptHelper {
return (declaration as Declaration).typeParameters;
}

export function GetDeclarationFromType(type: ts.Type): ts.TypeNode | ts.Declaration {
if (type.symbol && type.symbol.declarations) {
return GetFirstValidDeclaration(type.symbol.declarations);
} else if (type.aliasSymbol && type.aliasSymbol.declarations) {
return GetFirstValidDeclaration(type.aliasSymbol.declarations);
}

return TypeChecker().typeToTypeNode(type);
}

export function GetTypeParameterOwnerMock(declaration: ts.Declaration): ts.Declaration {
const typeDeclaration: ts.Declaration = ts.getTypeParameterOwner(declaration);

Expand All @@ -69,6 +64,6 @@ export namespace TypescriptHelper {
function GetFirstValidDeclaration(declarations: ts.Declaration[]): ts.Declaration {
return declarations.find((declaration: ts.Declaration) => {
return !ts.isVariableDeclaration(declaration);
});
}) || declarations[0];
}
}
2 changes: 1 addition & 1 deletion src/transformer/descriptor/method/bodyReturnType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Scope } from '../../scope/scope';
import { GetDescriptor } from '../descriptor';
import { GetNullDescriptor } from '../null/null';

export function GetReturnTypeFromBody(node: ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration, scope: Scope): ts.Expression {
export function GetReturnTypeFromBody(node: ts.ArrowFunction | ts.FunctionExpression | ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression {
let returnValue: ts.Expression;

const functionBody: ts.FunctionBody = node.body as ts.FunctionBody;
Expand Down
2 changes: 1 addition & 1 deletion src/transformer/descriptor/method/methodDeclaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { GetDescriptor } from '../descriptor';
import { GetReturnTypeFromBody } from './bodyReturnType';
import { GetMethodDescriptor } from './method';

export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration, scope: Scope): ts.Expression {
export function GetMethodDeclarationDescriptor(node: ts.MethodDeclaration | ts.FunctionDeclaration, scope: Scope): ts.Expression {
let returnType: ts.Expression;

if (node.type) {
Expand Down
9 changes: 6 additions & 3 deletions src/transformer/descriptor/mock/mockProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ export function GetMockPropertiesFromSymbol(propertiesSymbol: ts.Symbol[], signa
const properties: ts.Declaration[] = propertiesSymbol.map((prop: ts.Symbol) => {
return prop.declarations[0];
});
const signaturesDeclarations: ts.Declaration[] = signatures.map((signature: ts.Signature) => {
return signature.declaration;
});

return GetMockPropertiesFromDeclarations(properties, signatures, scope);
return GetMockPropertiesFromDeclarations(properties, signaturesDeclarations, scope);
}

export function GetMockPropertiesFromDeclarations(list: ts.Declaration[], signatures: ReadonlyArray<ts.Signature>, scope: Scope): ts.CallExpression {
export function GetMockPropertiesFromDeclarations(list: ReadonlyArray<ts.Declaration>, signatures: ReadonlyArray<ts.Declaration>, scope: Scope): ts.CallExpression {
const propertiesFilter: ts.Declaration[] = list.filter((member: ts.PropertyDeclaration) => {
const hasModifiers: boolean = !!member.modifiers;

Expand Down Expand Up @@ -42,6 +45,6 @@ export function GetMockPropertiesFromDeclarations(list: ts.Declaration[], signat
},
);

const signaturesDescriptor: ts.Expression = signatures.length > 0 ? GetDescriptor(signatures[0].declaration, scope) : null;
const signaturesDescriptor: ts.Expression = signatures.length > 0 ? GetDescriptor(signatures[0], scope) : null;
return GetMockCall(variableDeclarations, accessorDeclaration, signaturesDescriptor);
}
32 changes: 27 additions & 5 deletions src/transformer/descriptor/properties/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,38 @@ import { SignatureKind } from 'typescript';
import * as ts from 'typescript';
import { Scope } from '../../scope/scope';
import { TypeChecker } from '../../typeChecker/typeChecker';
import { GetMockPropertiesFromSymbol } from '../mock/mockProperties';
import { GetMockPropertiesFromDeclarations, GetMockPropertiesFromSymbol } from '../mock/mockProperties';

export function GetProperties(node: ts.Node, scope: Scope): ts.Expression {
const typeChecker: ts.TypeChecker = TypeChecker();
const type: ts.Type = typeChecker.getTypeAtLocation(node);
const symbols: ts.Symbol[] = typeChecker.getPropertiesOfType(type);

const signatures: Array<ts.Signature> = [];
Array.prototype.push.apply(signatures, typeChecker.getSignaturesOfType(type, SignatureKind.Call));
Array.prototype.push.apply(signatures, typeChecker.getSignaturesOfType(type, SignatureKind.Construct));
if (!symbols.length) {
return GetPropertiesFromMembers(node as ts.TypeLiteralNode, scope);
} else {
const signatures: Array<ts.Signature> = [];

return GetMockPropertiesFromSymbol(symbols, signatures, scope);
Array.prototype.push.apply(signatures, typeChecker.getSignaturesOfType(type, SignatureKind.Call));
Array.prototype.push.apply(signatures, typeChecker.getSignaturesOfType(type, SignatureKind.Construct));

return GetMockPropertiesFromSymbol(symbols, signatures, scope);
}
}

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> = [];

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

return GetMockPropertiesFromDeclarations(properties, signatures, scope);
}
22 changes: 22 additions & 0 deletions src/transformer/descriptor/typeQuery/enumTypeQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as ts from 'typescript';
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,
undefined,
[],
ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword),
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.createBlock(
[
enumDeclaration,
ts.createReturn(enumDeclaration.name),
],
true,
),
);
}
51 changes: 51 additions & 0 deletions src/transformer/descriptor/typeQuery/typeQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as ts from 'typescript';
import { TypescriptCreator } from '../../helper/creator';
import { TransformerLogger } from '../../logger/transformerLogger';
import { GetMockFactoryCallTypeofEnum } from '../../mockFactoryCall/mockFactoryCall';
import { Scope } from '../../scope/scope';
import { TypeChecker } from '../../typeChecker/typeChecker';
import { GetDescriptor } from '../descriptor';
import { TypescriptHelper } from '../helper/helper';
import { GetMethodDeclarationDescriptor } from '../method/methodDeclaration';
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<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();
}
}
60 changes: 49 additions & 11 deletions src/transformer/mockDefiner/mockDefiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as ts from 'typescript';
import { GetTsAutoMockCacheOptions, TsAutoMockCacheOptions } from '../../options/cache';
import { GetDescriptor } from '../descriptor/descriptor';
import { GetProperties } from '../descriptor/properties/properties';
import { GetTypeofEnumDescriptor } from '../descriptor/typeQuery/enumTypeQuery';
import { TypescriptCreator } from '../helper/creator';
import { createImportOnIdentifier } from '../helper/import';
import { MockGenericParameter } from '../mockGeneric/mockGenericParameter';
Expand Down Expand Up @@ -32,6 +33,7 @@ interface FactoryIntersectionRegistrationPerFile {

export class MockDefiner {
private _neededImportIdentifierPerFile: { [key: string]: Array<ModuleNameIdentifier> } = {};
private _internalModuleImportIdentifierPerFile: { [key: string]: { [key in ModuleName]: ts.Identifier } } = {};
private _factoryRegistrationsPerFile: FactoryRegistrationPerFile = {};
private _factoryIntersectionsRegistrationsPerFile: FactoryIntersectionRegistrationPerFile = {};
private _factoryCache: DeclarationCache;
Expand Down Expand Up @@ -62,15 +64,24 @@ export class MockDefiner {
}

public setTsAutoMockImportIdentifier(): void {
if (!this._neededImportIdentifierPerFile[this._fileName]) {
this._neededImportIdentifierPerFile[this._fileName] = Object.keys(ModulesImportUrl).map((key: ModuleName) => {
return {
name: key,
moduleUrl: ModulesImportUrl[key],
identifier: this._createUniqueFileName(key),
};
});
if (this._internalModuleImportIdentifierPerFile[this._fileName]) {
return;
}

this._internalModuleImportIdentifierPerFile[this._fileName] = {
[ModuleName.Extension]: this._createUniqueFileName(ModuleName.Extension),
[ModuleName.Merge]: this._createUniqueFileName(ModuleName.Merge),
[ModuleName.Repository]: this._createUniqueFileName(ModuleName.Repository),
};

this._neededImportIdentifierPerFile[this._fileName] = this._neededImportIdentifierPerFile[this._fileName] || [];

Array.prototype.push.apply(this._neededImportIdentifierPerFile[this._fileName], Object.keys(ModulesImportUrl).map((key: ModuleName) => {
return {
moduleUrl: ModulesImportUrl[key],
identifier: this._internalModuleImportIdentifierPerFile[this._fileName][key],
};
}));
}

public getCurrentModuleIdentifier(module: ModuleName): ts.Identifier {
Expand Down Expand Up @@ -101,6 +112,12 @@ export class MockDefiner {
return this.getMockFactoryByKey(key);
}

public getMockFactoryTypeofEnum(declaration: ts.EnumDeclaration): ts.Expression {
const key: string = this._getMockFactoryIdForTypeofEnum(declaration);

return this.getMockFactoryByKey(key);
}

public getMockFactoryIntersection(declarations: ts.Declaration[], type: ts.IntersectionTypeNode): ts.Expression {
const key: string = this._getMockFactoryIdForIntersections(declarations, type);

Expand Down Expand Up @@ -140,9 +157,7 @@ export class MockDefiner {
}

private _getModuleIdentifier(fileName: string, module: ModuleName): ts.Identifier {
return this._neededImportIdentifierPerFile[fileName].find((moduleNameIdentifier: ModuleNameIdentifier) => {
return moduleNameIdentifier.name === module;
}).identifier;
return this._internalModuleImportIdentifierPerFile[fileName][module];
}

private _getMockFactoryId(declaration: ts.Declaration): string {
Expand Down Expand Up @@ -172,6 +187,29 @@ export class MockDefiner {
return key;
}

private _getMockFactoryIdForTypeofEnum(declaration: ts.EnumDeclaration): string {
const thisFileName: string = this._fileName;

if (this._factoryCache.has(declaration)) {
return this._factoryCache.get(declaration);
}

const key: string = this.getDeclarationKeyMap(declaration);

this._factoryCache.set(declaration, key);

this._factoryRegistrationsPerFile[thisFileName] = this._factoryRegistrationsPerFile[thisFileName] || [];

const factory: ts.Expression = GetTypeofEnumDescriptor(declaration, new Scope(key));

this._factoryRegistrationsPerFile[thisFileName].push({
key: declaration,
factory,
});

return key;
}

private _getMockFactoryIdForIntersections(declarations: ts.Declaration[], intersectionTypeNode: ts.IntersectionTypeNode): string {
const thisFileName: string = this._fileName;

Expand Down
Loading

0 comments on commit 84d1663

Please sign in to comment.