Skip to content

Commit

Permalink
feat(reflect): Added support for static methods and properties reflec…
Browse files Browse the repository at this point in the history
…tion (#1023)
  • Loading branch information
pierissimo committed Dec 16, 2022
1 parent 9134b6d commit f8ee76d
Show file tree
Hide file tree
Showing 10 changed files with 692 additions and 41 deletions.
40 changes: 33 additions & 7 deletions packages/reflect/src/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export function addsDesignTypes(meta: TypedReflection, ctx: WalkMemberContext):
const returnType: any = Reflect.getOwnMetadata(DESIGN_RETURN_TYPE, ctx.target.prototype, meta.name)
return { ...meta, returnType }
}
else if (meta.kind === "StaticMethod") {
const returnType: any = Reflect.getOwnMetadata(DESIGN_RETURN_TYPE, ctx.target, meta.name)
return { ...meta, returnType }
}
else if (reflection.isParameterProperties(meta)) {
const parTypes: any[] = Reflect.getOwnMetadata(DESIGN_PARAMETER_TYPE, ctx.target) || []
return { ...meta, type: getType(parTypes, meta.index) }
Expand All @@ -36,6 +40,10 @@ export function addsDesignTypes(meta: TypedReflection, ctx: WalkMemberContext):
const type: any = Reflect.getOwnMetadata(DESIGN_TYPE, ctx.target.prototype, meta.name)
return { ...meta, type }
}
else if (meta.kind === "StaticProperty") {
const type: any = Reflect.getOwnMetadata(DESIGN_TYPE, ctx.target, meta.name)
return { ...meta, type }
}
else if (meta.kind === "Parameter" && ctx.parent.kind === "Constructor") {
const parTypes: any[] = Reflect.getOwnMetadata(DESIGN_PARAMETER_TYPE, ctx.target) || []
return { ...meta, type: getType(parTypes, meta.index) }
Expand All @@ -44,6 +52,10 @@ export function addsDesignTypes(meta: TypedReflection, ctx: WalkMemberContext):
const parTypes: any[] = Reflect.getOwnMetadata(DESIGN_PARAMETER_TYPE, ctx.target.prototype, ctx.parent.name) || []
return { ...meta, type: getType(parTypes, meta.index) }
}
else if (meta.kind === "Parameter" && ctx.parent.kind === "StaticMethod") {
const parTypes: any[] = Reflect.getOwnMetadata(DESIGN_PARAMETER_TYPE, ctx.target, ctx.parent.name) || []
return { ...meta, type: getType(parTypes, meta.index) }
}
else
return meta
}
Expand All @@ -67,7 +79,7 @@ export function addsDecorators(meta: TypedReflection, ctx: WalkMemberContext): T
.concat(...getMetadata(ctx.target, meta.name))
return { ...meta, decorators: meta.decorators.concat(decorators) }
}
if (meta.kind === "Method" || meta.kind === "Property") {
if (meta.kind === "Method" || meta.kind === "Property" || meta.kind === "StaticMethod" || meta.kind === "StaticProperty") {
const decorators = getMetadata(ctx.target, meta.name)
return { ...meta, decorators: meta.decorators.concat(decorators) }
}
Expand Down Expand Up @@ -135,7 +147,7 @@ function isTypeWithGenericParameter(decorator: TypeDecorator) {
}

function setType(meta: MethodReflection | PropertyReflection | ParameterReflection | ParameterPropertyReflection, type: Class | Class[]) {
if (meta.kind === "Method")
if (meta.kind === "Method" || meta.kind === "StaticMethod")
return { returnType: type }
return { type }
}
Expand All @@ -155,10 +167,10 @@ export function addsTypeByTypeDecorator(meta: TypedReflection, ctx: WalkMemberCo
}
if (isTypeWithGenericParameter(decorator)) {
const genericParams: any[] = decorator.genericArguments.map(x => {
const s = Array.isArray(x) ? x[0] : x
const s = Array.isArray(x) ? x[0] : x
// if the generic arguments already a class then return immediately
return typeof s === "function" ? x : generic.getType({ type: x, target: decorator.target }, ctx.target)
} )
})
const [parentType, isArray] = reflection.getTypeFromDecorator(decorator)
const dynType = createClass({}, { extends: parentType as any, genericParams })
const type = isArray ? [dynType] : dynType
Expand All @@ -179,9 +191,9 @@ export function addsTypeClassification(meta: TypedReflection, ctx: WalkMemberCon
else if (reflection.isCustomClass(type)) return "Class"
else return "Primitive"
}
if (meta.kind === "Method")
if (meta.kind === "Method" || meta.kind === "StaticMethod")
return { ...meta, typeClassification: get(meta.returnType) }
else if (meta.kind === "Property" || meta.kind === "Parameter")
else if (meta.kind === "Property" || meta.kind === "Parameter" || meta.kind === "StaticProperty")
return { ...meta, typeClassification: get(meta.type) }
else if (meta.kind === "Class")
return { ...meta, typeClassification: "Class" }
Expand All @@ -205,9 +217,23 @@ export function addsParameterProperties(meta: TypedReflection, ctx: WalkMemberCo
// --------------------------------------------------------------------- //

export function removeIgnored(meta: TypedReflection, ctx: WalkMemberContext): TypedReflection | undefined {
if (meta.kind === "Property" || meta.kind === "Method") {
if (meta.kind === "Property" || meta.kind === "Method" || meta.kind === "StaticMethod" || meta.kind === "StaticProperty") {
const decorator = meta.decorators.find((x: PrivateDecorator): x is PrivateDecorator => x.kind === "Ignore")
return !decorator ? meta : undefined
}
return meta
}


// --------------------------------------------------------------------- //
// ---------------------- REMOVE IS_STATIC OPTION ---------------------- //
// --------------------------------------------------------------------- //

export function removeIsStaticDecoratorOption(meta: TypedReflection, ctx: WalkMemberContext): TypedReflection | undefined {
if (meta.kind === "Constructor") return meta
for (const decorator of meta.decorators) {
const opt: DecoratorOption = decorator[DecoratorOptionId]
delete opt.isStatic
}
return meta
}
17 changes: 9 additions & 8 deletions packages/reflect/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ export function decorateMethod(callback: ((target: Class, name: string) => any),
export function decorateMethod(data: any, option?: DecoratorOption): MethodDecorator
export function decorateMethod(data: any, option?: DecoratorOption) {
return (target: any, name: string | symbol) => {
const targetClass = target.constructor
const isStatic = typeof target === 'function'
const targetClass = isStatic ? target: target.constructor
const meta = typeof data === "function" ? data(targetClass, name) : data
setMetadata({ ...meta, [DecoratorOptionId]: { ...meta[DecoratorOptionId], ...option } }, targetClass, name)
setMetadata({ ...meta, [DecoratorOptionId]: { ...meta[DecoratorOptionId], isStatic, ...option } }, targetClass, name)
}
}

Expand Down Expand Up @@ -103,13 +104,13 @@ export function decorate(data: any | ((...args: any[]) => any), targetTypes: Dec
}

/**
* Compose multiple metadata decorators into single decorator.
*
* Compose multiple metadata decorators into single decorator.
*
* ```
* function merged() {
* return mergeDecorator(decoratorOne(), decoratorTwo(), decoratorThree())
* }
*
*
* @merged()
* class TargetClass {
* }
Expand Down Expand Up @@ -189,7 +190,7 @@ export namespace generic {
* Get data type of declaration in specific class based on its type specified by type("T")
* @param decorator type() decorator contains generic type information
* @param typeTarget The current type where the type will be calculated
* @returns
* @returns
*/
export function getType(decorator: { type: TypeOverride | ((x: any) => TypeOverride), target: Class }, typeTarget: Class): Class | Class[] | undefined {
const getParent = (type: Class): Class[] => {
Expand All @@ -215,7 +216,7 @@ export namespace generic {
if (!templateDec)
throw new Error(`${decorator.target.name} doesn't have @generic.parameter() decorator required by generic parameter ${type}`)
/*
get list of parents, for example
get list of parents, for example
A <-- B <-- C <-- D (A is super super)
const result = getParent(D)
result = [B, C, D]
Expand All @@ -231,7 +232,7 @@ export namespace generic {
if (typeDec) {
if (typeDec.types.length !== templateDec.templates.length)
throw new Error(`Number of parameters @generic.parameter() and @generic.argument() mismatch between ${tmpType.name} and ${type.name}`)
// get the actual type in @generic.argument() list
// get the actual type in @generic.argument() list
result = typeDec.types[index] as any
// check if the result is a "function" (Class) then return immediately
const finalResult = isArray ? [result] : result
Expand Down
89 changes: 68 additions & 21 deletions packages/reflect/src/parser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Node, parse } from "acorn"
import { useCache } from "./helpers"

import { getAllMetadata } from "./storage"
import { getAllMetadata, getMetadata } from "./storage"
import {
Class,
ClassReflection,
Expand Down Expand Up @@ -35,6 +35,7 @@ type RootNode = string | KeyValueNode | ObjectNode | ArrayNode
type KeyValueNode = { kind: "KeyValue", key: string, value: RootNode }
interface ObjectNode { kind: "Object", members: RootNode[] }
interface ArrayNode { kind: "Array", members: RootNode[] }
const StaticMemberExclude = ["name", "prototype", "length"]

function getNamesFromAst(nodes: any[]) {
const getName = (node: any): undefined | RootNode => {
Expand All @@ -51,7 +52,7 @@ function getNamesFromAst(nodes: any[]) {
return { kind: "Object", members: node.properties.map((x: any) => getName(x)) }
}
//if (node.type === "ArrayPattern") {
return { kind: "Array", members: node.elements.map((x: any) => getName(x)) }
return { kind: "Array", members: node.elements.map((x: any) => getName(x)) }
//}
}
return nodes.map(x => getName(x)).filter((x): x is RootNode => !!x)
Expand Down Expand Up @@ -88,34 +89,44 @@ function getConstructorParameters(fn: Class) {
function getFunctionParameters(fn: Function) {
try {
const body = getCode(fn)
const ast:any = parse(body, { ecmaVersion: 2020 })
const ast: any = parse(body, { ecmaVersion: 2020 })
const expBody = ast.body[0]
if(expBody.type === "FunctionDeclaration")
if (expBody.type === "FunctionDeclaration")
return getNamesFromAst((ast as any).body[0].params)
else
else
return getNamesFromAst((ast as any).body[0].expression.params)
}
catch {
return []
}
}

function getClassMembers(fun: Function) {
const isGetter = (name: string) => Object.getOwnPropertyDescriptor(fun.prototype, name)!.get
const isSetter = (name: string) => Object.getOwnPropertyDescriptor(fun.prototype, name)!.set
const isFunction = (name: string) => typeof fun.prototype[name] === "function";
const members = Object.getOwnPropertyNames(fun.prototype)
function getMembers(obj: any, excludes: string[]) {
const isGetter = (name: string) => Object.getOwnPropertyDescriptor(obj, name)!.get
const isSetter = (name: string) => Object.getOwnPropertyDescriptor(obj, name)!.set
const isFunction = (name: string) => typeof obj[name] === "function";
const members = Object.getOwnPropertyNames(obj)
.filter(name => isGetter(name) || isSetter(name) || isFunction(name))
const properties = (getAllMetadata(fun as Class) || [])
return members.filter(name => !excludes.includes(name))
.filter(name => !~name.indexOf("__"))
}

function getMembersByDecorator(obj: Class) {
const properties = (getAllMetadata(obj as Class) || [])
.filter(x => !!x.memberName && !x.parIndex)
.filter(x => {
const opt: DecoratorOption = x.data[DecoratorOptionId]
return opt.applyTo!.length === 0
})
.map(x => x.memberName as string)
const names = members.concat(properties)
.filter(name => name !== "constructor" && !~name.indexOf("__"))
return [...new Set(names)]
return properties
}

function getClassMembers(fun: Function) {
const member = getMembers(fun.prototype, ["constructor"])
const staticMembers = getMembers(fun, StaticMemberExclude)
const decoratorMembers = getMembersByDecorator(fun as Class).filter(name => name !== "constructor")
return [...new Set([...member, ...decoratorMembers, ...staticMembers])]
}

function getName(param: RootNode): string {
Expand All @@ -133,6 +144,17 @@ function getField(params: RootNode): any {
if (params.kind === "Object") return [...getFields(params.members)]
return [...getFields(params.members)]
}

function getMemberTypeDescriptor(owner: any, name: string) {
const descriptor = Reflect.getOwnPropertyDescriptor(owner, name)
if (!descriptor) return false
const type: "Getter" | "Setter" | "Method" | "Field" = !!descriptor.get ? "Getter" :
!!descriptor.set ? "Setter" :
typeof descriptor.value === "function" ? "Method" :
"Field"
return { type, ...descriptor }
}

// --------------------------------------------------------------------- //
// ------------------------------- PARSER ------------------------------ //
// --------------------------------------------------------------------- //
Expand All @@ -150,8 +172,15 @@ function parseMethods(owner: Class): MethodReflection[] {
const result: MethodReflection[] = []
const members = getClassMembers(owner)
for (const name of members) {
const des = Reflect.getOwnPropertyDescriptor(owner.prototype, name)
if (des && typeof des.value === "function" && !des.get && !des.set) {
// static methods
const classDes = getMemberTypeDescriptor(owner, name)
if (classDes && classDes.type === "Method") {
const parameters = getMethodParameters(owner, name).map((x, i) => parseParameter(x, i))
result.push({ kind: "StaticMethod", name, parameters, decorators: [], returnType: undefined })
}
// instance method
const protoDes = getMemberTypeDescriptor(owner.prototype, name)
if (protoDes && protoDes.type === "Method") {
const parameters = getMethodParameters(owner, name).map((x, i) => parseParameter(x, i))
result.push({ kind: "Method", name, parameters, decorators: [], returnType: undefined })
}
Expand All @@ -163,12 +192,30 @@ function parseProperties(owner: Class): PropertyReflection[] {
const result: PropertyReflection[] = []
const members = getClassMembers(owner)
for (const name of members) {
const des = Reflect.getOwnPropertyDescriptor(owner.prototype, name)
if (!des || des.get || des.set) {
result.push({ kind: "Property", name, decorators: [], get: des?.get, set: des?.set })
if(typeof (owner as any)[name] === "function" || typeof owner.prototype[name] === "function") continue;
// static property
const classDes = getMemberTypeDescriptor(owner, name)
if (classDes && !StaticMemberExclude.includes(name)) {
result.push({ kind: "StaticProperty", name, decorators: [], get: classDes.get, set: classDes.set })
continue;
}
// instance property
const protoDes = getMemberTypeDescriptor(owner.prototype, name)
if (protoDes) {
result.push({ kind: "Property", name, decorators: [], get: protoDes.get, set: protoDes.set })
continue;
}
// typescript field can't be described using getOwnPropertyDescriptor
// instead we need to apply decorator on it.
const metadata = getMetadata(owner, name)
const isStatic = metadata.some(meta => {
const opt:DecoratorOption = meta[DecoratorOptionId]
return !!opt.isStatic
})
result.push({ kind: isStatic ? "StaticProperty" : "Property", name, decorators: [], get: undefined, set: undefined })

}
// include constructor parameter
// include constructor parameter (for constructor property parameter)
const params = getConstructorParameters(owner)
for (const [i, name] of params.entries()) {
const { fields, ...par } = parseParameter(name, i)
Expand Down Expand Up @@ -200,4 +247,4 @@ function parseClassNoCache(fn: Class): ClassReflection {
const cacheStore = new Map<Class, ClassReflection>()
const parseClass = useCache(cacheStore, parseClassNoCache, x => x)

export { parseClass, parseFunction, getMethodParameters, getConstructorParameters, getFunctionParameters, getClassMembers }
export { parseClass, parseFunction, getMethodParameters, getConstructorParameters, getFunctionParameters, getClassMembers }
4 changes: 3 additions & 1 deletion packages/reflect/src/reflect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ function reflectClass(target: Class): ClassReflection {
// add parameter properties
v.addsParameterProperties,
// remove @ignore decorator
v.removeIgnored,
v.removeIgnored
]),
classPath: []
})
Expand All @@ -38,6 +38,8 @@ function reflectClass(target: Class): ClassReflection {
v.addsTypeByTypeDecorator,
// add typeClassification information
v.addsTypeClassification,
// remove isStatic decorator option
v.removeIsStaticDecoratorOption
])
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/reflect/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const storage = new Map<Class, MetadataRecord[]>()

export function setMetadata(data: any, targetClass: Class, memberName?: string | symbol, parIndex?: number) {
const getMetadataOption = (opt?: DecoratorOption): Required<DecoratorOption> => ({
inherit: true, allowMultiple: true, applyTo: [], removeApplied: true, ...opt
inherit: true, allowMultiple: true, applyTo: [], removeApplied: true, isStatic:false, ...opt
})
const opt = data[DecoratorOptionId] = getMetadataOption(data[DecoratorOptionId])
if (!opt.allowMultiple && !data[DecoratorId]) {
Expand Down
7 changes: 6 additions & 1 deletion packages/reflect/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface ParameterReflection extends ReflectionBase {
index: number
}
export interface PropertyReflection extends ReflectionBase {
kind: "Property",
kind: "Property" | "StaticProperty",
decorators: any[],
type?: any,
get?: any,
Expand Down Expand Up @@ -133,6 +133,11 @@ export interface DecoratorOption {
* Remove applied decorator using `applyTo` on the class scope. Default `true`
*/
removeApplied?: boolean

/**
* Applied for method/property, describe if its a static method/property
*/
isStatic?:boolean
}

export type CustomPropertyDecorator = (target: Object, propertyKey: string | symbol, ...index: any[]) => void;
Expand Down

0 comments on commit f8ee76d

Please sign in to comment.