diff --git a/src/transformer/descriptor/descriptor.ts b/src/transformer/descriptor/descriptor.ts index ff8a85b22..e4df6f142 100644 --- a/src/transformer/descriptor/descriptor.ts +++ b/src/transformer/descriptor/descriptor.ts @@ -42,9 +42,25 @@ import { GetUndefinedDescriptor } from './undefined/undefined'; import { GetUnionDescriptor } from './union/union'; import { GetTypeOperatorDescriptor } from './typeOperator/typeOperator'; import { GetTupleDescriptor } from './tuple/tuple'; +import { GetShorthandPropertyAssignmentDescriptor } from './shorthandPropertyAssignment/shorthandPropertyAssignment'; +import { GetParameterDescriptor } from './parameter/parameter'; +import { GetVariableDeclarationDescriptor } from './variable/variable'; export function GetDescriptor(node: ts.Node, scope: Scope): ts.Expression { switch (node.kind) { + case core.ts.SyntaxKind.ShorthandPropertyAssignment: + return GetShorthandPropertyAssignmentDescriptor( + node as ts.ShorthandPropertyAssignment, + scope + ); + + case core.ts.SyntaxKind.VariableDeclaration: + return GetVariableDeclarationDescriptor( + node as ts.VariableDeclaration, + scope + ); + case core.ts.SyntaxKind?.Parameter: + return GetParameterDescriptor(node as ts.ParameterDeclaration, scope); case core.ts.SyntaxKind.TypeAliasDeclaration: return GetTypeAliasDescriptor(node as ts.TypeAliasDeclaration, scope); case core.ts.SyntaxKind.TypeReference: diff --git a/src/transformer/descriptor/parameter/parameter.ts b/src/transformer/descriptor/parameter/parameter.ts new file mode 100644 index 000000000..e3420920b --- /dev/null +++ b/src/transformer/descriptor/parameter/parameter.ts @@ -0,0 +1,19 @@ +import * as ts from 'typescript'; +import { Scope } from '../../scope/scope'; +import { GetNullDescriptor } from '../null/null'; +import { GetDescriptor } from '../descriptor'; + +export const GetParameterDescriptor: ( + node: ts.ParameterDeclaration, + scope: Scope +) => ts.Expression = (node: ts.ParameterDeclaration, scope: Scope) => { + if (node.type) { + return GetDescriptor(node.type, scope); + } + + if (node.initializer) { + return GetDescriptor(node.initializer, scope); + } + + return GetNullDescriptor(); +}; diff --git a/src/transformer/descriptor/shorthandPropertyAssignment/shorthandPropertyAssignment.ts b/src/transformer/descriptor/shorthandPropertyAssignment/shorthandPropertyAssignment.ts new file mode 100644 index 000000000..cff4bd48d --- /dev/null +++ b/src/transformer/descriptor/shorthandPropertyAssignment/shorthandPropertyAssignment.ts @@ -0,0 +1,27 @@ +import * as ts from 'typescript'; +import { Scope } from '../../scope/scope'; +import { core } from '../../core/core'; +import { TypescriptHelper } from '../helper/helper'; +import { GetDescriptor } from '../descriptor'; + +export const GetShorthandPropertyAssignmentDescriptor: ( + node: ts.ShorthandPropertyAssignment, + scope: Scope +) => ts.Expression = (node: ts.ShorthandPropertyAssignment, scope) => { + const typeChecker: ts.TypeChecker = core.typeChecker; + + const symbol: ts.Symbol | undefined = + typeChecker.getShorthandAssignmentValueSymbol(node); + + if (!symbol) { + throw new Error( + `The type checker failed to look up a symbol for \`${node.getText()}'. + Perhaps, the checker was searching an outdated source.` + ); + } + + const declaration: ts.Declaration = + TypescriptHelper.GetDeclarationFromSymbol(symbol); + + return GetDescriptor(declaration, scope); +}; diff --git a/src/transformer/descriptor/variable/variable.ts b/src/transformer/descriptor/variable/variable.ts new file mode 100644 index 000000000..85b9a9340 --- /dev/null +++ b/src/transformer/descriptor/variable/variable.ts @@ -0,0 +1,52 @@ +import * as ts from 'typescript'; +import { Scope } from '../../scope/scope'; +import { core } from '../../core/core'; +import { GetMockPropertiesFromSymbol } from '../mock/mockProperties'; +import { GetNullDescriptor } from '../null/null'; +import { GetDescriptor } from '../descriptor'; + +export const GetVariableDeclarationDescriptor: ( + node: ts.VariableDeclaration, + scope: Scope +) => ts.Expression = (node: ts.VariableDeclaration, scope: Scope) => { + const typeChecker: ts.TypeChecker = core.typeChecker; + const coreTs: typeof core.ts = core.ts; + if (node.type) { + return GetDescriptor(node.type, scope); + } + + const symbol: ts.Symbol | undefined = typeChecker.getSymbolAtLocation( + node.name + ); + + if (!symbol) { + throw new Error( + `The type checker failed to look up a symbol for \`${node.getText()}'. + Perhaps, the checker was searching an outdated source.` + ); + } + + const type: ts.Type = typeChecker.getTypeOfSymbolAtLocation(symbol, node); + const typeToNode: ts.TypeNode | undefined = typeChecker.typeToTypeNode( + type, + undefined, + undefined + ); + + if (!typeToNode) { + throw new Error( + `The type checker failed to look up a node for \`${node.getText()}'. + Perhaps, the checker was searching an outdated source.` + ); + } + + if (coreTs.isTypeLiteralNode(typeToNode)) { + const properties: ts.Symbol[] = typeChecker.getPropertiesOfType(type); + return GetMockPropertiesFromSymbol(properties, [], scope); + } + + if (coreTs.isLiteralTypeNode(typeToNode)) { + return GetDescriptor(typeToNode.literal, scope); + } + return GetNullDescriptor(); +}; diff --git a/test/transformer/descriptor/functions/functionsWithoutTypes.test.ts b/test/transformer/descriptor/functions/functionsWithoutTypes.test.ts new file mode 100644 index 000000000..2c99906f2 --- /dev/null +++ b/test/transformer/descriptor/functions/functionsWithoutTypes.test.ts @@ -0,0 +1,192 @@ +import { createMock } from 'ts-auto-mock'; +import { anImportedObject } from '../utils/object/object'; + +describe('functions without types defined', () => { + it('should infer basic boolean object literal types', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + // eslint-disable-next-line @typescript-eslint/typedef + const primitiveValue = false; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function whateverFunction() { + return true; + } + + return { primitiveValue, whateverFunction }; + } + + const type: typeof functionToMock = createMock(); + expect(type().primitiveValue).toBe(false); + expect(type().whateverFunction()).toBe(true); + }); + + it('should infer basic string object literal types', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + // eslint-disable-next-line @typescript-eslint/typedef + const primitiveValue = 'Hello world'; + + return { primitiveValue }; + } + + const type: typeof functionToMock = createMock(); + expect(type().primitiveValue).toBe('Hello world'); + }); + + it('should infer basic object literal types', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + // eslint-disable-next-line @typescript-eslint/typedef + const primitiveValue = { + test: 'hello', + }; + + return { primitiveValue }; + } + + const type: typeof functionToMock = createMock(); + expect(type().primitiveValue).toEqual({ + test: 'hello', + }); + }); + + it('should use the default behaviour for variables with a type defined', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + const primitiveValue: boolean = true; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function whateverFunction() { + return true; + } + + return { primitiveValue, whateverFunction }; + } + + const type: typeof functionToMock = createMock(); + expect(type().primitiveValue).toBe(false); + }); + + it('should use the default behaviour for internal function declarations with a type defined', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + const primitiveValue: boolean = true; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function whateverFunction(): boolean { + return true; + } + + return { primitiveValue, whateverFunction }; + } + + const type: typeof functionToMock = createMock(); + expect(type().whateverFunction()).toBe(false); + }); + + it('should use the default behaviour for internal function declarations with a type defined', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + const primitiveValue: boolean = true; + + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function whateverFunction(): boolean { + return true; + } + + return { primitiveValue, whateverFunction }; + } + + const type: typeof functionToMock = createMock(); + expect(type().whateverFunction()).toBe(false); + }); + + it('should infer object literal return types', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + return { a: 'hello world', b: 123 }; + } + + const type: typeof functionToMock = createMock(); + expect(type()).toEqual({ + a: 'hello world', + b: 123, + }); + }); + + it('should infer variables outside the function', () => { + // eslint-disable-next-line @typescript-eslint/typedef + const anObject = { a: 'hello world', b: 123 }; + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + return anObject; + } + + const type: typeof functionToMock = createMock(); + expect(type()).toEqual({ + a: 'hello world', + b: 123, + }); + }); + + it('should infer variables from a different file', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + return anImportedObject; + } + + const type: typeof functionToMock = createMock(); + expect(type()).toEqual({ + a: 'hello world', + b: 123, + }); + }); + + it('should infer a spread object', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock() { + return { + ...anImportedObject, + }; + } + + const type: typeof functionToMock = createMock(); + expect(type()).toEqual({ + a: 'hello world', + b: 123, + }); + }); + + it('should set null when a parameter has not type', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock(param) { + return { + param, + }; + } + + const type: typeof functionToMock = createMock(); + expect( + type({ + test: 'hello', + }) + ).toEqual({ + param: null, + }); + }); + + it('should be able to infer parameter types', () => { + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + function functionToMock(param: string) { + return { + param, + }; + } + + const type: typeof functionToMock = createMock(); + expect(type('hello')).toEqual({ + param: '', + }); + }); +}); diff --git a/test/transformer/descriptor/utils/object/object.ts b/test/transformer/descriptor/utils/object/object.ts new file mode 100644 index 000000000..c1ec8b081 --- /dev/null +++ b/test/transformer/descriptor/utils/object/object.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/typedef +export const anImportedObject = { a: 'hello world', b: 123 };