From 80c6b9d53f912b95179a3b08046d59be5359958f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roman=20J=C3=A1mbor?= Date: Mon, 14 Feb 2022 12:33:04 +0100 Subject: [PATCH] Added New tests (#28) - coverage ~88 %. Changed - Changed way of handling types of runtime value. Now there is SomeClass.prototype[REFLECTED_TYPE_ID] = ID of SomeClass's type; generated for each class in the project (not just those decorated via @reflect). It does not mean that metadata of those classes will be generated. Logic of generating type's metadata is still the same, use getType() somewhere, apply @reflect decorator or apply any other decorator tagged by @reflect JSDoc tag. This is preparation for include/exclude configuration (issue #29). - custom decorators tagged by @reflect JSDoc tag does not have to have generic parameter --- CHANGELOG.md | 18 ++++++ coverage/badge.svg | 2 +- runtime/src/consts.ts | 2 +- runtime/src/reflect.ts | 15 +---- tests/src/07-gettype-of-runtime-value.ts | 8 ++- tests/src/08-native-types.ts | 33 +++++++++- tests/src/09-reflect-decorator.ts | 30 +++++++-- tests/src/16-base-type.ts | 16 +++++ tests/src/17-assignable.ts | 82 ++++++++++++++++++++++++ transformer/src/getTypeCall.ts | 2 +- transformer/src/processDecorator.ts | 27 +++++--- transformer/src/visitors/mainVisitor.ts | 32 ++++++++- 12 files changed, 232 insertions(+), 35 deletions(-) create mode 100644 tests/src/16-base-type.ts create mode 100644 tests/src/17-assignable.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a21578f..4980a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [//]: # (### Added) [//]: # (### Changed) +## [0.7.2] - 2022-02-14 +### Added +- New tests (#28) - coverage ~88 %. + +### Changed +- Changed way of handling types of runtime value. +\ +Now there is `SomeClass.prototype[REFLECTED_TYPE_ID] = ID of SomeClass's type;` generated for each class in the project (not just those decorated via `@reflect`). +It does not mean that metadata of those classes will be generated. +Logic of generating type's metadata is still the same, +use `getType()` somewhere, apply `@reflect` decorator +or apply any other decorator tagged by `@reflect` JSDoc tag. +\ +This is preparation for include/exclude configuration (issue #29). +- custom decorators tagged by `@reflect` JSDoc tag does not have to have generic parameter + ## [0.7.1] - 2022-02-14 ### Added - `Type.isAny()` @@ -16,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `Type.isAssignableTo()` - added support of Arrays; fixed issue with optional members + ## [0.7.0] - 2022-02-14 ### Added - `Type.toString()` @@ -33,6 +50,7 @@ Native types (String, Number, Object, etc..) are always generated as eg.: `Promi - \[BREAKING] `Type.intersection` changed to `Type.isIntersection()` - Fixed of issue #23 - access to ctor of not exported class + ## [0.7.0-alpha.0] - 2022-02-13 ### Added - `getType(val: any)` it is possible to get type of runtime value, diff --git a/coverage/badge.svg b/coverage/badge.svg index 74e32b6..3a32f35 100644 --- a/coverage/badge.svg +++ b/coverage/badge.svg @@ -1 +1 @@ -Coverage: 79.9%Coverage79.9% \ No newline at end of file +Coverage: 87.98%Coverage87.98% \ No newline at end of file diff --git a/runtime/src/consts.ts b/runtime/src/consts.ts index 0a248aa..5f7a6ce 100644 --- a/runtime/src/consts.ts +++ b/runtime/src/consts.ts @@ -21,4 +21,4 @@ export const REFLECT_STORE_SYMBOL = Symbol("tst_reflect_store"); /** * Name of the property used to store Type instance on constructors */ -export const REFLECTED_TYPE = "__reflectedType__"; +export const REFLECTED_TYPE_ID = "__reflectedTypeId__"; diff --git a/runtime/src/reflect.ts b/runtime/src/reflect.ts index 6a1805c..8decece 100644 --- a/runtime/src/reflect.ts +++ b/runtime/src/reflect.ts @@ -1,6 +1,6 @@ import { ObjectLiteralTypeBuilder } from "./type-builder/ObjectLiteralTypeBuilder"; import { TypeBuilder } from "./type-builder/TypeBuilder"; -import { REFLECTED_TYPE } from "./consts"; +import { REFLECTED_TYPE_ID } from "./consts"; import { TypeKind } from "./enums"; import { Type, @@ -13,7 +13,7 @@ const ArrayItemsCountToCheckItsType = 10; * @param value * @internal */ -export function getTypeOfRuntimeValue(value: any) +export function getTypeOfRuntimeValue(value: any): Type { if (value === undefined) return Type.Undefined; if (value === null) return Type.Null; @@ -59,7 +59,7 @@ export function getTypeOfRuntimeValue(value: any) return arrayBuilder.setGenericType(unionBuilder.build()).build(); } - return value.constructor[REFLECTED_TYPE] || Type.Unknown; + return Type.store.get(value.constructor.prototype[REFLECTED_TYPE_ID]) || Type.Unknown; } /** @@ -101,10 +101,7 @@ getType.__tst_reflect__ = true; */ export function reflect() { - const typeOfTType = arguments[0]?.TType; - return function (Constructor: { new(...args: any[]): T }) { - (Constructor as any)[REFLECTED_TYPE] = typeOfTType; return Constructor; }; } @@ -145,12 +142,6 @@ const nativeTypes = { "BigInt": createNativeType("BigInt"), }; -(Object as any)[REFLECTED_TYPE] = nativeTypes.Object; -(String as any)[REFLECTED_TYPE] = nativeTypes.String; -(Number as any)[REFLECTED_TYPE] = nativeTypes.Number; -(Boolean as any)[REFLECTED_TYPE] = nativeTypes.Boolean; -(Date as any)[REFLECTED_TYPE] = nativeTypes.Date; - /** * @internal */ diff --git a/tests/src/07-gettype-of-runtime-value.ts b/tests/src/07-gettype-of-runtime-value.ts index cb16db9..49081d7 100644 --- a/tests/src/07-gettype-of-runtime-value.ts +++ b/tests/src/07-gettype-of-runtime-value.ts @@ -5,14 +5,18 @@ import { } from "tst-reflect"; test("getType(val) called with runtime value and it is not Type.Unknown", () => { - @reflect() + // @reflect() decorator required in some configurations class A { + constructor(public foo: string) + { + } } - const a: unknown = new A(); + const a: unknown = new A("Lipsum"); expect(getType(a) instanceof Type).toBe(true); expect(getType(a)).not.toBe(Type.Unknown); expect(getType(a).name).toBe("A"); + expect(getType(a)).toBe(getType()); }); \ No newline at end of file diff --git a/tests/src/08-native-types.ts b/tests/src/08-native-types.ts index db3e687..6000570 100644 --- a/tests/src/08-native-types.ts +++ b/tests/src/08-native-types.ts @@ -1,4 +1,7 @@ -import { Type } from "tst-reflect"; +import { + getType, + Type +} from "tst-reflect"; test("All native types are correct", () => { expect(Type.Unknown.name).toBe("unknown"); @@ -10,4 +13,32 @@ test("All native types are correct", () => { expect(Type.String.name).toBe("String"); expect(Type.Boolean.name).toBe("Boolean"); expect(Type.Date.name).toBe("Date"); +}); + +test("Type of number is Type.Number", () => { + expect(getType(5)).toBe(Type.Number); +}); + +test("Type of string is Type.String", () => { + expect(getType("foo")).toBe(Type.String); +}); + +test("Type of boolean is Type.Boolean", () => { + expect(getType(true)).toBe(Type.Boolean); +}); + +test("Type of undefined is Type.Undefined", () => { + expect(getType(undefined)).toBe(Type.Undefined); +}); + +test("Type of null is Type.Null", () => { + expect(getType(null)).toBe(Type.Null); +}); + +test("Type of Date is Type.Date", () => { + expect(getType(new Date())).toBe(Type.Date); +}); + +test("Base type of object literal is Type.Object", () => { + expect(getType({}).baseType).toBe(Type.Object); }); \ No newline at end of file diff --git a/tests/src/09-reflect-decorator.ts b/tests/src/09-reflect-decorator.ts index 16f4564..7c119e2 100644 --- a/tests/src/09-reflect-decorator.ts +++ b/tests/src/09-reflect-decorator.ts @@ -3,12 +3,30 @@ import { Type } from "tst-reflect"; -test("@reflect decorator force type to appear in metadata", () => { - @reflect() - class A - { - } +@reflect() +class A +{ +} + +/** + * @reflect + */ +function foo() +{ + return function(_: any) {} +} + +@foo() +class B +{ +} - expect(Type.getTypes()).toHaveLength(1); +test("@reflect decorator force type to appear in metadata", () => { + expect(Type.getTypes()).toHaveLength(2); expect(Type.getTypes()[0].name).toBe("A"); +}); + +test("Custom tagged decorator force type to appear in metadata", () => { + expect(Type.getTypes()).toHaveLength(2); + expect(Type.getTypes()[1].name).toBe("B"); }); \ No newline at end of file diff --git a/tests/src/16-base-type.ts b/tests/src/16-base-type.ts new file mode 100644 index 0000000..8049083 --- /dev/null +++ b/tests/src/16-base-type.ts @@ -0,0 +1,16 @@ +import { + getType, + Type +} from "tst-reflect"; + +test("getType(val) called with runtime value and it is not Type.Unknown", () => { + const type = getType({}); + + expect(type).toBeDefined(); + expect(type instanceof Type).toBe(true); + expect(type).not.toBe(Type.Unknown); + expect(type.isObjectLiteral()).toBeTruthy(); + + expect(type.baseType).toBe(Type.Object); + expect(type.baseType!.baseType).toBe(undefined); +}); \ No newline at end of file diff --git a/tests/src/17-assignable.ts b/tests/src/17-assignable.ts new file mode 100644 index 0000000..7bed187 --- /dev/null +++ b/tests/src/17-assignable.ts @@ -0,0 +1,82 @@ +import { getType } from "tst-reflect"; + +interface ISomeInterface +{ + stringProp: string; + numberProp: number; + booleanProp: boolean; + arrayProp: Array; + stringOrNumber: string | number; +} + +class SomeClass +{ + stringProp: string; + anyProp: any; + stringArrayProp: string[]; + + constructor(stringProp: string, stringArrayProp: string[]) + { + this.stringProp = stringProp; + this.stringArrayProp = stringArrayProp; + } + + optionalMethod?(this: ISomeInterface, size: number): void + { + } +} + +test("Type.isAssignableTo()", () => { + const someObject = { + anyProp: true, + stringProp: "", + stringArrayProp: ["foo"], + + optionalMethod() + { + } + }; + + const someObject2 = { + stringProp: "", + numberProp: 123, + booleanProp: true, + arrayProp: ["foo"], + stringOrNumber: 0 + }; + + const someObject3 = { + stringProp: "", + numberProp: 123 + }; + + const someObject4 = { + anyProp: new Date(), + stringProp: "", + numberProp: 123, + booleanProp: false, + stringArrayProp: ["foo"], + stringOrNumber: "lorem", + arrayProp: ["bar"] + }; + + const someInterfaceType = getType(); + const someClassType = getType(); + + const obj1Type = getType(someObject); + const obj2Type = getType(someObject2); + const obj3Type = getType(someObject3); + const obj4Type = getType(someObject4); + + expect(obj1Type.isAssignableTo(someInterfaceType)).toBeFalsy(); + expect(obj1Type.isAssignableTo(someClassType)).toBeTruthy(); + + expect(obj2Type.isAssignableTo(someInterfaceType)).toBeTruthy(); + expect(obj2Type.isAssignableTo(someClassType)).toBeFalsy(); + + expect(obj3Type.isAssignableTo(someInterfaceType)).toBeFalsy(); + expect(obj3Type.isAssignableTo(someClassType)).toBeFalsy(); + + expect(obj4Type.isAssignableTo(someInterfaceType)).toBeTruthy(); + expect(obj4Type.isAssignableTo(someClassType)).toBeTruthy(); +}); \ No newline at end of file diff --git a/transformer/src/getTypeCall.ts b/transformer/src/getTypeCall.ts index 1dc1460..2c9cd4e 100644 --- a/transformer/src/getTypeCall.ts +++ b/transformer/src/getTypeCall.ts @@ -30,7 +30,7 @@ const creatingTypes: Array = []; * @param context * @param typeCtor */ -export function getTypeCall(type: ts.Type, symbol: ts.Symbol | undefined, context: Context, typeCtor?: ts.EntityName | ts.DeclarationName): GetTypeCall // TODO: Remove symbol parameter +export function getTypeCall(type: ts.Type, symbol: ts.Symbol | undefined, context: Context, typeCtor?: ts.EntityName | ts.DeclarationName): GetTypeCall // TODO: Remove symbol parameter if possible { const id: number | undefined = (type.aliasSymbol || type.symbol as any)?.["id"]; let typePropertiesObjectLiteral: ts.ObjectLiteralExpression | undefined = undefined; diff --git a/transformer/src/processDecorator.ts b/transformer/src/processDecorator.ts index b0bb0f3..bcf99b6 100644 --- a/transformer/src/processDecorator.ts +++ b/transformer/src/processDecorator.ts @@ -12,7 +12,7 @@ import { updateCallExpression } from "./updateCallExpr export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, context: Context): ts.Decorator | undefined { // Method/function declaration - const declaration = decoratorType.symbol.declarations?.[0] as ts.FunctionLikeDeclarationBase; + const declaration = getDeclaration(decoratorType.symbol) as ts.FunctionLikeDeclarationBase; if (!declaration) { @@ -22,14 +22,6 @@ export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, con // Try to get State const state: FunctionLikeDeclarationGenericParametersDetail = getGenericParametersDetails(declaration, context, []); - if (!state || !state.usedGenericParameters || !state.indexesOfGenericParameters || !state.requestedGenericsReflection) - { - return undefined; - } - - // Decorator has no generic parameters in nature; we just abusing it so only one generic parameter makes sense - const genericParamName = state.usedGenericParameters[0]; - // Type of Class let genericTypeNode: ts.NamedDeclaration, genericType: ts.Type; @@ -46,6 +38,23 @@ export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, con const genericTypeSymbol = genericType.getSymbol(); + if (!state || !state.usedGenericParameters || !state.indexesOfGenericParameters || !state.requestedGenericsReflection) + { + // Decorator does not accept generic type argument but processDecorator was + // forced by @reflect, so we'll generate metadata and keep decorator as is. + getTypeCall( + genericType, + genericTypeSymbol, + context, + genericTypeNode.name + ); + + return undefined; + } + + // Decorator has no generic parameters in nature; we just abusing it so only one generic parameter makes sense + const genericParamName = state.usedGenericParameters[0]; + let callExpression: ts.CallExpression; const typeArgumentDescription = { genericTypeName: genericParamName, diff --git a/transformer/src/visitors/mainVisitor.ts b/transformer/src/visitors/mainVisitor.ts index cab93e7..81452c7 100644 --- a/transformer/src/visitors/mainVisitor.ts +++ b/transformer/src/visitors/mainVisitor.ts @@ -1,5 +1,6 @@ import { GET_TYPE_FNC_NAME, + REFLECTED_TYPE_ID, TYPE_ID_PROPERTY_NAME } from "tst-reflect"; import * as ts from "typescript"; @@ -8,7 +9,7 @@ import { getType, hasReflectJsDoc, isNodeIgnored -} from "../helpers"; +} from "../helpers"; import { log } from "../log"; import { processDecorator } from "../processDecorator"; import { processGenericCallExpression } from "../processGenericCallExpression"; @@ -39,7 +40,7 @@ export function mainVisitor(nodeToVisit: ts.Node, context: Context): ts.VisitRes { return node; } - + // Is it call of some function named "getType"? if (ts.isIdentifier(node.expression) && node.expression.escapedText == GET_TYPE_FNC_NAME) { @@ -129,6 +130,33 @@ export function mainVisitor(nodeToVisit: ts.Node, context: Context): ts.VisitRes } } } + else if (ts.isClassDeclaration(node)) + { + const typeId = (context.typeChecker.getTypeAtLocation(node).symbol as any).id; + + if (typeId) + { + // Generate assignment of class's type ID to its prototype + return [ + ts.visitEachChild(node, context.visitor, context.transformationContext), + + // ClassIdentifier.prototype[REFLECTED_TYPE_ID] = typeId; + ts.factory.createExpressionStatement( + ts.factory.createBinaryExpression( + ts.factory.createElementAccessExpression( + ts.factory.createPropertyAccessExpression( + node.name as ts.Expression, + "prototype" + ), + ts.factory.createStringLiteral(REFLECTED_TYPE_ID) + ), + ts.factory.createToken(ts.SyntaxKind.EqualsToken), + ts.factory.createNumericLiteral(typeId) + ) + ) + ]; + } + } return ts.visitEachChild(node, context.visitor, context.transformationContext); }