diff --git a/nad-java-core/src/main/java/cn/lalaframework/nad/interfaces/NadClass.java b/nad-java-core/src/main/java/cn/lalaframework/nad/interfaces/NadClass.java index ce37cfe..8e919c6 100644 --- a/nad-java-core/src/main/java/cn/lalaframework/nad/interfaces/NadClass.java +++ b/nad-java-core/src/main/java/cn/lalaframework/nad/interfaces/NadClass.java @@ -23,4 +23,6 @@ public interface NadClass extends NadDef { @NonNull List getImportantMethods(); + + int getModifiers(); } diff --git a/nad-java-core/src/main/java/cn/lalaframework/nad/models/NadClassImpl.java b/nad-java-core/src/main/java/cn/lalaframework/nad/models/NadClassImpl.java index bd024f7..0f5c0ce 100644 --- a/nad-java-core/src/main/java/cn/lalaframework/nad/models/NadClassImpl.java +++ b/nad-java-core/src/main/java/cn/lalaframework/nad/models/NadClassImpl.java @@ -31,6 +31,8 @@ public class NadClassImpl extends NadDefImpl implements NadClass { @NonNull private final List importantMethods; + private final int modifiers; + /** * Create a NadClass from a standard java class. * @@ -39,6 +41,8 @@ public class NadClassImpl extends NadDefImpl implements NadClass { public NadClassImpl(Class clz) { super(clz); + modifiers = clz.getModifiers(); + // For each generic type parameter, collect them and convert to type name strings. typeParameters = Arrays.stream(clz.getTypeParameters()).map(NadContext::cc).collect(Collectors.toList()); @@ -99,4 +103,9 @@ public List getInnerClasses() { public List getImportantMethods() { return importantMethods; } + + @Override + public int getModifiers() { + return modifiers; + } } \ No newline at end of file diff --git a/packages/builder/package.json b/packages/builder/package.json index 72e17cb..235c623 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -1,6 +1,6 @@ { "name": "@huolala-tech/nad-builder", - "version": "1.1.2", + "version": "1.1.3", "description": "Convert the Java AST to client-side code", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/packages/builder/src/codegen/CodeGenForOc.ts b/packages/builder/src/codegen/CodeGenForOc.ts index e4f9484..3b36243 100644 --- a/packages/builder/src/codegen/CodeGenForOc.ts +++ b/packages/builder/src/codegen/CodeGenForOc.ts @@ -159,7 +159,7 @@ export class CodeGenForOc extends CodeGen { } private writeDefs() { - const list = this.root.declarationList.slice(0).sort((a, b) => { + const list = this.root.classList.sort((a, b) => { if (checkSuper(a, b)) return 1; if (checkSuper(b, a)) return -1; return 0; diff --git a/packages/builder/src/codegen/CodeGenForTs.ts b/packages/builder/src/codegen/CodeGenForTs.ts index ce45fa1..f5d5fac 100644 --- a/packages/builder/src/codegen/CodeGenForTs.ts +++ b/packages/builder/src/codegen/CodeGenForTs.ts @@ -191,7 +191,7 @@ export class CodeGenForTs extends CodeGen { } private writeClasses() { - for (const c of this.root.declarationList) { + for (const c of this.root.classList) { this.writeComment(() => { this.write(c.description || c.simpleName); this.write(`@iface ${c.name}`); diff --git a/packages/builder/src/helpers/tsHelper.ts b/packages/builder/src/helpers/tsHelper.ts index 3af420f..583a893 100644 --- a/packages/builder/src/helpers/tsHelper.ts +++ b/packages/builder/src/helpers/tsHelper.ts @@ -1,6 +1,6 @@ import { neverReachHere } from '../utils/neverReachHere'; import { Class } from '../models/Class'; -import type { Type } from '../models/Type'; +import { Type } from '../models/Type'; import { isJavaBoolean, isJavaList, @@ -13,7 +13,7 @@ import { isJavaVoid, isJavaWrapper, } from './javaHelper'; -import { toLowerCamel } from '../utils'; +import { toLowerCamel, notEmpty } from '../utils'; import { RootOptions } from '../models/RootOptions'; import { Enum, TypeUsage } from '../models'; @@ -34,6 +34,16 @@ export const ss = (u: string | number | boolean) => { throw neverReachHere(u); }; +const clzToTs = (clz: Class, parameters: readonly Type[]) => { + const { typeParameters, simpleName } = clz; + let t = simpleName; + if (typeParameters.length > 0) { + const pars = typeParameters.map((_, i) => t2s(parameters[i])).join(', '); + t += `<${pars}>`; + } + return t; +}; + const TS_UNKNOWN = 'unknown'; const uTypeCache: Record = Object.create(null); @@ -104,22 +114,65 @@ export const t2s = (type: Type | undefined): string => { if (!clz) return TS_UNKNOWN; if (clz instanceof Class) { - const { typeParameters, simpleName } = clz; - let t = simpleName; - if (typeParameters.length > 0) { - const pars = typeParameters.map((_, i) => t2s(parameters[i])).join(', '); - t += `<${pars}>`; - } + if (type.usage === TypeUsage.superType) return clzToTs(clz, parameters); - if (type.usage === TypeUsage.superType) return t; + // Merge all derivatived classes as a TypeScript union type. + // + // For example: + // + // ```java + // abstract class Animal {} + // class Cat extends Animal {} + // class Dog extends Animal {} + // ``` + // + // The `Animal` type will be `Cat | Dog` in TypeScript. + // NOTE: Normally, abstract class or interface will not be contained. + // + // Another example: + // + // ```java + // class BaseUserInfo {} + // class UserInfo extends BaseUserInfo {} + // ``` + // + // The `BaseUserInfo` type will be `BaseUserInfo | UserInfo`, because BaseUseInfo is not an abstact class. + // + const union = [ + // Place the current class and generic parameters. + [clz, parameters] as const, + // And add more derivatived refs with empty generic parameters. + ...[...builder.findDerivativedRefs(name)] + .map(([c]) => { + if (!builder.isUsing(c)) return null; + return [c, []] as const; + }) + .filter(notEmpty), + ] + // Filter out some pairs which have no constructor. + .filter(([n]) => n.hasConstructor()); - const { derivativedTypes } = clz; - if (derivativedTypes.length === 0) return t; - if (derivativedTypes.length === 1 && clz.isInterface) return t; - const ts = derivativedTypes.map(t2s); - if (!clz.isInterface) ts.push(t); + // TODO: Considers the generic parameter matching. + // + // For example: + // + // class Animal {} + // class Cat extends Animal {} + // class Dog extends Animal {} + // + // If a method returns Animal, it will be extended to Cat and Dog. + // If a method returns Animal, it will be extended to Dog and cannot match Cat. + // If a method returns Animal, it will be extended to Cat and Dog. + // + // But this feature is too complex, just mark this as a TODO temporarily. + // + + if (union.length === 0) return clzToTs(clz, parameters); + + const ts = union.map(([c, p]) => clzToTs(c, p)); return buildUnionType(...ts); } + if (clz instanceof Enum) { return clz.simpleName; } diff --git a/packages/builder/src/models/Annotated.ts b/packages/builder/src/models/Annotated.ts index b25501b..670e8db 100644 --- a/packages/builder/src/models/Annotated.ts +++ b/packages/builder/src/models/Annotated.ts @@ -1,3 +1,4 @@ +import { hideProperty } from '../utils'; import { Annotations } from './annotations'; import { u2a, u2o, u2s } from 'u2x'; @@ -9,6 +10,7 @@ export class Annotated; @@ -14,52 +14,55 @@ export class Class extends DefBase { * For example, the value of typeParameters is [ "T", "M" ] for `class Foo {}` */ public readonly typeParameters; - public readonly defName; public readonly description; + public readonly modifiers; + public readonly bounds; - constructor(raw: ClassRaw, builder: Root) { - super(raw, builder); + constructor(raw: ClassRaw, parent: Root) { + super(raw, parent); this.typeParameters = u2a(this.raw.typeParameters, u2s); - - this.defName = this.simpleName; + this.modifiers = u2n(this.raw.modifiers); + this.bounds = [this.raw.superclass, ...u2a(this.raw.interfaces)].map((i) => u2s(i)).filter(notEmpty); this.description = this.annotations.swagger.getApiModel()?.description; + } + + public get defName() { + let value = this.simpleName; if (this.typeParameters.length) { const pars = this.typeParameters.join(', '); - this.defName += `<${pars}>`; + value += `<${pars}>`; } + Object.defineProperty(this, 'defName', { configurable: true, value }); + return value; } - get members() { + public get members() { + // TODO: Declare interfaces and remove members which are duplicate with super interfaces. const value = u2a(this.raw.members, (i) => new Member(i, this)).filter((m) => m.visible); Object.defineProperty(this, 'members', { configurable: true, value }); return value; } - get isInterface() { - return !this.raw.superclass; - } - - get superclass() { + public get superclass() { const value = Type.create(u2s(this.raw.superclass), this, TypeUsage.superType); Object.defineProperty(this, 'superclass', { configurable: true, value }); return value; } - get derivativedTypes() { - const { builder } = this; - const innserClassNameSet = new Set(u2a(this.raw.innerClasses, u2s).filter(notEmpty)); - const value = builder - .findDerivativedTypes(this.name) - .filter((n) => !this.isInterface || innserClassNameSet.has(n)) - .map((n) => Type.create(n, this)); - Object.defineProperty(this, 'derivativedTypes', { configurable: true, value }); - return value; + public hasConstructor() { + const { modifiers } = this; + if (modifiers === undefined) return true; + return !Modifier.isAbstract(modifiers) && !Modifier.isInterface(modifiers); } - spread() { - this.superclass.valueOf(); - this.members.valueOf(); - this.derivativedTypes.valueOf(); + public spread() { + const { superclass, members, name, root: builder } = this; + superclass.valueOf(); + members.valueOf(); + // Touch all classes which is extending this class. + for (const [rawClz] of builder.findDerivativedRefs(name)) { + if (rawClz.hasConstructor()) builder.touchDef(rawClz); + } } } diff --git a/packages/builder/src/models/DefBase.ts b/packages/builder/src/models/DefBase.ts index 18a1d1d..75ccb32 100644 --- a/packages/builder/src/models/DefBase.ts +++ b/packages/builder/src/models/DefBase.ts @@ -1,15 +1,22 @@ +import { hideProperty } from '../utils'; import { AnnocatedRaw, Annotated } from './Annotated'; import type { Root } from './Root'; export class DefBase extends Annotated { - public readonly simpleName; - public readonly builder; + public readonly root; public get options() { - return this.builder.options; + return this.root.options; } - constructor(raw: T, builder: Root) { + constructor(raw: T, root: Root) { super(raw); - this.builder = builder; - this.simpleName = builder.takeUniqueName(this.name, this.options.fixClassName); + this.root = root; + hideProperty(this, 'root'); + hideProperty(this, 'root'); + } + + public get simpleName() { + const value = this.root.takeUniqueName(this.name, this.options.fixClassName); + Object.defineProperty(this, 'simpleName', { configurable: true, value }); + return value; } } diff --git a/packages/builder/src/models/Enum.ts b/packages/builder/src/models/Enum.ts index 6d70357..73aedce 100644 --- a/packages/builder/src/models/Enum.ts +++ b/packages/builder/src/models/Enum.ts @@ -1,9 +1,9 @@ import { Dubious } from '../utils'; import { u2a } from 'u2x'; -import type { Root } from './Root'; import { DefBase } from './DefBase'; import { EnumConstant } from './EnumConstant'; import { NadEnum } from '../types/nad'; +import { Root } from './Root'; type EnumRaw = Dubious; @@ -11,8 +11,8 @@ export class Enum extends DefBase { public readonly constants; public readonly valueType; public readonly description; - constructor(raw: EnumRaw, builder: Root) { - super(raw, builder); + constructor(raw: EnumRaw, root: Root) { + super(raw, root); this.constants = u2a(this.raw.constants, (i) => new EnumConstant(i, this)); this.valueType = this.initValueType(); this.description = this.annotations.swagger.getApiModel()?.description; @@ -25,4 +25,8 @@ export class Enum extends DefBase { if (vt !== 'string' && vt !== 'number') vt = 'unknown' as const; return vt; } + + public spread() { + // noop + } } diff --git a/packages/builder/src/models/Root.ts b/packages/builder/src/models/Root.ts index 303974f..804b48e 100644 --- a/packages/builder/src/models/Root.ts +++ b/packages/builder/src/models/Root.ts @@ -1,6 +1,6 @@ import { Module } from './Module'; import { Class } from './Class'; -import { computeIfAbsent, Dubious, toUpperCamel, parseDsv, UniqueName, removeDynamicSuffix, notEmpty } from '../utils'; +import { computeIfAbsent, Dubious, toUpperCamel, parseDsv, UniqueName, removeDynamicSuffix, assert } from '../utils'; import { u2o, u2a, u2s } from 'u2x'; import { Enum } from './Enum'; import { NadResult } from '../types/nad'; @@ -11,13 +11,12 @@ import { CommonDefs } from './CommonDefs'; export type RawDefs = Dubious; export class Root { - private readonly rawClassMap; + private readonly classMap; private readonly derivationMap; - private readonly rawEnumMap; + private readonly enumMap; private readonly rawModuleMap; - private readonly classes: Record; - private readonly enums: Record; + private readonly activeDefs; public readonly options; @@ -30,27 +29,13 @@ export class Root { constructor(raw: RawDefs, options: Partial = {}) { this.options = new RootOptions(options); - const rawClasses = u2a(raw.classes, u2o); - this.rawClassMap = new Map(rawClasses.map((i) => [u2s(u2o(i).name), i])); - - this.derivationMap = rawClasses.reduce((map, clz) => { - const { superclass, interfaces, name } = clz; - if (typeof name !== 'string') return map; - [superclass] - .concat(interfaces) - .map((s) => u2s(s)) - .filter(notEmpty) - .map((n) => n.replace(/\<.*/, '')) - .filter((n) => this.rawClassMap.has(n)) - .forEach((n) => computeIfAbsent(map, n, () => []).push(name)); - return map; - }, new Map()); - - this.rawEnumMap = new Map(u2a(raw.enums, (i) => [u2s(u2o(i).name), i])); + this.enumMap = new Map(u2a(raw.enums, (i) => new Enum(i, this)).map((d) => [d.name, d] as const)); + this.classMap = new Map(u2a(raw.classes, (i) => new Class(i, this)).map((d) => [d.name, d] as const)); + this.derivationMap = this.buildDerivationMap(); this.rawModuleMap = new Map(u2a(raw.modules, (i) => [u2s(u2o(i).name), i])); - this.classes = Object.create(null); - this.enums = Object.create(null); + this.activeDefs = new Set(); + this.commonDefs = new CommonDefs(); this.uniqueNameSeparator = options.uniqueNameSeparator; @@ -77,45 +62,82 @@ export class Root { ); } - get declarationList() { - return Object.values(this.classes); + private buildDerivationMap() { + const { classMap: rawClassMap } = this; + + // For example: + // + // If the classes defining like this: + // ```java + // class Animal {} + // class Dog extends Animal {} + // class Cat extends Animal {} + // ``` + // + // The map result will be: + // + // { 'Animal': { Dog => 'Animal', Cat => 'Animal' } } + // + const map = new Map>(); + for (const [, clz] of rawClassMap) { + for (const n of clz.bounds) { + const sn = n.replace(/\<.*/, ''); + if (!rawClassMap.has(sn)) continue; + const item = computeIfAbsent(map, n, () => new Map()); + item.set(clz, n); + } + } + + return map; + } + + public get classList() { + return [...this.activeDefs].filter((i): i is Class => i instanceof Class); } - get enumList() { - return Object.values(this.enums); + public get enumList() { + return [...this.activeDefs].filter((i): i is Enum => i instanceof Enum); } - get moduleCount() { + public get moduleCount() { return this.modules.length; } - get defCount() { - return Object.keys(this.classes).length; + + public get defCount() { + return this.activeDefs.size; } - get apiCount() { + + public get apiCount() { return this.modules.reduce((s, i) => s + i.routes.length, 0); } - public getDefByName(name: string): Enum | Class | null { - const { classes, rawClassMap: rawClasses, enums, rawEnumMap: rawEnums } = this; - if (name in classes) return classes[name]; - if (name in enums) return enums[name]; - let raw = rawClasses.get(name); - if (raw) { - const clz = new Class(raw, this); - classes[name] = clz; - clz.spread(); - return clz; + /** + * This method can only be called from Type. + */ + public touchDef(name: string): Enum | Class | null; + public touchDef(def: T): T; + public touchDef(what: string | Enum | Class): Enum | Class | null { + if (typeof what === 'string') { + const { classMap, enumMap } = this; + what = classMap.get(what) || enumMap.get(what) || what; } - raw = rawEnums.get(name); - if (raw) { - const en = new Enum(raw, this); - enums[name] = en; - return en; + + if (what instanceof Enum || what instanceof Class) { + const { activeDefs } = this; + if (activeDefs.has(what)) return what; + activeDefs.add(what); + what.spread(); + return what; } - this.unknownTypes.add(name); + + this.unknownTypes.add(what); return null; } + public isUsing(def: Enum | Class) { + return this.activeDefs.has(def); + } + public takeUniqueName(javaClassPath: string, fixFunction: (s: string) => string) { // Concat the java package path before name, while the class name is already in use. const { uniqueNameSeparator } = this; @@ -128,10 +150,11 @@ export class Root { return UniqueName.createFor(this, fixFunction(name), uniqueNameSeparator); } - public findDerivativedTypes(rawTypeName: string) { - // TODO: Support generic type matching. - const list = this.derivationMap.get(rawTypeName); - if (!list) return []; - return list; + public *findDerivativedRefs(name: string, depth = 0): Iterable<[Class, string]> { + assert(depth < 20, 'Too many nested derivative class: ' + name); + for (const [clz, usage] of this.derivationMap.get(name) ?? []) { + yield [clz, usage]; + yield* this.findDerivativedRefs(clz.name, depth + 1); + } } } diff --git a/packages/builder/src/models/Type.ts b/packages/builder/src/models/Type.ts index b9ecb28..f461f22 100644 --- a/packages/builder/src/models/Type.ts +++ b/packages/builder/src/models/Type.ts @@ -7,7 +7,7 @@ import type { Root } from './Root'; export type TypeOwner = Class | Root; const getBuilderFromOwner = (owner: TypeOwner) => { - if ('builder' in owner) return owner.builder; + if ('root' in owner) return owner.root; return owner; }; @@ -44,11 +44,11 @@ export class Type { if (isJavaNonClass(name) || this.isGenericVariable) { this.clz = null; } else { - this.clz = this.builder.getDefByName(name); + this.clz = this.builder.touchDef(name); } } - get owner() { + private get owner() { const owner = wm.get(this); /* istanbul ignore next */ if (!owner) throw neverReachHere(); @@ -113,7 +113,7 @@ export class Type { parameters = []; name = JAVA_STRING; } else { - parameters = [new Type(owner, usage, name, parameters)]; + parameters = [new this(owner, usage, name, parameters)]; name = JAVA_LIST; } } diff --git a/packages/builder/src/tests/codegen/all-types.test.ts b/packages/builder/src/tests/codegen/all-types.test.ts index 260b74d..6ed44b2 100644 --- a/packages/builder/src/tests/codegen/all-types.test.ts +++ b/packages/builder/src/tests/codegen/all-types.test.ts @@ -1,6 +1,6 @@ import { buildOcMethodWithParameters, buildTsMethodWithParameters } from '../test-tools/buildMethodWithParameters'; -import { DeepPartial } from 'src/utils'; -import { NadParameter } from 'src/types/nad'; +import { DeepPartial } from '../../utils'; +import { NadParameter } from '../../types/nad'; const typeVector = describe.each([ ['', 'unknown', 'NSObject*'], diff --git a/packages/builder/src/tests/codegen/oc-types.test.ts b/packages/builder/src/tests/codegen/oc-types.test.ts index aa72879..4478cc0 100644 --- a/packages/builder/src/tests/codegen/oc-types.test.ts +++ b/packages/builder/src/tests/codegen/oc-types.test.ts @@ -98,3 +98,36 @@ test('preserved keyword property name', () => { @end `); }); + +test('chaining extending', () => { + const routes: DeepPartial[] = [{ bean: 'test.Foo', name: 'foo', returnType: 'test.A' }]; + const classes: DeepPartial[] = [ + { name: 'test.A', superclass: 'test.B' }, + { name: 'test.C' }, + { name: 'test.B', superclass: 'test.C' }, + ]; + const code = new Builder({ target: 'oc', base: '', defs: { routes, classes } }).code; + + expect(code).toMatchCode(` + /** + * C + * @JavaClass test.C + */ + @interface C : NSObject + @end + + /** + * B + * @JavaClass test.B + */ + @interface B : C + @end + + /** + * A + * @JavaClass test.A + */ + @interface A : B + @end + `); +}); diff --git a/packages/builder/src/tests/codegen/ts-types.test.ts b/packages/builder/src/tests/codegen/ts-types.test.ts index 47dd84e..0f03a69 100644 --- a/packages/builder/src/tests/codegen/ts-types.test.ts +++ b/packages/builder/src/tests/codegen/ts-types.test.ts @@ -1,7 +1,7 @@ import { Builder } from '../../Builder'; import { NadClass, NadEnum, NadRoute } from '../../types/nad'; import { paginitionDefs } from '../defs/paginitionTestDefs'; -import { DeepPartial } from '../../utils'; +import { DeepPartial, Modifier } from '../../utils'; test('Paginition', () => { const code = new Builder({ target: 'ts', base: '', defs: paginitionDefs }).code; @@ -52,12 +52,12 @@ test('empty bean', () => { test('union type', () => { const classes: DeepPartial[] = [ - { name: 'test.U', members: [], innerClasses: ['test.U$A', 'test.U$B'] }, + { name: 'test.U', members: [], innerClasses: ['test.U$A', 'test.U$B'], modifiers: Modifier.INTERFACE }, { name: 'test.U$A', superclass: 'java.lang.Object', members: [], interfaces: ['test.U'] }, { name: 'test.U$B', superclass: 'java.lang.Object', members: [], interfaces: ['test.U'] }, - { name: 'test.M', members: [], innerClasses: ['test.M$A'] }, - { name: 'test.M$A', superclass: 'java.lang.Object', members: [], interfaces: ['test.M'] }, + { name: 'test.M', members: [], innerClasses: ['test.M$A'], modifiers: Modifier.INTERFACE }, + { name: 'test.M$A', members: [], interfaces: ['test.M'], modifiers: Modifier.INTERFACE }, { name: 'test.G', members: [], innerClasses: ['test.G$A'], superclass: 'java.lang.Object' }, { name: 'test.G$A', superclass: 'test.G', members: [] }, @@ -93,8 +93,8 @@ test('union type', () => { /** * foo3 */ - async foo3(a?: Array, settings?: Partial) { - return new Runtime() + async foo3(a?: Array, settings?: Partial) { + return new Runtime() .open('POST', '', settings) .addRequestParam('a', a) .execute(); diff --git a/packages/builder/src/tests/models/Class.test.ts b/packages/builder/src/tests/models/Class.test.ts index 4caf1b4..989bd70 100644 --- a/packages/builder/src/tests/models/Class.test.ts +++ b/packages/builder/src/tests/models/Class.test.ts @@ -1,5 +1,4 @@ -import { isJavaUnknown } from 'src/helpers/javaHelper'; -import { Class, Member, Root, Type } from '../../models'; +import { Member, Root, Type, Class } from '../../models'; const root = new Root({}); @@ -54,6 +53,7 @@ test('bad class name', () => { test('bad member name', () => { const clz = new Class( { + name: expect.getState().currentTestName, members: [{ name: '' }, { name: undefined }], }, root, @@ -65,6 +65,7 @@ test('bad member name', () => { test('hidden members', () => { const clz = new Class( { + name: expect.getState().currentTestName, members: [ { name: '' }, { name: '~!@#' }, @@ -99,6 +100,7 @@ test('hidden members', () => { test('JsonNaming SnakeCaseStrategy', () => { const clz = new Class( { + name: expect.getState().currentTestName, annotations: [ { type: 'com.fasterxml.jackson.databind.annotation.JsonNaming', diff --git a/packages/builder/src/tests/models/Enum.test.ts b/packages/builder/src/tests/models/Enum.test.ts index 1d0b64f..687b85c 100644 --- a/packages/builder/src/tests/models/Enum.test.ts +++ b/packages/builder/src/tests/models/Enum.test.ts @@ -5,7 +5,7 @@ const root = new Root({}); test('number value', () => { const obj = new Enum( { - name: 'test.MyEnum', + name: expect.getState().currentTestName, constants: [ { name: 'One', value: 1 }, { name: 'Two', value: 2 }, @@ -36,7 +36,7 @@ test('number value', () => { test('string value', () => { const obj = new Enum( { - name: 'test.MyEnum', + name: expect.getState().currentTestName, constants: [ { name: 'One', value: '1' }, { name: 'Two', value: '2' }, @@ -67,7 +67,7 @@ test('string value', () => { test('mixed value', () => { const obj = new Enum( { - name: 'test.MyEnum', + name: expect.getState().currentTestName, constants: [ { name: 'One', value: '1' }, { name: 'Two', value: 2 }, @@ -98,7 +98,7 @@ test('mixed value', () => { test('description', () => { const obj = new Enum( { - name: 'test.MyEnum', + name: expect.getState().currentTestName, annotations: [{ type: 'io.swagger.annotations.ApiModel', attributes: { description: 'hehe' } }], constants: [], }, diff --git a/packages/builder/src/tests/models/Root.test.ts b/packages/builder/src/tests/models/Root.test.ts index 4252da2..2e25c72 100644 --- a/packages/builder/src/tests/models/Root.test.ts +++ b/packages/builder/src/tests/models/Root.test.ts @@ -69,19 +69,19 @@ test('defs', () => { const getDefBySimpleName = (root: Root, name: string): Enum | Class | null => { return ( - root.declarationList.find((def) => def.simpleName === name) || + root.classList.find((def) => def.simpleName === name) || root.enumList.find((def) => def.simpleName === name) || null ); }; - const user = root.getDefByName('test.User'); - expect(root.getDefByName('test.User')).toBe(user); // from cache + const user = root.touchDef('test.User'); + expect(root.touchDef('test.User')).toBe(user); // from cache expect(user).toBeInstanceOf(Class); if (user) expect(getDefBySimpleName(root, user.simpleName)).toBe(user); - const userType = root.getDefByName('test.UserType'); - expect(root.getDefByName('test.UserType')).toBe(userType); // from cache + const userType = root.touchDef('test.UserType'); + expect(root.touchDef('test.UserType')).toBe(userType); // from cache expect(userType).toBeInstanceOf(Enum); if (userType) expect(getDefBySimpleName(root, userType.simpleName)).toBe(userType); @@ -102,5 +102,5 @@ test('typeMapping', () => { name: 'java.util.Map', parameters: [{ name: 'java.lang.Long' }, { name: 'java.lang.String' }], }); - expect(root.declarationList).toHaveLength(0); + expect(root.classList).toHaveLength(0); }); diff --git a/packages/builder/src/types/nad.ts b/packages/builder/src/types/nad.ts index 4ac36a3..7a19470 100644 --- a/packages/builder/src/types/nad.ts +++ b/packages/builder/src/types/nad.ts @@ -20,6 +20,7 @@ export interface NadClass extends NadDef { typeParameters: string[]; innerClasses: string[]; importantMethods: NadMethod[]; + modifiers: number; } /** @@ -81,7 +82,9 @@ export type NadModule = NadDef; */ export interface NadMethod extends NadDef { parameters: NadParameter[]; + typeParameters: NadParameter[]; returnType: string; + modifiers: number; } /** diff --git a/packages/builder/src/utils/Modifiers.ts b/packages/builder/src/utils/Modifiers.ts new file mode 100644 index 0000000..b20de90 --- /dev/null +++ b/packages/builder/src/utils/Modifiers.ts @@ -0,0 +1,395 @@ +/* istanbul ignore file */ +/** + * From java.lang.reflect.Modifier + */ +export abstract class Modifier { + /** + * Return {@code true} if the integer argument includes the + * {@code public} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code public} modifier; {@code false} otherwise. + */ + public static isPublic(mod: number): boolean { + return (mod & Modifier.PUBLIC) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code private} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code private} modifier; {@code false} otherwise. + */ + public static isPrivate(mod: number): boolean { + return (mod & Modifier.PRIVATE) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code protected} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code protected} modifier; {@code false} otherwise. + */ + public static isProtected(mod: number): boolean { + return (mod & Modifier.PROTECTED) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code static} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code static} modifier; {@code false} otherwise. + */ + public static isStatic(mod: number): boolean { + return (mod & Modifier.STATIC) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code final} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code final} modifier; {@code false} otherwise. + */ + public static isFinal(mod: number): boolean { + return (mod & Modifier.FINAL) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code synchronized} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code synchronized} modifier; {@code false} otherwise. + */ + public static isSynchronized(mod: number): boolean { + return (mod & Modifier.SYNCHRONIZED) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code volatile} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code volatile} modifier; {@code false} otherwise. + */ + public static isVolatile(mod: number): boolean { + return (mod & Modifier.VOLATILE) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code transient} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code transient} modifier; {@code false} otherwise. + */ + public static isTransient(mod: number): boolean { + return (mod & Modifier.TRANSIENT) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code native} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code native} modifier; {@code false} otherwise. + */ + public static isNative(mod: number): boolean { + return (mod & Modifier.NATIVE) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code interface} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code interface} modifier; {@code false} otherwise. + */ + public static isInterface(mod: number): boolean { + return (mod & Modifier.INTERFACE) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code abstract} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code abstract} modifier; {@code false} otherwise. + */ + public static isAbstract(mod: number): boolean { + return (mod & Modifier.ABSTRACT) !== 0; + } + + /** + * Return {@code true} if the integer argument includes the + * {@code strictfp} modifier, {@code false} otherwise. + * + * @param mod a set of modifiers + * @return {@code true} if {@code mod} includes the + * {@code strictfp} modifier; {@code false} otherwise. + */ + public static isStrict(mod: number): boolean { + return (mod & Modifier.STRICT) !== 0; + } + + /* + * Access modifier flag constants from tables 4.1, 4.4, 4.5, and 4.7 of + * The Java Virtual Machine Specification + */ + + /** + * The {@code int} value representing the {@code public} + * modifier. + */ + public static PUBLIC = 0x00000001; + + /** + * The {@code int} value representing the {@code private} + * modifier. + */ + public static PRIVATE = 0x00000002; + + /** + * The {@code int} value representing the {@code protected} + * modifier. + */ + public static PROTECTED = 0x00000004; + + /** + * The {@code int} value representing the {@code static} + * modifier. + */ + public static STATIC = 0x00000008; + + /** + * The {@code int} value representing the {@code final} + * modifier. + */ + public static FINAL = 0x00000010; + + /** + * The {@code int} value representing the {@code synchronized} + * modifier. + */ + public static SYNCHRONIZED = 0x00000020; + + /** + * The {@code int} value representing the {@code volatile} + * modifier. + */ + public static VOLATILE = 0x00000040; + + /** + * The {@code int} value representing the {@code transient} + * modifier. + */ + public static TRANSIENT = 0x00000080; + + /** + * The {@code int} value representing the {@code native} + * modifier. + */ + public static NATIVE = 0x00000100; + + /** + * The {@code int} value representing the {@code interface} + * modifier. + */ + public static INTERFACE = 0x00000200; + + /** + * The {@code int} value representing the {@code abstract} + * modifier. + */ + public static ABSTRACT = 0x00000400; + + /** + * The {@code int} value representing the {@code strictfp} + * modifier. + */ + public static STRICT = 0x00000800; + + // Bits not (yet) exposed in the public API either because they + // have different meanings for fields and methods and there is no + // way to distinguish between the two in this class, or because + // they are not Java programming language keywords + static BRIDGE = 0x00000040; + static VARARGS = 0x00000080; + static SYNTHETIC = 0x00001000; + static ANNOTATION = 0x00002000; + static ENUM = 0x00004000; + static MANDATED = 0x00008000; + + static isSynthetic(mod: number): boolean { + return (mod & Modifier.SYNTHETIC) !== 0; + } + + static isMandated(mod: number): boolean { + return (mod & Modifier.MANDATED) !== 0; + } + + // Note on the FOO_MODIFIERS fields and fooModifiers() methods: + // the sets of modifiers are not guaranteed to be constants + // across time and Java SE releases. Therefore, it would not be + // appropriate to expose an external interface to this information + // that would allow the values to be treated as Java-level + // constants since the values could be constant folded and updates + // to the sets of modifiers missed. Thus, the fooModifiers() + // methods return an unchanging values for a given release, but a + // value that can potentially change over time. + + /** + * The Java source modifiers that can be applied to a class. + * @jls 8.1.1 Class Modifiers + */ + private static CLASS_MODIFIERS = + Modifier.PUBLIC | + Modifier.PROTECTED | + Modifier.PRIVATE | + Modifier.ABSTRACT | + Modifier.STATIC | + Modifier.FINAL | + Modifier.STRICT; + + /** + * The Java source modifiers that can be applied to an interface. + * @jls 9.1.1 Interface Modifiers + */ + private static INTERFACE_MODIFIERS = + Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE | Modifier.ABSTRACT | Modifier.STATIC | Modifier.STRICT; + + /** + * The Java source modifiers that can be applied to a constructor. + * @jls 8.8.3 Constructor Modifiers + */ + private static CONSTRUCTOR_MODIFIERS = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE; + + /** + * The Java source modifiers that can be applied to a method. + * @jls 8.4.3 Method Modifiers + */ + private static METHOD_MODIFIERS = + Modifier.PUBLIC | + Modifier.PROTECTED | + Modifier.PRIVATE | + Modifier.ABSTRACT | + Modifier.STATIC | + Modifier.FINAL | + Modifier.SYNCHRONIZED | + Modifier.NATIVE | + Modifier.STRICT; + + /** + * The Java source modifiers that can be applied to a field. + * @jls 8.3.1 Field Modifiers + */ + private static FIELD_MODIFIERS = + Modifier.PUBLIC | + Modifier.PROTECTED | + Modifier.PRIVATE | + Modifier.STATIC | + Modifier.FINAL | + Modifier.TRANSIENT | + Modifier.VOLATILE; + + /** + * The Java source modifiers that can be applied to a method or constructor parameter. + * @jls 8.4.1 Formal Parameters + */ + private static PARAMETER_MODIFIERS = Modifier.FINAL; + + static ACCESS_MODIFIERS = Modifier.PUBLIC | Modifier.PROTECTED | Modifier.PRIVATE; + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a class. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a class. + * + * @jls 8.1.1 Class Modifiers + * @since 1.7 + */ + public static classModifiers() { + return Modifier.CLASS_MODIFIERS; + } + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to an interface. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to an interface. + * + * @jls 9.1.1 Interface Modifiers + * @since 1.7 + */ + public static interfaceModifiers() { + return Modifier.INTERFACE_MODIFIERS; + } + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a constructor. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a constructor. + * + * @jls 8.8.3 Constructor Modifiers + * @since 1.7 + */ + public static constructorModifiers() { + return Modifier.CONSTRUCTOR_MODIFIERS; + } + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a method. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a method. + * + * @jls 8.4.3 Method Modifiers + * @since 1.7 + */ + public static methodModifiers() { + return Modifier.METHOD_MODIFIERS; + } + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a field. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a field. + * + * @jls 8.3.1 Field Modifiers + * @since 1.7 + */ + public static fieldModifiers() { + return Modifier.FIELD_MODIFIERS; + } + + /** + * Return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a parameter. + * @return an {@code int} value OR-ing together the source language + * modifiers that can be applied to a parameter. + * + * @jls 8.4.1 Formal Parameters + * @since 1.8 + */ + public static parameterModifiers() { + return Modifier.PARAMETER_MODIFIERS; + } +} diff --git a/packages/builder/src/utils/index.ts b/packages/builder/src/utils/index.ts index 7885bf1..c6bc361 100644 --- a/packages/builder/src/utils/index.ts +++ b/packages/builder/src/utils/index.ts @@ -3,6 +3,7 @@ export * from './UniqueName'; export * from './computeIfAbsent'; export * from './neverReachHere'; export * from './parseDsv'; +export * from './Modifiers'; export const notEmpty = (w: T): w is NonNullable => w !== null && w !== undefined; @@ -61,3 +62,18 @@ export type DeepPartial = T extends (infer U)[] [P in keyof T]?: DeepPartial; } : T; + +export function assert( + value: T, + message: string = 'The value must be true here', +): asserts value is Exclude { + /* istanbul ignore next */ + if (!value) throw new Error(message); +} + +export const hideProperty = (object: object, propertyName: string) => { + const desc = Object.getOwnPropertyDescriptor(object, propertyName); + assert(desc); + desc.enumerable = false; + Object.defineProperty(object, propertyName, desc); +};