diff --git a/README.md b/README.md index 5a8d7b6..03b9996 100644 --- a/README.md +++ b/README.md @@ -839,8 +839,7 @@ export interface Property /** * Decoration description */ -export interface Decorator -{ +export class Decorator { /** * Decorator name */ @@ -849,6 +848,10 @@ export interface Decorator * Decorator full name */ fullName?: string; + /** + * List of literal arguments + */ + getArguments(): Array; } /** diff --git a/dev/quick-tests/SomeEnum.ts b/dev/quick-tests/SomeEnum.ts new file mode 100644 index 0000000..7080aa4 --- /dev/null +++ b/dev/quick-tests/SomeEnum.ts @@ -0,0 +1,5 @@ +export enum SomeEnum +{ + One, + Two +} \ No newline at end of file diff --git a/dev/quick-tests/SomeType.ts b/dev/quick-tests/SomeType.ts index e839004..92368ac 100644 --- a/dev/quick-tests/SomeType.ts +++ b/dev/quick-tests/SomeType.ts @@ -1,4 +1,7 @@ +export const SomeString = "SomeTypeString"; + export class SomeType { + foo() { } diff --git a/dev/quick-tests/index.ts b/dev/quick-tests/index.ts index 14360f5..f60eb44 100644 --- a/dev/quick-tests/index.ts +++ b/dev/quick-tests/index.ts @@ -1,23 +1,32 @@ -import { getType } from "tst-reflect"; +import { getType } from "tst-reflect"; +import { property } from "./property"; +import { SomeEnum } from "./SomeEnum"; +import { SomeString } from "./SomeType"; -enum SomeEnum -{ - One, - Two -} +const MyString = "SomeType"; -interface Foo +/** + * @reflectDecorator + */ +function klass(str: string, num: number, enu: SomeEnum) { - enum: SomeEnum; + console.log("klass", str, num, enu); + const t = getType(); + + console.log(t.getDecorators().map(d => "\tdecorator: " + d.name + " args:" + d.getArguments().join(", "))); + return function (Constructor: { new(...args: any[]): T }) { + }; } -const type = getType(); - -console.log(type); - -const enumProperty = type.getProperties().find(prop => prop.type.isEnum()); - -if (enumProperty) +@klass(MyString, 5, SomeEnum.One) +class A { - console.log(enumProperty.type.name, enumProperty.type.getEnum().getEnumerators()); + @property("Foo property", 5, SomeEnum.One, { foo: "f", bar: 5, baz: SomeEnum.Two }) + foo: string; + + @property(SomeString, 5, SomeEnum.Two, true) + get bar(): number + { + return 0; + } } \ No newline at end of file diff --git a/dev/quick-tests/property.ts b/dev/quick-tests/property.ts new file mode 100644 index 0000000..9fba448 --- /dev/null +++ b/dev/quick-tests/property.ts @@ -0,0 +1,7 @@ +import { SomeEnum } from "./SomeEnum"; + +export function property(str: string, num: number, enu: SomeEnum, any: any) +{ + console.log("property", str, num, enu, any); + return function (_, __) {}; +} \ No newline at end of file diff --git a/runtime/src/Type.ts b/runtime/src/Type.ts index 089151d..3550447 100644 --- a/runtime/src/Type.ts +++ b/runtime/src/Type.ts @@ -1,25 +1,24 @@ -import type { MetadataStore } from "./meta-stores/MetadataStore"; +import type { MetadataStore } from "./meta-stores"; import { Constructor, Method, -} from "./descriptions/method"; -import type { Decorator } from "./descriptions/decorator"; -import type { IndexedAccessType } from "./descriptions/indexed-access-type"; -import type { ConditionalType } from "./descriptions/conditional-type"; +} from "./descriptions/method"; +import type { Decorator } from "./descriptions/decorator"; +import type { IndexedAccessType } from "./descriptions/indexed-access-type"; +import type { ConditionalType } from "./descriptions/conditional-type"; import { ConstructorImport, ConstructorImportActivator -} from "./descriptions/constructor-import"; -import type { Property, } from "./descriptions/property"; -import type { MethodParameter, } from "./descriptions/parameter"; -import type { EnumInfo } from "./descriptions/enum-info"; -import type { TypeProperties } from "./descriptions/type-properties"; -import { TypeKind } from "./enums"; +} from "./descriptions/constructor-import"; +import type { Property, } from "./descriptions/property"; +import type { MethodParameter, } from "./descriptions/parameter"; +import type { EnumInfo } from "./descriptions/enum-info"; +import type { TypeProperties } from "./descriptions/type-properties"; +import { TypeKind } from "./enums"; import { Mapper, resolveLazyType -} from "./mapper"; -import { InlineMetadataStore } from "./meta-stores/InlineMetadataStore"; +} from "./mapper"; /** * Object representing TypeScript type in memory @@ -34,6 +33,8 @@ export class Type public static readonly Number: Type; public static readonly Boolean: Type; public static readonly Date: Type; + public static readonly Null: Type; + public static readonly Undefined: Type; /** @internal */ private _ctor?: () => Function; @@ -79,7 +80,25 @@ export class Type private _genericTypeDefault?: Type; /** @internal */ - private static _store: MetadataStore = InlineMetadataStore.initiate(); + private static _store: MetadataStore = { + store: {}, + get(id: number) + { + return undefined; + }, + set(id: number, description: any) + { + return Type.Unknown; + }, + getLazy(id: number) + { + return () => undefined; + }, + wrap(description: any) + { + return Type.Unknown; + } + }; /** * Internal Type constructor @@ -308,7 +327,7 @@ export class Type { return false; } - + return type != undefined && this._fullName == type._fullName && !!this._fullName; } @@ -507,7 +526,7 @@ export class Type } { // TODO: Important to handle Unions and Intersections - + const interfaceMembers = this.interface?.flattenInheritedMembers() ?? { properties: {}, methods: {} }; const baseTypeMembers = this.baseType?.flattenInheritedMembers() ?? { properties: {}, methods: {} }; diff --git a/runtime/src/consts.ts b/runtime/src/consts.ts index 842b079..5a87bbb 100644 --- a/runtime/src/consts.ts +++ b/runtime/src/consts.ts @@ -22,3 +22,8 @@ export const GET_TYPE_FNC_NAME = "getType"; * Symbol used as key in objects, holding metadata */ 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__"; diff --git a/runtime/src/descriptions/decorator.ts b/runtime/src/descriptions/decorator.ts index dd6c2ec..57c1197 100644 --- a/runtime/src/descriptions/decorator.ts +++ b/runtime/src/descriptions/decorator.ts @@ -12,12 +12,17 @@ export interface DecoratorDescription * Decorator full name */ fn: string; + + /** + * List of literal arguments + */ + args?: Array; } /** * Decoration description */ -export interface Decorator +export class Decorator { /** * Decorator name @@ -28,4 +33,33 @@ export interface Decorator * Decorator full name */ fullName?: string; + + /** + * @internal + */ + private args: Array; + + /** + * Internal constructor + * @internal + */ + protected constructor(description: DecoratorDescription) + { + this.name = description.n; + this.fullName = description.fn; + this.args = description.args || []; + } + + /** + * List of literal arguments + */ + getArguments(): Array + { + return this.args.slice(); + } } + +/** + * @internal + */ +export class DecoratorActivator extends Decorator {} diff --git a/runtime/src/mapper.ts b/runtime/src/mapper.ts index 6bdf390..9e58211 100644 --- a/runtime/src/mapper.ts +++ b/runtime/src/mapper.ts @@ -1,12 +1,13 @@ import { AccessModifier, Accessor -} from "./enums"; +} from "./enums"; import type { Type } from "./Type"; import { Decorator, + DecoratorActivator, DecoratorDescription -} from "./descriptions/decorator"; +} from "./descriptions/decorator"; import { Constructor, ConstructorActivator, @@ -14,15 +15,15 @@ import { Method, MethodActivator, MethodDescription -} from "./descriptions/method"; +} from "./descriptions/method"; import { MethodParameter, ParameterDescription -} from "./descriptions/parameter"; +} from "./descriptions/parameter"; import { Property, PropertyDescription -} from "./descriptions/property"; +} from "./descriptions/property"; /** * @internal @@ -44,7 +45,7 @@ export const Mapper = { */ mapDecorators(d: DecoratorDescription): Decorator { - return ({ name: d.n, fullName: d.fn }); + return Reflect.construct(Decorator, [d], DecoratorActivator); }, /** diff --git a/runtime/src/meta-stores/MetadataStoreBase.ts b/runtime/src/meta-stores/MetadataStoreBase.ts index afb5bd7..f695b50 100644 --- a/runtime/src/meta-stores/MetadataStoreBase.ts +++ b/runtime/src/meta-stores/MetadataStoreBase.ts @@ -43,7 +43,7 @@ export abstract class MetadataStoreBase implements MetadataStore } } - const type = Reflect.construct(Type, [], TypeActivator); + const type: Type = Reflect.construct(Type, [], TypeActivator); type.initialize(description); return type; } diff --git a/runtime/src/meta-stores/index.ts b/runtime/src/meta-stores/index.ts index 371a340..9da95fd 100644 --- a/runtime/src/meta-stores/index.ts +++ b/runtime/src/meta-stores/index.ts @@ -1,3 +1,4 @@ +import { InlineMetadataStore } from "./InlineMetadataStore"; import { MetadataStore } from "./MetadataStore"; import { NodeProcessMetadataStore } from "./NodeProcessMetadataStore"; import { WindowMetadataStore } from "./WindowMetadataStore"; @@ -7,6 +8,9 @@ export * from "./InlineMetadataStore"; export * from "./WindowMetadataStore"; export * from "./NodeProcessMetadataStore"; +// Default store +InlineMetadataStore.initiate(); + /** * Function used in transformer templates for metadata files. * @return {MetadataStore} diff --git a/runtime/src/reflect.ts b/runtime/src/reflect.ts index 98038af..58a9942 100644 --- a/runtime/src/reflect.ts +++ b/runtime/src/reflect.ts @@ -1,85 +1,158 @@ -import { TypeKind } from "./enums"; -import { - Type, - TypeActivator -} from "./Type"; - -/** - * Returns Type of generic parameter - */ -export function getType(): Type -{ - if (!(((typeof window === "object" && window) || (typeof global === "object" && global) || globalThis) as any)["tst-reflect-disable"]) - { - console.debug("[ERR] tst-reflect: You call getType() method directly. " + - "You have probably wrong configuration, because tst-reflect-transformer package should replace this call by the Type instance.\n" + - "If you have right configuration it may be BUG so try to create an issue.\n" + - "If it is not an issue and you don't want to see this debug message, " + - "create field 'tst-reflect-disable' in global object (window | global | globalThis) eg. `window['tst-reflect-disable'] = true;`"); - } - - // In case of direct call, we'll return Unknown type. - return Type.Unknown; -} - -/** @internal */ -getType.__tst_reflect__ = true; - -/** - * Class decorator which marks classes to be processed and included in metadata lib file. - * @reflectDecorator - */ -export function reflect() -{ - getType(); - return function (Constructor: { new(...args: any[]): T }) { - }; -} - - -function createNativeType(typeName: string, ctor?: Function): Type -{ - const type = Reflect.construct(Type, [], TypeActivator); - - type.initialize({ - n: typeName, - fn: typeName, - ctor: () => ctor, - k: TypeKind.Native - }); - - return type; -} - -/** - * List of native types - * @description It should save some memory and all native Types will be the same instances. - */ -const nativeTypes: { [typeName: string]: Type } = { - "Object": createNativeType("Object", Object), - "Unknown": createNativeType("unknown"), - "Any": createNativeType("any"), - "Void": createNativeType("void"), - "String": createNativeType("String", String), - "Number": createNativeType("Number", Number), - "Boolean": createNativeType("Boolean", Boolean), - "Date": createNativeType("Date", Date), -}; - -/** - * @internal - */ -export const NativeTypes = new Map(); - -for (let entry of Object.entries(nativeTypes)) -{ - NativeTypes.set(entry[0].toLowerCase(), entry[1]); -} - -for (const typeName in nativeTypes) -{ - if (nativeTypes.hasOwnProperty(typeName)) - { - (Type as any)[typeName] = nativeTypes[typeName]; - } +import { TypeBuilder } from "./type-builder/TypeBuilder"; +import { REFLECTED_TYPE } from "./consts"; +import { TypeKind } from "./enums"; +import { + Type, + TypeActivator +} from "./Type"; + +const ArrayItemsCountToCheckItsType = 10; + +/** + * @param args + */ +function getTypeFromRuntimeValue(args: any[]) +{ + const value = args[0]; + + if (value === undefined) return Type.Undefined; + if (value === null) return Type.Null; + + if (!value.constructor) + { + return Type.Unknown; + } + + if (value.constructor == Array) + { + const set = new Set(); + + // If it is an array, there can be anything; we'll check first X cuz of performance. + for (let item of value.slice(0, ArrayItemsCountToCheckItsType)) + { + set.add(item?.constructor?.[REFLECTED_TYPE] ?? Type.Undefined); + } + + const valuesTypes = Array.from(set); + const arrayBuilder = TypeBuilder.createArray(); + + if (value.length == 0) + { + return arrayBuilder + .setGenericType(Type.Any) + .build(); + } + + const unionBuilder = TypeBuilder.createUnion(valuesTypes); + + // If there are more items than we checked, add Unknown type to the union. + if (value.length > ArrayItemsCountToCheckItsType) + { + unionBuilder.addTypes(Type.Unknown); + } + + return arrayBuilder.setGenericType(unionBuilder.build()).build(); + } + + return value.constructor[REFLECTED_TYPE] || Type.Unknown; +} + +/** + * Returns Type of value in memory. + * @description Returns + * @param value + */ +export function getType(value: any): Type +/** + * Returns Type of generic parameter. + */ +export function getType(): Type +export function getType(...args: any[]): Type +{ + if (args.length) + { + return getTypeFromRuntimeValue(args); + } + + if (!(((typeof window === "object" && window) || (typeof global === "object" && global) || globalThis) as any)["tst-reflect-disable"]) + { + console.debug("[ERR] tst-reflect: You call getType() method directly. " + + "You have probably wrong configuration, because tst-reflect-transformer package should replace this call by the Type instance.\n" + + "If you have right configuration it may be BUG so try to create an issue.\n" + + "If it is not an issue and you don't want to see this debug message, " + + "create field 'tst-reflect-disable' in global object (window | global | globalThis) eg. `window['tst-reflect-disable'] = true;`"); + } + + // In case of direct call, we'll return Unknown type. + return Type.Unknown; +} + +/** @internal */ +getType.__tst_reflect__ = true; + +/** + * Class decorator which marks classes to be processed and included in metadata lib file. + * @reflectDecorator + */ +export function reflect(Constructor: { new(...args: any[]): TType }) +{ + getType(); +} + +reflect.__tst_reflect__ = true; + + +function createNativeType(typeName: string, ctor?: Function): Type +{ + const type = Reflect.construct(Type, [], TypeActivator); + + type.initialize({ + n: typeName, + fn: typeName, + ctor: () => ctor, + k: TypeKind.Native + }); + + return type; +} + +/** + * List of native types + * @description It should save some memory and all native Types will be the same instances. + */ +const nativeTypes = { + "Object": createNativeType("Object", Object), + "Unknown": createNativeType("unknown"), + "Any": createNativeType("any"), + "Void": createNativeType("void"), + "String": createNativeType("String", String), + "Number": createNativeType("Number", Number), + "Boolean": createNativeType("Boolean", Boolean), + "Date": createNativeType("Date", Date), + "Null": createNativeType("null"), + "Undefined": createNativeType("undefined"), +}; + +(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 + */ +export const NativeTypes = new Map(); + +for (let entry of Object.entries(nativeTypes)) +{ + NativeTypes.set(entry[0].toLowerCase(), entry[1]); +} + +for (const typeName in nativeTypes) +{ + if (nativeTypes.hasOwnProperty(typeName)) + { + (Type as any)[typeName] = (nativeTypes as any)[typeName]; + } } \ No newline at end of file diff --git a/runtime/src/type-builder/ArrayBuilder.ts b/runtime/src/type-builder/ArrayBuilder.ts new file mode 100644 index 0000000..871a8c1 --- /dev/null +++ b/runtime/src/type-builder/ArrayBuilder.ts @@ -0,0 +1,41 @@ +import { TypeKind } from "../enums"; +import { Type } from "../Type"; +import { TypeBuilderBase } from "./TypeBuilderBase"; + +export class ArrayTypeBuilder extends TypeBuilderBase +{ + private type?: Type; + + /** + * @internal + */ + constructor() + { + super(); + this.typeName = "Array"; + } + + /** + * Set generic type of the Array + * @param type + */ + setGenericType(type: Type) + { + this.type = type; + return this; + } + + /** + * @inheritDoc + */ + build(): Type + { + return Type.store.wrap({ + k: TypeKind.Native, + n: this.typeName, + fn: this.fullName, + args: [this.type ?? Type.Any], + ctor: () => Array + }); + } +} \ No newline at end of file diff --git a/runtime/src/type-builder/IntersectionTypeBuilder.ts b/runtime/src/type-builder/IntersectionTypeBuilder.ts new file mode 100644 index 0000000..8f0f3df --- /dev/null +++ b/runtime/src/type-builder/IntersectionTypeBuilder.ts @@ -0,0 +1,36 @@ +import { TypeKind } from "../enums"; +import { Type } from "../Type"; +import { TypeBuilderBase } from "./TypeBuilderBase"; + +export class IntersectionTypeBuilder extends TypeBuilderBase +{ + private types: Set = new Set(); + + /** + * Add types to union + * @param types + */ + addTypes(...types: Type[]) + { + for (let type of types) + { + this.types.add(type); + } + + return this; + } + + /** + * @inheritDoc + */ + build(): Type + { + return Type.store.wrap({ + k: TypeKind.Container, + n: this.typeName, + fn: this.fullName, + inter: true, + types: Array.from(this.types) + }); + } +} \ No newline at end of file diff --git a/runtime/src/type-builder/TypeBuilder.ts b/runtime/src/type-builder/TypeBuilder.ts new file mode 100644 index 0000000..598e553 --- /dev/null +++ b/runtime/src/type-builder/TypeBuilder.ts @@ -0,0 +1,26 @@ +import { Type } from "../Type"; +import { ArrayTypeBuilder } from "./ArrayBuilder"; +import { IntersectionTypeBuilder } from "./IntersectionTypeBuilder"; +import { UnionTypeBuilder } from "./UnionTypeBuilder"; + +export class TypeBuilder +{ + private constructor() + { + } + + static createUnion(types: Type[]): UnionTypeBuilder + { + return new UnionTypeBuilder().addTypes(...types); + } + + static createIntersection(types: Type[]): IntersectionTypeBuilder + { + return new IntersectionTypeBuilder().addTypes(...types); + } + + static createArray(): ArrayTypeBuilder + { + return new ArrayTypeBuilder(); + } +} \ No newline at end of file diff --git a/runtime/src/type-builder/TypeBuilderBase.ts b/runtime/src/type-builder/TypeBuilderBase.ts new file mode 100644 index 0000000..03645f0 --- /dev/null +++ b/runtime/src/type-builder/TypeBuilderBase.ts @@ -0,0 +1,35 @@ +import { Type } from "../Type"; + +let typeIdCounter = 0; + +export abstract class TypeBuilderBase +{ + protected typeName: string = "dynamic"; + protected fullName: string; + + constructor() + { + this.fullName = TypeBuilderBase.generateFullName(); + } + + /** + * Generates full name for dynamic type. + * @description Generated name should be unique. + * @private + */ + private static generateFullName() + { + return "@@dynamic/" + Date.now().toString(16) + (++typeIdCounter); + } + + setName(typeName: string) + { + + this.typeName = typeName; + } + + /** + * Build Type from configured properties. + */ + abstract build(): Type; +} \ No newline at end of file diff --git a/runtime/src/type-builder/UnionTypeBuilder.ts b/runtime/src/type-builder/UnionTypeBuilder.ts new file mode 100644 index 0000000..40c8e5e --- /dev/null +++ b/runtime/src/type-builder/UnionTypeBuilder.ts @@ -0,0 +1,50 @@ +import { TypeKind } from "../enums"; +import { Type } from "../Type"; +import { TypeBuilderBase } from "./TypeBuilderBase"; + +export class UnionTypeBuilder extends TypeBuilderBase +{ + private types: Set = new Set(); + + /** + * Add types to union + * @param types + */ + addTypes(...types: Type[]) + { + for (let type of types) + { + this.types.add(type); + } + + return this; + } + + /** + * Build Union Type. + * Does not return Union if there is only one type. + * Returns Type.Undefined if there is no type. + */ + build(): Type + { + const types = Array.from(this.types); + + if (types.length === 0) + { + return Type.Undefined; + } + + if (types.length === 1) + { + return types[0]; + } + + return Type.store.wrap({ + k: TypeKind.Container, + n: this.typeName, + fn: this.fullName, + union: true, + types: types + }); + } +} \ No newline at end of file diff --git a/transformer/src/declarations.ts b/transformer/src/declarations.ts index 42fd864..33639cd 100644 --- a/transformer/src/declarations.ts +++ b/transformer/src/declarations.ts @@ -98,8 +98,20 @@ export interface PropertyDescriptionSource */ export interface DecoratorDescriptionSource { + /** + * Name of the decorator + */ n: string; + + /** + * Full name of the decorator + */ fn?: string; + + /** + * List of constant arguments + */ + args?: Array; } /** diff --git a/transformer/src/getConstructors.ts b/transformer/src/getConstructors.ts index b5bbd0d..08ace57 100644 --- a/transformer/src/getConstructors.ts +++ b/transformer/src/getConstructors.ts @@ -2,6 +2,7 @@ import * as ts from "typescript"; import { ConstructorDescriptionSource } from "./declarations"; import { getCtorTypeReference, + getDeclaration, getType } from "./helpers"; import { getTypeCall } from "./getTypeCall"; @@ -20,7 +21,7 @@ export function getConstructors(type: ts.Type, context: Context) for (paramSymbol of ctor.parameters) { paramType = getType(paramSymbol, context.typeChecker); - const declaration = paramSymbol.valueDeclaration as ts.ParameterDeclaration; + const declaration = getDeclaration(paramSymbol) as ts.ParameterDeclaration; params.push({ n: paramSymbol.getName(), diff --git a/transformer/src/getDecorators.ts b/transformer/src/getDecorators.ts index 4fc6a02..b21d659 100644 --- a/transformer/src/getDecorators.ts +++ b/transformer/src/getDecorators.ts @@ -1,39 +1,73 @@ import * as ts from "typescript"; import { DecoratorDescriptionSource } from "./declarations"; -import { getTypeFullName } from "./helpers"; +import { getNodeLocationText } from "./getNodeLocationText"; +import { + getDeclaration, + getTypeFullName +} from "./helpers"; +import { log } from "./log"; export function getDecorators(symbol: ts.Symbol, checker: ts.TypeChecker): Array | undefined { + const declaration = getDeclaration(symbol); - if (!symbol.valueDeclaration?.decorators) + if (!declaration?.decorators) { return undefined; } const decorators: Array = []; - for (let decorator of symbol.valueDeclaration.decorators) + for (let decorator of declaration.decorators) { - const firstToken = decorator.expression.getFirstToken(); + const identifier = ts.isCallExpression(decorator.expression) + ? decorator.expression.getFirstToken() + : ts.isIdentifier(decorator.expression) + ? decorator.expression + : undefined; - if (!firstToken) + if (!identifier) { + log.warn(`Identifier of some decorator on ${symbol.escapedName} not found.`); continue; } - const symbol = checker.getSymbolAtLocation(firstToken); + const decoratorSymbol = checker.getSymbolAtLocation(identifier); + const decoratorDeclaration = getDeclaration(decoratorSymbol); - if (!symbol || !symbol.valueDeclaration) + if (!decoratorDeclaration) { // TODO: Log in debug mode continue; } - let decoratorType = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration); + let decoratorType = checker.getTypeOfSymbolAtLocation(decoratorSymbol!, decoratorDeclaration); + let args: Array = []; + + if (ts.isCallExpression(decorator.expression)) + { + for (let arg of decorator.expression.arguments) + { + const type = checker.getTypeAtLocation(arg); + + if (type.isLiteral()) + { + args.push(type.value); + } + else + { + + + log.warn("Unexpected decorator argument. Only constant values are allowed.\n\tAt " + getNodeLocationText(arg)); + args.push(ts.factory.createNull()); + } + } + } decorators.push({ - n: symbol.escapedName.toString(), - fn: getTypeFullName(decoratorType.getSymbol()) + n: decoratorSymbol!.escapedName.toString(), + fn: getTypeFullName(decoratorType.getSymbol()), + args: args.length == 0 ? undefined : args }); } diff --git a/transformer/src/getProperties.ts b/transformer/src/getProperties.ts index 9de2dd5..5a5e551 100644 --- a/transformer/src/getProperties.ts +++ b/transformer/src/getProperties.ts @@ -7,6 +7,7 @@ import { getAccessModifier, getAccessor, getCtorTypeReference, + getDeclaration, getType, isReadonly } from "./helpers"; @@ -27,14 +28,16 @@ export function getProperties(symbol: ts.Symbol | undefined, type: ts.Type, cont .filter(m => (m.flags & ts.SymbolFlags.Property) == ts.SymbolFlags.Property || (m.flags & ts.SymbolFlags.GetAccessor) == ts.SymbolFlags.GetAccessor || (m.flags & ts.SymbolFlags.SetAccessor) == ts.SymbolFlags.SetAccessor) .map((memberSymbol: ts.Symbol) => { + const declaration = getDeclaration(memberSymbol); + return { n: memberSymbol.escapedName.toString(), t: getTypeCall(getType(memberSymbol, context.typeChecker), memberSymbol, context, getCtorTypeReference(memberSymbol)), d: getDecorators(memberSymbol, context.typeChecker), - am: getAccessModifier(memberSymbol.valueDeclaration?.modifiers), - acs: getAccessor(memberSymbol.valueDeclaration), - ro: isReadonly(memberSymbol.valueDeclaration?.modifiers), - o: memberSymbol.valueDeclaration && ts.isPropertyDeclaration(memberSymbol.valueDeclaration) && !!memberSymbol.valueDeclaration.questionToken + am: getAccessModifier(declaration?.modifiers), + acs: getAccessor(declaration), + ro: isReadonly(declaration?.modifiers), + o: declaration && ts.isPropertyDeclaration(declaration) && !!declaration.questionToken }; }); diff --git a/transformer/src/helpers.ts b/transformer/src/helpers.ts index 916432d..e07791c 100644 --- a/transformer/src/helpers.ts +++ b/transformer/src/helpers.ts @@ -57,17 +57,33 @@ let unknownTypeCallExpression: GetTypeCall | undefined = undefined; */ export function getType(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Type { - if (symbol.flags == ts.SymbolFlags.Interface || symbol.flags == ts.SymbolFlags.Alias) + if (symbol.flags == ts.SymbolFlags.Interface/* || symbol.flags == ts.SymbolFlags.Alias*/) { return checker.getDeclaredTypeOfSymbol(symbol); } - if (!symbol.valueDeclaration) + const declaration = getDeclaration(symbol); + + if (!declaration) { throw new Error("Unable to resolve declarations of symbol."); } - return checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration); + return checker.getTypeOfSymbolAtLocation(symbol, declaration); +} + +/** + * Returns declaration of symbol. ValueDeclaration is preferred. + * @param symbol + */ +export function getDeclaration(symbol?: ts.Symbol): ts.Declaration | undefined +{ + if (!symbol) + { + return undefined; + } + + return symbol.valueDeclaration || symbol.declarations?.[0]; } /** @@ -141,7 +157,7 @@ export function getTypeFullName(typeSymbol?: ts.Symbol) */ export function isExpression(value: any) { - return value.hasOwnProperty("kind") && (value.constructor.name == "NodeObject" || value.constructor.name == "IdentifierObject"); + return value.hasOwnProperty("kind") && (value.constructor.name == "NodeObject" || value.constructor.name == "IdentifierObject" || value.constructor.name == "TokenObject"); } /** @@ -381,9 +397,11 @@ export function getUnknownTypeCall(context: Context): GetTypeCall */ export function getFunctionLikeSignature(symbol: ts.Symbol, checker: ts.TypeChecker): ts.Signature | undefined { - if (symbol.valueDeclaration && ts.isMethodSignature(symbol.valueDeclaration)) + const declaration = getDeclaration(symbol); + + if (declaration && ts.isMethodSignature(declaration)) { - return checker.getSignatureFromDeclaration(symbol.valueDeclaration); + return checker.getSignatureFromDeclaration(declaration); } return checker.getSignaturesOfType(getType(symbol, checker), ts.SignatureKind.Call)?.[0]; @@ -484,22 +502,24 @@ export function isTypedDeclaration(declaration: ts.Declaration): declaration is // This allows us to get the ctor node which we resolve descriptor info and create the ctor require export function getCtorTypeReference(symbol: ts.Symbol): ts.Identifier | undefined { - if (!symbol.valueDeclaration) + const declaration = getDeclaration(symbol); + + if (!declaration) { return undefined; } - if (isTypedDeclaration(symbol.valueDeclaration)) + if (isTypedDeclaration(declaration)) { let typeName: ts.Identifier | undefined = undefined; - if (ts.isIndexedAccessTypeNode(symbol.valueDeclaration.type)) + if (ts.isIndexedAccessTypeNode(declaration.type)) { - typeName = (symbol.valueDeclaration.type.indexType as any).typeName; + typeName = (declaration.type.indexType as any).typeName; } else { - typeName = (symbol.valueDeclaration.type as any).typeName; + typeName = (declaration.type as any).typeName; } if (typeName && typeName?.kind === SyntaxKind.Identifier) diff --git a/transformer/src/processDecorator.ts b/transformer/src/processDecorator.ts index 8197057..a5fbb00 100644 --- a/transformer/src/processDecorator.ts +++ b/transformer/src/processDecorator.ts @@ -7,11 +7,6 @@ import { updateCallExpression } from "./updateCallExpr export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, context: Context): ts.Decorator | undefined { - if (!ts.isCallExpression(node.expression)) - { - return undefined; - } - // Method/function declaration const declaration = decoratorType.symbol.declarations?.[0] as ts.FunctionLikeDeclarationBase; @@ -36,7 +31,8 @@ export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, con const genericType = context.typeChecker.getTypeAtLocation(genericTypeNode); const genericTypeSymbol = genericType.getSymbol(); - return ts.factory.updateDecorator(node, updateCallExpression(node.expression, state, [{ + let callExpression: ts.CallExpression; + const typeArgumentDescription = { genericTypeName: genericParamName, reflectedType: getTypeCall( genericType, @@ -44,5 +40,25 @@ export function processDecorator(node: ts.Decorator, decoratorType: ts.Type, con context, genericTypeNode.name ) - }])); + }; + + if (ts.isCallExpression(node.expression)) + { + callExpression = updateCallExpression(node.expression, state, [typeArgumentDescription]); + } + else if (ts.isIdentifier(node.expression)) + { + callExpression = ts.factory.createCallExpression(node.expression, undefined, [ + ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment( + typeArgumentDescription.genericTypeName, + typeArgumentDescription.reflectedType + )]) + ]); + } + else + { + return undefined; + } + + return ts.factory.updateDecorator(node, callExpression); } \ No newline at end of file diff --git a/transformer/src/processGenericCallExpression.ts b/transformer/src/processGenericCallExpression.ts index 629c309..7ca5b74 100644 --- a/transformer/src/processGenericCallExpression.ts +++ b/transformer/src/processGenericCallExpression.ts @@ -3,6 +3,7 @@ import { Context } from "./contexts/Conte import { FunctionLikeDeclarationGenericParametersDetail } from "./FunctionLikeDeclarationGenericParametersDetail"; import { getGenericParametersDetails } from "./getGenericParametersDetails"; import { getTypeCall } from "./getTypeCall"; +import { log } from "./log"; import { TypeArgumentValueDescription, updateCallExpression @@ -17,8 +18,17 @@ export function processGenericCallExpression(node: ts.CallExpression, fncType: t // Method/function declaration; take the only one or find right declaration by signature. const declaration = (fncType.symbol.declarations.length > 1 - ? context.typeChecker.getResolvedSignature(node)?.declaration - : fncType.symbol.declarations[0]) as ts.FunctionLikeDeclarationBase; + ? ( + context.typeChecker.getResolvedSignature(node)?.declaration + || context.typeChecker.getSignaturesOfType(fncType, ts.SignatureKind.Call) + ) + : fncType.symbol.declarations[0]) as ts.FunctionLikeDeclarationBase | undefined; + + if (!declaration) + { + log.error("Unable to resolve declaration of symbol signature."); + return undefined; + } // Try to get State const state: FunctionLikeDeclarationGenericParametersDetail = getGenericParametersDetails( diff --git a/transformer/src/visitors/mainVisitor.ts b/transformer/src/visitors/mainVisitor.ts index f5cb58f..13a96ae 100644 --- a/transformer/src/visitors/mainVisitor.ts +++ b/transformer/src/visitors/mainVisitor.ts @@ -5,8 +5,9 @@ import { import * as ts from "typescript"; import { Context } from "../contexts/Context"; import { + getType, hasReflectDecoratorJsDoc -} from "../helpers"; +} from "../helpers"; import { log } from "../log"; import { processDecorator } from "../processDecorator"; import { processGenericCallExpression } from "../processGenericCallExpression"; @@ -28,7 +29,6 @@ export function mainVisitor(nodeToVisit: ts.Node, context: Context): ts.Node | u return nodeToVisit; } - // Is it call expression? if (ts.isCallExpression(node)) { @@ -92,12 +92,28 @@ export function mainVisitor(nodeToVisit: ts.Node, context: Context): ts.Node | u } } } - else if (ts.isDecorator(node) && ts.isClassDeclaration(node.parent) && ts.isCallExpression(node.expression)) + else if (ts.isDecorator(node) && ts.isClassDeclaration(node.parent)) { // type of decorator - const type = context.typeChecker.getTypeAtLocation(node.expression.expression); + let type: ts.Type | undefined = undefined; + + if (ts.isCallExpression(node.expression)) + { + type = context.typeChecker.getTypeAtLocation(node.expression.expression); + } + else if (ts.isIdentifier(node.expression)) + { + const symbol = context.typeChecker.getSymbolAtLocation(node.expression); + + if (symbol) + { + type = getType(symbol, context.typeChecker); + } + } - if (hasReflectDecoratorJsDoc(type.getSymbol())) + // TODO: support property decorators + + if (type && hasReflectDecoratorJsDoc(type.getSymbol())) { const res = processDecorator(node, type, context);