From 21e5e20313a1d20e4c290c90d1a8642cbaadca5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BB=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier?= Date: Mon, 17 Jul 2023 12:15:53 +0200 Subject: [PATCH 1/6] feat: leverage runtime type information in jsii runtime Instead of eagerly loading each type from the assemblies in order to annotate the constructos with jsii fully qualified type names (FQNs), leverage the `jsii.rtti` data that is injected by the `jsii` compiler since release `1.19.0`. --- packages/@jsii/kernel/src/kernel.ts | 451 ++++++++++-------- packages/@jsii/kernel/src/objects.ts | 120 +++-- .../@jsii/kernel/src/serialization.test.ts | 4 + packages/@jsii/kernel/src/serialization.ts | 260 ++++++---- .../@jsii/kernel/src/serialization.ux.test.ts | 4 + .../jsii-calc-lib/lib/deprecation-removal.ts | 19 +- .../@scope/jsii-calc-lib/test/assembly.jsii | 50 +- .../__snapshots__/target-dotnet.test.js.snap | 61 ++- .../__snapshots__/target-go.test.js.snap | 72 ++- .../__snapshots__/target-java.test.js.snap | 43 +- .../__snapshots__/target-python.test.js.snap | 24 + .../test/__snapshots__/jsii-tree.test.js.snap | 12 + .../test/__snapshots__/tree.test.js.snap | 7 + .../__snapshots__/type-system.test.js.snap | 1 + 14 files changed, 768 insertions(+), 360 deletions(-) diff --git a/packages/@jsii/kernel/src/kernel.ts b/packages/@jsii/kernel/src/kernel.ts index db723f121e..4223f3e9f3 100644 --- a/packages/@jsii/kernel/src/kernel.ts +++ b/packages/@jsii/kernel/src/kernel.ts @@ -45,16 +45,16 @@ export class Kernel { */ public debugTimingEnabled = false; - private readonly assemblies = new Map(); - private readonly objects = new ObjectTable(this._typeInfoForFqn.bind(this)); - private readonly cbs = new Map(); - private readonly waiting = new Map(); - private readonly promises = new Map(); - private nextid = 20000; // incrementing counter for objid, cbid, promiseid - private syncInProgress?: string; // forbids async calls (begin) while processing sync calls (get/set/invoke) - private installDir?: string; + readonly #assemblies = new Map(); + readonly #objects = new ObjectTable(this.#typeInfoForFqn.bind(this)); + readonly #cbs = new Map(); + readonly #waiting = new Map(); + readonly #promises = new Map(); + #nextid = 20000; // incrementing counter for objid, cbid, promiseid + #syncInProgress?: string; // forbids async calls (begin) while processing sync calls (get/set/invoke) + #installDir?: string; /** The internal require function, used instead of the global "require" so that webpack does not transform it... */ - private require?: typeof require; + #require?: typeof require; /** * Creates a jsii kernel object. @@ -66,14 +66,14 @@ export class Kernel { public constructor(public callbackHandler: (callback: api.Callback) => any) {} public load(req: api.LoadRequest): api.LoadResponse { - return this._debugTime( - () => this._load(req), + return this.#debugTime( + () => this.#load(req), `load(${JSON.stringify(req, null, 2)})`, ); } - private _load(req: api.LoadRequest): api.LoadResponse { - this._debug('load', req); + #load(req: api.LoadRequest): api.LoadResponse { + this.#debug('load', req); if ('assembly' in req) { throw new JsiiFault( @@ -85,7 +85,7 @@ export class Kernel { const pkgver = req.version; // check if we already have such a module - const packageDir = this._getPackageDir(pkgname); + const packageDir = this.#getPackageDir(pkgname); if (fs.pathExistsSync(packageDir)) { // module exists, verify version const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); @@ -98,8 +98,8 @@ export class Kernel { } // same version, no-op - this._debug('look up already-loaded assembly', pkgname); - const assm = this.assemblies.get(pkgname)!; + this.#debug('look up already-loaded assembly', pkgname); + const assm = this.#assemblies.get(pkgname)!; return { assembly: assm.metadata.name, @@ -111,7 +111,7 @@ export class Kernel { const originalUmask = process.umask(0o022); try { // untar the archive to its final location - const { cache } = this._debugTime( + const { cache } = this.#debugTime( () => tar.extract( req.tarball, @@ -128,7 +128,7 @@ export class Kernel { ); if (cache != null) { - this._debug( + this.#debug( `Package cache enabled, extraction resulted in a cache ${cache}`, ); } @@ -140,7 +140,7 @@ export class Kernel { // read .jsii metadata from the root of the package let assmSpec; try { - assmSpec = this._debugTime( + assmSpec = this.#debugTime( () => loadAssemblyFromPath(packageDir), `loadAssemblyFromPath(${packageDir})`, ); @@ -151,17 +151,12 @@ export class Kernel { } // load the module and capture its closure - const closure = this._debugTime( - () => this.require!(packageDir), + const closure = this.#debugTime( + () => this.#require!(packageDir), `require(${packageDir})`, ); const assm = new Assembly(assmSpec, closure); - this._debugTime( - () => this._addAssembly(assm), - `registerAssembly({ name: ${assm.metadata.name}, types: ${ - Object.keys(assm.metadata.types ?? {}).length - } })`, - ); + this.#addAssembly(assm); return { assembly: assmSpec.name, @@ -172,13 +167,13 @@ export class Kernel { public getBinScriptCommand( req: api.GetScriptCommandRequest, ): api.GetScriptCommandResponse { - return this._getBinScriptCommand(req); + return this.#getBinScriptCommand(req); } public invokeBinScript( req: api.InvokeScriptRequest, ): api.InvokeScriptResponse { - const { command, args, env } = this._getBinScriptCommand(req); + const { command, args, env } = this.#getBinScriptCommand(req); const result = cp.spawnSync(command, args, { encoding: 'utf-8', @@ -195,14 +190,14 @@ export class Kernel { } public create(req: api.CreateRequest): api.CreateResponse { - return this._create(req); + return this.#create(req); } public del(req: api.DelRequest): api.DelResponse { const { objref } = req; - this._debug('del', objref); - this.objects.deleteObject(objref); + this.#debug('del', objref); + this.#objects.deleteObject(objref); return {}; } @@ -210,31 +205,31 @@ export class Kernel { public sget(req: api.StaticGetRequest): api.GetResponse { const { fqn, property } = req; const symbol = `${fqn}.${property}`; - this._debug('sget', symbol); - const ti = this._typeInfoForProperty(property, fqn); + this.#debug('sget', symbol); + const ti = this.#typeInfoForProperty(property, fqn); if (!ti.static) { throw new JsiiFault(`property ${symbol} is not static`); } - const prototype = this._findSymbol(fqn); + const prototype = this.#findSymbol(fqn); - const value = this._ensureSync( + const value = this.#ensureSync( `property ${property}`, () => prototype[property], ); - this._debug('value:', value); - const ret = this._fromSandbox(value, ti, `of static property ${symbol}`); - this._debug('ret', ret); + this.#debug('value:', value); + const ret = this.#fromSandbox(value, ti, `of static property ${symbol}`); + this.#debug('ret', ret); return { value: ret }; } public sset(req: api.StaticSetRequest): api.SetResponse { const { fqn, property, value } = req; const symbol = `${fqn}.${property}`; - this._debug('sset', symbol); - const ti = this._typeInfoForProperty(property, fqn); + this.#debug('sset', symbol); + const ti = this.#typeInfoForProperty(property, fqn); if (!ti.static) { throw new JsiiFault(`property ${symbol} is not static`); @@ -244,12 +239,12 @@ export class Kernel { throw new JsiiFault(`static property ${symbol} is readonly`); } - const prototype = this._findSymbol(fqn); + const prototype = this.#findSymbol(fqn); - this._ensureSync( + this.#ensureSync( `property ${property}`, () => - (prototype[property] = this._toSandbox( + (prototype[property] = this.#toSandbox( value, ti, `assigned to static property ${symbol}`, @@ -261,35 +256,35 @@ export class Kernel { public get(req: api.GetRequest): api.GetResponse { const { objref, property } = req; - this._debug('get', objref, property); - const { instance, fqn, interfaces } = this.objects.findObject(objref); - const ti = this._typeInfoForProperty(property, fqn, interfaces); + this.#debug('get', objref, property); + const { instance, fqn, interfaces } = this.#objects.findObject(objref); + const ti = this.#typeInfoForProperty(property, fqn, interfaces); // if the property is overridden by the native code and "get" is called on the object, it // means that the native code is trying to access the "super" property. in order to enable // that, we actually keep a copy of the original property descriptor when we override, // so `findPropertyTarget` will return either the original property name ("property") or // the "super" property name (somehing like "$jsii$super$$"). - const propertyToGet = this._findPropertyTarget(instance, property); + const propertyToGet = this.#findPropertyTarget(instance, property); // make the actual "get", and block any async calls that might be performed // by jsii overrides. - const value = this._ensureSync( + const value = this.#ensureSync( `property '${objref[TOKEN_REF]}.${propertyToGet}'`, () => instance[propertyToGet], ); - this._debug('value:', value); - const ret = this._fromSandbox(value, ti, `of property ${fqn}.${property}`); - this._debug('ret:', ret); + this.#debug('value:', value); + const ret = this.#fromSandbox(value, ti, `of property ${fqn}.${property}`); + this.#debug('ret:', ret); return { value: ret }; } public set(req: api.SetRequest): api.SetResponse { const { objref, property, value } = req; - this._debug('set', objref, property, value); - const { instance, fqn, interfaces } = this.objects.findObject(objref); + this.#debug('set', objref, property, value); + const { instance, fqn, interfaces } = this.#objects.findObject(objref); - const propInfo = this._typeInfoForProperty(req.property, fqn, interfaces); + const propInfo = this.#typeInfoForProperty(req.property, fqn, interfaces); if (propInfo.immutable) { throw new JsiiFault( @@ -297,12 +292,12 @@ export class Kernel { ); } - const propertyToSet = this._findPropertyTarget(instance, property); + const propertyToSet = this.#findPropertyTarget(instance, property); - this._ensureSync( + this.#ensureSync( `property '${objref[TOKEN_REF]}.${propertyToSet}'`, () => - (instance[propertyToSet] = this._toSandbox( + (instance[propertyToSet] = this.#toSandbox( value, propInfo, `assigned to property ${fqn}.${property}`, @@ -316,21 +311,21 @@ export class Kernel { const { objref, method } = req; const args = req.args ?? []; - this._debug('invoke', objref, method, args); - const { ti, obj, fn } = this._findInvokeTarget(objref, method, args); + this.#debug('invoke', objref, method, args); + const { ti, obj, fn } = this.#findInvokeTarget(objref, method, args); // verify this is not an async method if (ti.async) { throw new JsiiFault(`${method} is an async method, use "begin" instead`); } - const fqn = jsiiTypeFqn(obj); - const ret = this._ensureSync( + const fqn = jsiiTypeFqn(obj, this.#isVisibleType.bind(this)); + const ret = this.#ensureSync( `method '${objref[TOKEN_REF]}.${method}'`, () => { return fn.apply( obj, - this._toSandboxValues( + this.#toSandboxValues( args, `method ${fqn ? `${fqn}#` : ''}${method}`, ti.parameters, @@ -339,12 +334,12 @@ export class Kernel { }, ); - const result = this._fromSandbox( + const result = this.#fromSandbox( ret, ti.returns ?? 'void', `returned by method ${fqn ? `${fqn}#` : ''}${method}`, ); - this._debug('invoke result', result); + this.#debug('invoke result', result); return { result }; } @@ -353,9 +348,9 @@ export class Kernel { const { fqn, method } = req; const args = req.args ?? []; - this._debug('sinvoke', fqn, method, args); + this.#debug('sinvoke', fqn, method, args); - const ti = this._typeInfoForMethod(method, fqn); + const ti = this.#typeInfoForMethod(method, fqn); if (!ti.static) { throw new JsiiFault(`${fqn}.${method} is not a static method`); @@ -366,13 +361,13 @@ export class Kernel { throw new JsiiFault(`${method} is an async method, use "begin" instead`); } - const prototype = this._findSymbol(fqn); + const prototype = this.#findSymbol(fqn); const fn = prototype[method] as (...params: any[]) => any; - const ret = this._ensureSync(`method '${fqn}.${method}'`, () => { + const ret = this.#ensureSync(`method '${fqn}.${method}'`, () => { return fn.apply( prototype, - this._toSandboxValues( + this.#toSandboxValues( args, `static method ${fqn}.${method}`, ti.parameters, @@ -380,9 +375,9 @@ export class Kernel { ); }); - this._debug('method returned:', ret); + this.#debug('method returned:', ret); return { - result: this._fromSandbox( + result: this.#fromSandbox( ret, ti.returns ?? 'void', `returned by static method ${fqn}.${method}`, @@ -394,26 +389,28 @@ export class Kernel { const { objref, method } = req; const args = req.args ?? []; - this._debug('begin', objref, method, args); + this.#debug('begin', objref, method, args); - if (this.syncInProgress) { + if (this.#syncInProgress) { throw new JsiiFault( - `Cannot invoke async method '${req.objref[TOKEN_REF]}.${req.method}' while sync ${this.syncInProgress} is being processed`, + `Cannot invoke async method '${req.objref[TOKEN_REF]}.${ + req.method + }' while sync ${this.#syncInProgress} is being processed`, ); } - const { ti, obj, fn } = this._findInvokeTarget(objref, method, args); + const { ti, obj, fn } = this.#findInvokeTarget(objref, method, args); // verify this is indeed an async method if (!ti.async) { throw new JsiiFault(`Method ${method} is expected to be an async method`); } - const fqn = jsiiTypeFqn(obj); + const fqn = jsiiTypeFqn(obj, this.#isVisibleType.bind(this)); const promise = fn.apply( obj, - this._toSandboxValues( + this.#toSandboxValues( args, `async method ${fqn ? `${fqn}#` : ''}${method}`, ti.parameters, @@ -425,8 +422,8 @@ export class Kernel { // [1]: https://stackoverflow.com/questions/40920179/should-i-refrain-from-handling-promise-rejection-asynchronously/40921505 promise.catch((_) => undefined); - const prid = this._makeprid(); - this.promises.set(prid, { + const prid = this.#makeprid(); + this.#promises.set(prid, { promise, method: ti, }); @@ -437,9 +434,9 @@ export class Kernel { public async end(req: api.EndRequest): Promise { const { promiseid } = req; - this._debug('end', promiseid); + this.#debug('end', promiseid); - const storedPromise = this.promises.get(promiseid); + const storedPromise = this.#promises.get(promiseid); if (storedPromise == null) { throw new JsiiFault(`Cannot find promise with ID: ${promiseid}`); } @@ -448,9 +445,9 @@ export class Kernel { let result; try { result = await promise; - this._debug('promise result:', result); + this.#debug('promise result:', result); } catch (e: any) { - this._debug('promise error:', e); + this.#debug('promise error:', e); if (e.name === JsiiErrorType.JSII_FAULT) { if (e instanceof JsiiFault) { throw e; @@ -467,7 +464,7 @@ export class Kernel { } return { - result: this._fromSandbox( + result: this.#fromSandbox( result, method.returns ?? 'void', `returned by async method ${method.name}`, @@ -476,10 +473,10 @@ export class Kernel { } public callbacks(_req?: api.CallbacksRequest): api.CallbacksResponse { - this._debug('callbacks'); - const ret = Array.from(this.cbs.entries()).map(([cbid, cb]) => { - this.waiting.set(cbid, cb); // move to waiting - this.cbs.delete(cbid); // remove from created + this.#debug('callbacks'); + const ret = Array.from(this.#cbs.entries()).map(([cbid, cb]) => { + this.#waiting.set(cbid, cb); // move to waiting + this.#cbs.delete(cbid); // remove from created const callback: api.Callback = { cbid, cookie: cb.override.cookie, @@ -498,31 +495,31 @@ export class Kernel { public complete(req: api.CompleteRequest): api.CompleteResponse { const { cbid, err, result, name } = req; - this._debug('complete', cbid, err, result); + this.#debug('complete', cbid, err, result); - const cb = this.waiting.get(cbid); + const cb = this.#waiting.get(cbid); if (!cb) { throw new JsiiFault(`Callback ${cbid} not found`); } if (err) { - this._debug('completed with error:', err); + this.#debug('completed with error:', err); cb.fail( name === JsiiErrorType.JSII_FAULT ? new JsiiFault(err) : new RuntimeError(err), ); } else { - const sandoxResult = this._toSandbox( + const sandoxResult = this.#toSandbox( result, cb.expectedReturnType ?? 'void', `returned by callback ${cb.toString()}`, ); - this._debug('completed with result:', sandoxResult); + this.#debug('completed with result:', sandoxResult); cb.succeed(sandoxResult); } - this.waiting.delete(cbid); + this.#waiting.delete(cbid); return { cbid }; } @@ -534,9 +531,9 @@ export class Kernel { public naming(req: api.NamingRequest): api.NamingResponse { const assemblyName = req.assembly; - this._debug('naming', assemblyName); + this.#debug('naming', assemblyName); - const assembly = this._assemblyFor(assemblyName); + const assembly = this.#assemblyFor(assemblyName); const targets = assembly.metadata.targets; if (!targets) { throw new JsiiFault( @@ -549,12 +546,27 @@ export class Kernel { public stats(_req?: api.StatsRequest): api.StatsResponse { return { - objectCount: this.objects.count, + objectCount: this.#objects.count, }; } - private _addAssembly(assm: Assembly) { - this.assemblies.set(assm.metadata.name, assm); + #addAssembly(assm: Assembly) { + this.#assemblies.set(assm.metadata.name, assm); + + // We can use jsii runtime type information from jsii 1.19.0 onwards... Note that a version of + // 0.0.0 means we are assessing against a development tree, which is newer... + const jsiiVersion = assm.metadata.jsiiVersion.split(' ', 1)[0]; + const [jsiiMajor, jsiiMinor, _jsiiPatch, ..._rest] = jsiiVersion + .split('.') + .map((str) => parseInt(str, 10)); + if ( + jsiiVersion === '0.0.0' || + jsiiMajor > 1 || + (jsiiMajor === 1 && jsiiMinor >= 19) + ) { + this.#debug('Using compiler-woven runtime type information!'); + return; + } // add the __jsii__.fqn property on every constructor. this allows // traversing between the javascript and jsii worlds given any object. @@ -565,14 +577,14 @@ export class Kernel { continue; // interfaces don't really exist case spec.TypeKind.Class: case spec.TypeKind.Enum: - const constructor = this._findSymbol(fqn); + const constructor = this.#findSymbol(fqn); tagJsiiConstructor(constructor, fqn); } } } // find the javascript constructor function for a jsii FQN. - private _findCtor( + #findCtor( fqn: string, args: any[], ): { ctor: any; parameters?: spec.Parameter[] } { @@ -580,14 +592,14 @@ export class Kernel { return { ctor: Object }; } - const typeinfo = this._typeInfoForFqn(fqn); + const typeinfo = this.#typeInfoForFqn(fqn); switch (typeinfo.kind) { case spec.TypeKind.Class: const classType = typeinfo as spec.ClassType; - this._validateMethodArguments(classType.initializer, args); + this.#validateMethodArguments(classType.initializer, args); return { - ctor: this._findSymbol(fqn), + ctor: this.#findSymbol(fqn), parameters: classType.initializer && classType.initializer.parameters, }; @@ -601,42 +613,42 @@ export class Kernel { } } - private _getPackageDir(pkgname: string): string { - if (!this.installDir) { - this.installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii-kernel-')); - this.require = createRequire(this.installDir); - fs.mkdirpSync(path.join(this.installDir, 'node_modules')); - this._debug('creating jsii-kernel modules workdir:', this.installDir); + #getPackageDir(pkgname: string): string { + if (!this.#installDir) { + this.#installDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsii-kernel-')); + this.#require = createRequire(this.#installDir); + fs.mkdirpSync(path.join(this.#installDir, 'node_modules')); + this.#debug('creating jsii-kernel modules workdir:', this.#installDir); - onExit.removeSync(this.installDir); + onExit.removeSync(this.#installDir); } - return path.join(this.installDir, 'node_modules', pkgname); + return path.join(this.#installDir, 'node_modules', pkgname); } // prefixed with _ to allow calling this method internally without // getting it recorded for testing. - private _create(req: api.CreateRequest): api.CreateResponse { - this._debug('create', req); + #create(req: api.CreateRequest): api.CreateResponse { + this.#debug('create', req); const { fqn, interfaces, overrides } = req; const requestArgs = req.args ?? []; - const ctorResult = this._findCtor(fqn, requestArgs); + const ctorResult = this.#findCtor(fqn, requestArgs); const ctor = ctorResult.ctor; const obj = new ctor( - ...this._toSandboxValues( + ...this.#toSandboxValues( requestArgs, `new ${fqn}`, ctorResult.parameters, ), ); - const objref = this.objects.registerObject(obj, fqn, req.interfaces ?? []); + const objref = this.#objects.registerObject(obj, fqn, req.interfaces ?? []); // overrides: for each one of the override method names, installs a // method on the newly created object which represents the remote "reverse proxy". if (overrides) { - this._debug('overrides', overrides); + this.#debug('overrides', overrides); const overrideTypeErrorMessage = 'Override can either be "method" or "property"'; @@ -655,7 +667,7 @@ export class Kernel { } methods.add(override.method); - this._applyMethodOverride(obj, objref, fqn, interfaces, override); + this.#applyMethodOverride(obj, objref, fqn, interfaces, override); } else if (api.isPropertyOverride(override)) { if (api.isMethodOverride(override)) { throw new JsiiFault(overrideTypeErrorMessage); @@ -667,7 +679,7 @@ export class Kernel { } properties.add(override.property); - this._applyPropertyOverride(obj, objref, fqn, interfaces, override); + this.#applyPropertyOverride(obj, objref, fqn, interfaces, override); } else { throw new JsiiFault(overrideTypeErrorMessage); } @@ -677,11 +689,11 @@ export class Kernel { return objref; } - private _getSuperPropertyName(name: string) { + #getSuperPropertyName(name: string) { return `$jsii$super$${name}$`; } - private _applyPropertyOverride( + #applyPropertyOverride( obj: any, objref: api.ObjRef, typeFqn: string, @@ -689,20 +701,20 @@ export class Kernel { override: api.PropertyOverride, ) { // error if we can find a method with this name - if (this._tryTypeInfoForMethod(override.property, typeFqn, interfaces)) { + if (this.#tryTypeInfoForMethod(override.property, typeFqn, interfaces)) { throw new JsiiFault( `Trying to override method '${override.property}' as a property`, ); } - let propInfo = this._tryTypeInfoForProperty( + let propInfo = this.#tryTypeInfoForProperty( override.property, typeFqn, interfaces, ); // if this is a private property (i.e. doesn't have `propInfo` the object has a key) if (!propInfo && override.property in obj) { - this._debug(`Skipping override of private property ${override.property}`); + this.#debug(`Skipping override of private property ${override.property}`); return; } @@ -719,10 +731,10 @@ export class Kernel { }; } - this._defineOverridenProperty(obj, objref, override, propInfo); + this.#defineOverridenProperty(obj, objref, override, propInfo); } - private _defineOverridenProperty( + #defineOverridenProperty( obj: any, objref: api.ObjRef, override: api.PropertyOverride, @@ -730,7 +742,7 @@ export class Kernel { ) { const propertyName = override.property; - this._debug('apply override', propertyName); + this.#debug('apply override', propertyName); // save the old property under $jsii$super$$ so that property overrides // can still access it via `super.`. @@ -743,7 +755,7 @@ export class Kernel { const prevEnumerable = prev.enumerable; prev.enumerable = false; - Object.defineProperty(obj, this._getSuperPropertyName(propertyName), prev); + Object.defineProperty(obj, this.#getSuperPropertyName(propertyName), prev); // we add callbacks for both 'get' and 'set', even if the property // is readonly. this is fine because if you try to set() a readonly @@ -752,32 +764,32 @@ export class Kernel { enumerable: prevEnumerable, configurable: prev.configurable, get: () => { - this._debug('virtual get', objref, propertyName, { + this.#debug('virtual get', objref, propertyName, { cookie: override.cookie, }); const result = this.callbackHandler({ cookie: override.cookie, - cbid: this._makecbid(), + cbid: this.#makecbid(), get: { objref, property: propertyName }, }); - this._debug('callback returned', result); - return this._toSandbox( + this.#debug('callback returned', result); + return this.#toSandbox( result, propInfo, `returned by callback property ${propertyName}`, ); }, set: (value: any) => { - this._debug('virtual set', objref, propertyName, { + this.#debug('virtual set', objref, propertyName, { cookie: override.cookie, }); this.callbackHandler({ cookie: override.cookie, - cbid: this._makecbid(), + cbid: this.#makecbid(), set: { objref, property: propertyName, - value: this._fromSandbox( + value: this.#fromSandbox( value, propInfo, `assigned to callback property ${propertyName}`, @@ -804,7 +816,7 @@ export class Kernel { } } - private _applyMethodOverride( + #applyMethodOverride( obj: any, objref: api.ObjRef, typeFqn: string, @@ -812,13 +824,13 @@ export class Kernel { override: api.MethodOverride, ) { // error if we can find a property with this name - if (this._tryTypeInfoForProperty(override.method, typeFqn, interfaces)) { + if (this.#tryTypeInfoForProperty(override.method, typeFqn, interfaces)) { throw new JsiiFault( `Trying to override property '${override.method}' as a method`, ); } - let methodInfo = this._tryTypeInfoForMethod( + let methodInfo = this.#tryTypeInfoForMethod( override.method, typeFqn, interfaces, @@ -827,7 +839,7 @@ export class Kernel { // If this is a private method (doesn't have methodInfo, key resolves on the object), we // are going to skip the override. if (!methodInfo && obj[override.method]) { - this._debug(`Skipping override of private method ${override.method}`); + this.#debug(`Skipping override of private method ${override.method}`); return; } @@ -849,17 +861,17 @@ export class Kernel { }; } - this._defineOverridenMethod(obj, objref, override, methodInfo); + this.#defineOverridenMethod(obj, objref, override, methodInfo); } - private _defineOverridenMethod( + #defineOverridenMethod( obj: any, objref: api.ObjRef, override: api.MethodOverride, methodInfo: spec.Method, ) { const methodName = override.method; - const fqn = jsiiTypeFqn(obj); + const fqn = jsiiTypeFqn(obj, this.#isVisibleType.bind(this)); const methodContext = `${methodInfo.async ? 'async ' : ''}method${ fqn ? `${fqn}#` : methodName }`; @@ -871,16 +883,16 @@ export class Kernel { configurable: false, writable: false, value: (...methodArgs: any[]) => { - this._debug('invoke async method override', override); - const args = this._toSandboxValues( + this.#debug('invoke async method override', override); + const args = this.#toSandboxValues( methodArgs, methodContext, methodInfo.parameters, ); return new Promise((succeed, fail) => { - const cbid = this._makecbid(); - this._debug('adding callback to queue', cbid); - this.cbs.set(cbid, { + const cbid = this.#makecbid(); + this.#debug('adding callback to queue', cbid); + this.#cbs.set(cbid, { objref, override, args, @@ -898,7 +910,7 @@ export class Kernel { configurable: false, writable: false, value: (...methodArgs: any[]) => { - this._debug( + this.#debug( 'invoke sync method override', override, 'args', @@ -909,19 +921,19 @@ export class Kernel { // other end has done its work. const result = this.callbackHandler({ cookie: override.cookie, - cbid: this._makecbid(), + cbid: this.#makecbid(), invoke: { objref, method: methodName, - args: this._fromSandboxValues( + args: this.#fromSandboxValues( methodArgs, methodContext, methodInfo.parameters, ), }, }); - this._debug('Result', result); - return this._toSandbox( + this.#debug('Result', result); + return this.#toSandbox( result, methodInfo.returns ?? 'void', `returned by callback method ${methodName}`, @@ -931,14 +943,10 @@ export class Kernel { } } - private _findInvokeTarget( - objref: api.ObjRef, - methodName: string, - args: any[], - ) { - const { instance, fqn, interfaces } = this.objects.findObject(objref); - const ti = this._typeInfoForMethod(methodName, fqn, interfaces); - this._validateMethodArguments(ti, args); + #findInvokeTarget(objref: api.ObjRef, methodName: string, args: any[]) { + const { instance, fqn, interfaces } = this.#objects.findObject(objref); + const ti = this.#typeInfoForMethod(methodName, fqn, interfaces); + this.#validateMethodArguments(ti, args); // always first look up the method in the prototype. this practically bypasses // any methods overridden by derived classes (which are by definition native @@ -957,10 +965,7 @@ export class Kernel { return { ti, obj: instance, fn }; } - private _validateMethodArguments( - method: spec.Callable | undefined, - args: any[], - ) { + #validateMethodArguments(method: spec.Callable | undefined, args: any[]) { const params: spec.Parameter[] = method?.parameters ?? []; // error if args > params @@ -999,17 +1004,17 @@ export class Kernel { } } - private _assemblyFor(assemblyName: string) { - const assembly = this.assemblies.get(assemblyName); + #assemblyFor(assemblyName: string) { + const assembly = this.#assemblies.get(assemblyName); if (!assembly) { throw new JsiiFault(`Could not find assembly: ${assemblyName}`); } return assembly; } - private _findSymbol(fqn: string) { + #findSymbol(fqn: string) { const [assemblyName, ...parts] = fqn.split('.'); - const assembly = this._assemblyFor(assemblyName); + const assembly = this.#assemblyFor(assemblyName); let curr = assembly.closure; while (parts.length > 0) { @@ -1026,11 +1031,11 @@ export class Kernel { return curr; } - private _typeInfoForFqn(fqn: string): spec.Type { + #typeInfoForFqn(fqn: spec.FQN): spec.Type { const components = fqn.split('.'); const moduleName = components[0]; - const assembly = this.assemblies.get(moduleName); + const assembly = this.#assemblies.get(moduleName); if (!assembly) { throw new JsiiFault(`Module '${moduleName}' not found`); } @@ -1044,12 +1049,32 @@ export class Kernel { return fqnInfo; } - private _typeInfoForMethod( + /** + * Determines whether the provided FQN corresponds to a valid, exported type + * from any currently loaded assembly. + * + * @param fqn the tested FQN. + * + * @returns `true` IIF the FQN corresponds to a know exported type. + */ + #isVisibleType(fqn: spec.FQN): boolean { + try { + /* ignored */ this.#typeInfoForFqn(fqn); + return true; + } catch (e) { + if (e instanceof JsiiFault) { + return false; + } + throw e; + } + } + + #typeInfoForMethod( methodName: string, fqn: string, interfaces?: string[], ): spec.Method { - const ti = this._tryTypeInfoForMethod(methodName, fqn, interfaces); + const ti = this.#tryTypeInfoForMethod(methodName, fqn, interfaces); if (!ti) { const addendum = interfaces && interfaces.length > 0 @@ -1062,7 +1087,7 @@ export class Kernel { return ti; } - private _tryTypeInfoForMethod( + #tryTypeInfoForMethod( methodName: string, classFqn: string, interfaces: string[] = [], @@ -1071,7 +1096,7 @@ export class Kernel { if (fqn === wire.EMPTY_OBJECT_FQN) { continue; } - const typeinfo = this._typeInfoForFqn(fqn); + const typeinfo = this.#typeInfoForFqn(fqn); const methods = (typeinfo as spec.ClassType | spec.InterfaceType).methods ?? []; @@ -1092,7 +1117,7 @@ export class Kernel { continue; } - const found = this._tryTypeInfoForMethod(methodName, base); + const found = this.#tryTypeInfoForMethod(methodName, base); if (found) { return found; } @@ -1102,7 +1127,7 @@ export class Kernel { return undefined; } - private _tryTypeInfoForProperty( + #tryTypeInfoForProperty( property: string, classFqn: string, interfaces: string[] = [], @@ -1111,7 +1136,7 @@ export class Kernel { if (fqn === wire.EMPTY_OBJECT_FQN) { continue; } - const typeInfo = this._typeInfoForFqn(fqn); + const typeInfo = this.#typeInfoForFqn(fqn); let properties; let bases; @@ -1138,7 +1163,7 @@ export class Kernel { // recurse to parent type (if exists) for (const baseFqn of bases) { - const ret = this._tryTypeInfoForProperty(property, baseFqn); + const ret = this.#tryTypeInfoForProperty(property, baseFqn); if (ret) { return ret; } @@ -1148,12 +1173,12 @@ export class Kernel { return undefined; } - private _typeInfoForProperty( + #typeInfoForProperty( property: string, fqn: string, interfaces?: string[], ): spec.Property { - const typeInfo = this._tryTypeInfoForProperty(property, fqn, interfaces); + const typeInfo = this.#tryTypeInfoForProperty(property, fqn, interfaces); if (!typeInfo) { const addendum = interfaces && interfaces.length > 0 @@ -1165,17 +1190,18 @@ export class Kernel { } return typeInfo; } - private _toSandbox( + #toSandbox( v: any, expectedType: wire.OptionalValueOrVoid, context: string, ): any { return wire.process( { - objects: this.objects, - debug: this._debug.bind(this), - findSymbol: this._findSymbol.bind(this), - lookupType: this._typeInfoForFqn.bind(this), + objects: this.#objects, + debug: this.#debug.bind(this), + findSymbol: this.#findSymbol.bind(this), + isVisibleType: this.#isVisibleType.bind(this), + lookupType: this.#typeInfoForFqn.bind(this), }, 'deserialize', v, @@ -1184,17 +1210,18 @@ export class Kernel { ); } - private _fromSandbox( + #fromSandbox( v: any, targetType: wire.OptionalValueOrVoid, context: string, ): any { return wire.process( { - objects: this.objects, - debug: this._debug.bind(this), - findSymbol: this._findSymbol.bind(this), - lookupType: this._typeInfoForFqn.bind(this), + objects: this.#objects, + debug: this.#debug.bind(this), + findSymbol: this.#findSymbol.bind(this), + isVisibleType: this.#isVisibleType.bind(this), + lookupType: this.#typeInfoForFqn.bind(this), }, 'serialize', v, @@ -1203,33 +1230,33 @@ export class Kernel { ); } - private _toSandboxValues( + #toSandboxValues( xs: readonly unknown[], methodContext: string, parameters: readonly spec.Parameter[] | undefined, ) { - return this._boxUnboxParameters( + return this.#boxUnboxParameters( xs, methodContext, parameters, - this._toSandbox.bind(this), + this.#toSandbox.bind(this), ); } - private _fromSandboxValues( + #fromSandboxValues( xs: readonly unknown[], methodContext: string, parameters: readonly spec.Parameter[] | undefined, ) { - return this._boxUnboxParameters( + return this.#boxUnboxParameters( xs, methodContext, parameters, - this._fromSandbox.bind(this), + this.#fromSandbox.bind(this), ); } - private _boxUnboxParameters( + #boxUnboxParameters( xs: readonly unknown[], methodContext: string, parameters: readonly spec.Parameter[] = [], @@ -1261,13 +1288,13 @@ export class Kernel { ); } - private _debug(...args: any[]) { + #debug(...args: any[]) { if (this.traceEnabled) { console.error('[@jsii/kernel]', ...args); } } - private _debugTime(cb: () => T, label: string): T { + #debugTime(cb: () => T, label: string): T { const fullLabel = `[@jsii/kernel:timing] ${label}`; if (this.debugTimingEnabled) { console.time(fullLabel); @@ -1285,8 +1312,8 @@ export class Kernel { * Ensures that `fn` is called and defends against beginning to invoke * async methods until fn finishes (successfully or not). */ - private _ensureSync(desc: string, fn: () => T): T { - this.syncInProgress = desc; + #ensureSync(desc: string, fn: () => T): T { + this.#syncInProgress = desc; try { return fn(); } catch (e: any) { @@ -1304,12 +1331,12 @@ export class Kernel { } throw new RuntimeError(e); } finally { - delete this.syncInProgress; + this.#syncInProgress = undefined; } } - private _findPropertyTarget(obj: any, property: string) { - const superProp = this._getSuperPropertyName(property); + #findPropertyTarget(obj: any, property: string) { + const superProp = this.#getSuperPropertyName(property); if (superProp in obj) { return superProp; } @@ -1319,10 +1346,10 @@ export class Kernel { /** * Shared (non-public implementation) to as not to break API recording. */ - private _getBinScriptCommand( + #getBinScriptCommand( req: api.GetScriptCommandRequest, ): api.GetScriptCommandResponse { - const packageDir = this._getPackageDir(req.assembly); + const packageDir = this.#getPackageDir(req.assembly); if (fs.pathExistsSync(packageDir)) { // module exists, verify version const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); @@ -1352,12 +1379,12 @@ export class Kernel { // type information // - private _makecbid() { - return `jsii::callback::${this.nextid++}`; + #makecbid() { + return `jsii::callback::${this.#nextid++}`; } - private _makeprid() { - return `jsii::promise::${this.nextid++}`; + #makeprid() { + return `jsii::promise::${this.#nextid++}`; } } diff --git a/packages/@jsii/kernel/src/objects.ts b/packages/@jsii/kernel/src/objects.ts index ea7d78fd76..7f7d205eb3 100644 --- a/packages/@jsii/kernel/src/objects.ts +++ b/packages/@jsii/kernel/src/objects.ts @@ -16,15 +16,30 @@ const OBJID_SYMBOL = Symbol.for('$__jsii__objid__$'); const IFACES_SYMBOL = Symbol.for('$__jsii__interfaces__$'); /** - * Symbol we use to tag constructors that are exported from a JSII module. + * Symbol we use to tag constructors that are exported from a jsii module. */ const JSII_TYPE_FQN_SYMBOL = Symbol('$__jsii__fqn__$'); -interface ManagedConstructor { - readonly [JSII_TYPE_FQN_SYMBOL]: string; -} +/** + * Symbol under which jsii runtime type information is stored. + */ +const JSII_RTTI_SYMBOL = Symbol.for('jsii.rtti'); + +/** + * Exported constructors processed by the jsii compiler have runtime type + * information woven into them under the `[JSII_RTTI]` symbol property. + */ +interface MaybeManagedConstructor { + readonly [JSII_RTTI_SYMBOL]?: { + /** The fully qualified name of the jsii type. */ + readonly fqn: string; + /** The version of the assembly that declared this type. */ + readonly version: string; + }; -type MaybeManagedConstructor = Partial; + /** The resolved JSII type FQN for this type. This is set only after resolution */ + readonly [JSII_TYPE_FQN_SYMBOL]?: spec.FQN; +} /** * Get the JSII fqn for an object (if available) @@ -32,9 +47,37 @@ type MaybeManagedConstructor = Partial; * This will return something if the object was constructed from a JSII-enabled * class/constructor, or if a literal object was annotated with type * information. + * + * @param obj the object for which a jsii FQN is requested. + * @param isVisibleType a function that determines if a type is visible. */ -export function jsiiTypeFqn(obj: any): string | undefined { - return (obj.constructor as MaybeManagedConstructor)[JSII_TYPE_FQN_SYMBOL]; +export function jsiiTypeFqn( + obj: any, + isVisibleType: (fqn: spec.FQN) => boolean, +): spec.FQN | undefined { + const ctor = obj.constructor as MaybeManagedConstructor; + + // We've already resolved for this type, return the cached value. + if (ctor[JSII_TYPE_FQN_SYMBOL] != null) { + return ctor[JSII_TYPE_FQN_SYMBOL]; + } + + let curr = ctor; + while (curr[JSII_RTTI_SYMBOL]?.fqn) { + if (isVisibleType(curr[JSII_RTTI_SYMBOL].fqn)) { + const fqn = curr[JSII_RTTI_SYMBOL].fqn; + + tagJsiiConstructor(curr, fqn); + tagJsiiConstructor(ctor, fqn); + + return fqn; + } + + // Walk up the prototype chain... + curr = Object.getPrototypeOf(curr); + } + + return undefined; } /** @@ -96,8 +139,9 @@ function tagObject(obj: unknown, objid: string, interfaces?: string[]) { */ export function tagJsiiConstructor(constructor: any, fqn: string) { if (Object.prototype.hasOwnProperty.call(constructor, JSII_TYPE_FQN_SYMBOL)) { - return assert( - constructor[JSII_TYPE_FQN_SYMBOL] === fqn, + return assert.strictEqual( + constructor[JSII_TYPE_FQN_SYMBOL], + fqn, `Unable to register ${constructor.name} as ${fqn}: it is already registerd with FQN ${constructor[JSII_TYPE_FQN_SYMBOL]}`, ); } @@ -119,12 +163,13 @@ export function tagJsiiConstructor(constructor: any, fqn: string) { * type. */ export class ObjectTable { - private readonly objects = new Map(); - private nextid = 10000; + readonly #objects = new Map(); + readonly #resolveType: (fqn: spec.FQN) => spec.Type; + #nextid = 10000; - public constructor( - private readonly resolveType: (fqn: string) => spec.Type, - ) {} + public constructor(resolveType: (fqn: spec.FQN) => spec.Type) { + this.#resolveType = resolveType; + } /** * Register the given object with the given type @@ -157,19 +202,19 @@ export class ObjectTable { ); } - this.objects.get(existingRef[api.TOKEN_REF])!.interfaces = + this.#objects.get(existingRef[api.TOKEN_REF])!.interfaces = (obj as any)[IFACES_SYMBOL] = existingRef[api.TOKEN_INTERFACES] = interfaces = - this.removeRedundant(Array.from(allIfaces), fqn); + this.#removeRedundant(Array.from(allIfaces), fqn); } return existingRef; } - interfaces = this.removeRedundant(interfaces, fqn); + interfaces = this.#removeRedundant(interfaces, fqn); - const objid = this.makeId(fqn); - this.objects.set(objid, { instance: obj, fqn, interfaces }); + const objid = this.#makeId(fqn); + this.#objects.set(objid, { instance: obj, fqn, interfaces }); tagObject(obj, objid, interfaces); return { [api.TOKEN_REF]: objid, [api.TOKEN_INTERFACES]: interfaces }; @@ -186,7 +231,7 @@ export class ObjectTable { } const objid = objref[api.TOKEN_REF]; - const obj = this.objects.get(objid); + const obj = this.#objects.get(objid); if (!obj) { throw new JsiiFault(`Object ${objid} not found`); } @@ -217,20 +262,20 @@ export class ObjectTable { * Delete the registration with the given objref */ public deleteObject({ [api.TOKEN_REF]: objid }: api.ObjRef) { - if (!this.objects.delete(objid)) { + if (!this.#objects.delete(objid)) { throw new JsiiFault(`Object ${objid} not found`); } } public get count(): number { - return this.objects.size; + return this.#objects.size; } - private makeId(fqn: string) { - return `${fqn}@${this.nextid++}`; + #makeId(fqn: string) { + return `${fqn}@${this.#nextid++}`; } - private removeRedundant( + #removeRedundant( interfaces: string[] | undefined, fqn: string, ): string[] | undefined { @@ -239,7 +284,7 @@ export class ObjectTable { } const result = new Set(interfaces); - const builtIn = new InterfaceCollection(this.resolveType); + const builtIn = new InterfaceCollection(this.#resolveType.bind(this)); if (fqn !== EMPTY_OBJECT_FQN) { builtIn.addFromClass(fqn); @@ -261,14 +306,15 @@ export interface RegisteredObject { } class InterfaceCollection implements Iterable { - private readonly interfaces = new Set(); + readonly #interfaces = new Set(); + readonly #resolveType: (fqn: spec.FQN) => spec.Type; - public constructor( - private readonly resolveType: (fqn: string) => spec.Type, - ) {} + public constructor(resolveType: (fqn: spec.FQN) => spec.Type) { + this.#resolveType = resolveType; + } public addFromClass(fqn: string): void { - const ti = this.resolveType(fqn); + const ti = this.#resolveType(fqn); if (!spec.isClassType(ti)) { throw new JsiiFault( `Expected a class, but received ${spec.describeTypeReference(ti)}`, @@ -279,17 +325,17 @@ class InterfaceCollection implements Iterable { } if (ti.interfaces) { for (const iface of ti.interfaces) { - if (this.interfaces.has(iface)) { + if (this.#interfaces.has(iface)) { continue; } - this.interfaces.add(iface); + this.#interfaces.add(iface); this.addFromInterface(iface); } } } public addFromInterface(fqn: string): void { - const ti = this.resolveType(fqn); + const ti = this.#resolveType(fqn); if (!spec.isInterfaceType(ti)) { throw new JsiiFault( `Expected an interface, but received ${spec.describeTypeReference(ti)}`, @@ -299,15 +345,15 @@ class InterfaceCollection implements Iterable { return; } for (const iface of ti.interfaces) { - if (this.interfaces.has(iface)) { + if (this.#interfaces.has(iface)) { continue; } - this.interfaces.add(iface); + this.#interfaces.add(iface); this.addFromInterface(iface); } } public [Symbol.iterator]() { - return this.interfaces[Symbol.iterator](); + return this.#interfaces[Symbol.iterator](); } } diff --git a/packages/@jsii/kernel/src/serialization.test.ts b/packages/@jsii/kernel/src/serialization.test.ts index 3ed3a510f5..938e69bed7 100644 --- a/packages/@jsii/kernel/src/serialization.test.ts +++ b/packages/@jsii/kernel/src/serialization.test.ts @@ -21,12 +21,16 @@ const TYPE_STRING: OptionalValue = { }; const TYPE_VOID = 'void'; +const isVisibleType: SerializerHost['isVisibleType'] = jest + .fn() + .mockName('host.isVisibleType'); const lookupType: SerializerHost['lookupType'] = jest .fn() .mockName('host.lookupType'); const host: SerializerHost = { debug: jest.fn().mockName('host.debug'), findSymbol: jest.fn().mockName('host.findSymbol'), + isVisibleType, lookupType, objects: new ObjectTable(lookupType), }; diff --git a/packages/@jsii/kernel/src/serialization.ts b/packages/@jsii/kernel/src/serialization.ts index 27b6be1a99..95c3779957 100644 --- a/packages/@jsii/kernel/src/serialization.ts +++ b/packages/@jsii/kernel/src/serialization.ts @@ -91,14 +91,16 @@ export const enum SerializationClass { Any = 'Any', } -type TypeLookup = (fqn: spec.FQN) => spec.Type; -type SymbolLookup = (fqn: spec.FQN) => any; +type FindSymbol = (fqn: spec.FQN) => any; +type IsVisibleType = (fqn: spec.FQN) => boolean; +type LookupType = (fqn: spec.FQN) => spec.Type; export interface SerializerHost { readonly objects: ObjectTable; debug(...args: any[]): void; - lookupType(fqn: string): spec.Type; - findSymbol(fqn: spec.FQN): any; + isVisibleType: IsVisibleType; + lookupType: LookupType; + findSymbol: FindSymbol; } interface Serializer { @@ -135,20 +137,24 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Date]: { - serialize(value, optionalValue): WireDate | undefined { - if (nullAndOk(value, optionalValue)) { + serialize(value, optionalValue, host): WireDate | undefined { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (!isDate(value)) { - throw new SerializationError(`Value is not an instance of Date`, value); + throw new SerializationError( + `Value is not an instance of Date`, + value, + host, + ); } return serializeDate(value); }, - deserialize(value, optionalValue) { - if (nullAndOk(value, optionalValue)) { + deserialize(value, optionalValue, host) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } @@ -156,6 +162,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Value does not have the "${TOKEN_DATE}" key`, value, + host, ); } return deserializeDate(value); @@ -164,8 +171,8 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Scalar]: { - serialize(value, optionalValue) { - if (nullAndOk(value, optionalValue)) { + serialize(value, optionalValue, host) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); @@ -176,19 +183,21 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Value is not a ${spec.describeTypeReference(optionalValue.type)}`, value, + host, ); } if (typeof value !== primitiveType.primitive) { throw new SerializationError( `Value is not a ${spec.describeTypeReference(optionalValue.type)}`, value, + host, ); } return value; }, - deserialize(value, optionalValue) { - if (nullAndOk(value, optionalValue)) { + deserialize(value, optionalValue, host) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); @@ -199,12 +208,14 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Value is not a ${spec.describeTypeReference(optionalValue.type)}`, value, + host, ); } if (typeof value !== primitiveType.primitive) { throw new SerializationError( `Value is not a ${spec.describeTypeReference(optionalValue.type)}`, value, + host, ); } @@ -214,9 +225,9 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Json]: { - serialize(value, optionalValue) { + serialize(value, optionalValue, host) { // /!\ Top-level "null" will turn to undefined, but any null nested in the value is valid JSON, so it'll stay! - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } @@ -225,7 +236,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { }, deserialize(value, optionalValue, host) { // /!\ Top-level "null" will turn to undefined, but any null nested in the value is valid JSON, so it'll stay! - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } @@ -257,7 +268,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { return value.map(mapJsonValue); } - return mapValues(value, mapJsonValue); + return mapValues(value, mapJsonValue, host); function mapJsonValue(toMap: any, key: string | number) { if (toMap == null) { @@ -277,13 +288,17 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Enum]: { serialize(value, optionalValue, host): WireEnum | undefined { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (typeof value !== 'string' && typeof value !== 'number') { - throw new SerializationError(`Value is not a string or number`, value); + throw new SerializationError( + `Value is not a string or number`, + value, + host, + ); } host.debug('Serializing enum'); @@ -297,12 +312,13 @@ export const SERIALIZERS: { [k: string]: Serializer } = { enumType, )}`, value, + host, ); } return { [TOKEN_ENUM]: `${enumType.fqn}/${enumEntry[0]}` }; }, deserialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } @@ -310,23 +326,24 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Value does not have the "${TOKEN_ENUM}" key`, value, + host, ); } - return deserializeEnum(value, host.findSymbol); + return deserializeEnum(value, host); }, }, // ---------------------------------------------------------------------- [SerializationClass.Array]: { serialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (!Array.isArray(value)) { - throw new SerializationError(`Value is not an array`, value); + throw new SerializationError(`Value is not an array`, value, host); } const arrayType = optionalValue.type as spec.CollectionTypeReference; @@ -342,13 +359,13 @@ export const SERIALIZERS: { [k: string]: Serializer } = { ); }, deserialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (!Array.isArray(value)) { - throw new SerializationError(`Value is not an array`, value); + throw new SerializationError(`Value is not an array`, value, host); } const arrayType = optionalValue.type as spec.CollectionTypeReference; @@ -368,21 +385,24 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Map]: { serialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); const mapType = optionalValue.type as spec.CollectionTypeReference; return { - [TOKEN_MAP]: mapValues(value, (v, key) => - process( - host, - 'serialize', - v, - { type: mapType.collection.elementtype }, - `key ${inspect(key)}`, - ), + [TOKEN_MAP]: mapValues( + value, + (v, key) => + process( + host, + 'serialize', + v, + { type: mapType.collection.elementtype }, + `key ${inspect(key)}`, + ), + host, ), }; }, @@ -392,7 +412,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { host, { allowNullishMapValue = false } = {}, ) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); @@ -400,7 +420,25 @@ export const SERIALIZERS: { [k: string]: Serializer } = { const mapType = optionalValue.type as spec.CollectionTypeReference; if (!isWireMap(value)) { // Compatibility mode with older versions that didn't wrap in [TOKEN_MAP] - return mapValues(value, (v, key) => + return mapValues( + value, + (v, key) => + process( + host, + 'deserialize', + v, + { + optional: allowNullishMapValue, + type: mapType.collection.elementtype, + }, + `key ${inspect(key)}`, + ), + host, + ); + } + const result = mapValues( + value[TOKEN_MAP], + (v, key) => process( host, 'deserialize', @@ -411,19 +449,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { }, `key ${inspect(key)}`, ), - ); - } - const result = mapValues(value[TOKEN_MAP], (v, key) => - process( - host, - 'deserialize', - v, - { - optional: allowNullishMapValue, - type: mapType.collection.elementtype, - }, - `key ${inspect(key)}`, - ), + host, ); Object.defineProperty(result, SYMBOL_WIRE_TYPE, { configurable: false, @@ -438,17 +464,17 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // ---------------------------------------------------------------------- [SerializationClass.Struct]: { serialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (typeof value !== 'object' || value == null || value instanceof Date) { - throw new SerializationError(`Value is not an object`, value); + throw new SerializationError(`Value is not an object`, value, host); } if (Array.isArray(value)) { - throw new SerializationError(`Value is an array`, value); + throw new SerializationError(`Value is an array`, value, host); } /* @@ -476,13 +502,13 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // Treat empty structs as `undefined` (see https://github.com/aws/jsii/issues/411) value = undefined; } - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (typeof value !== 'object' || value == null) { - throw new SerializationError(`Value is not an object`, value); + throw new SerializationError(`Value is not an object`, value, host); } const namedType = host.lookupType( @@ -494,6 +520,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( 'Value is an array (varargs may have been incorrectly supplied)', value, + host, ); } @@ -510,6 +537,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { host.objects.findObject(value).instance, namedType.fqn, props, + host, ); } @@ -519,6 +547,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Wired struct has type '${fqn}', which does not match expected type`, value, + host, ); } value = data; @@ -529,38 +558,42 @@ export const SERIALIZERS: { [k: string]: Serializer } = { value = value[api.TOKEN_MAP]; } - value = validateRequiredProps(value as any, namedType.fqn, props); + value = validateRequiredProps(value as any, namedType.fqn, props, host); // Return a dict COPY, we have by-value semantics anyway. - return mapValues(value, (v, key) => { - if (!props[key]) { - return undefined; - } // Don't map if unknown property - return process( - host, - 'deserialize', - v, - props[key], - `key ${inspect(key)}`, - ); - }); + return mapValues( + value, + (v, key) => { + if (!props[key]) { + return undefined; + } // Don't map if unknown property + return process( + host, + 'deserialize', + v, + props[key], + `key ${inspect(key)}`, + ); + }, + host, + ); }, }, // ---------------------------------------------------------------------- [SerializationClass.ReferenceType]: { serialize(value, optionalValue, host): ObjRef | undefined { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); if (typeof value !== 'object' || value == null || Array.isArray(value)) { - throw new SerializationError(`Value is not an object`, value); + throw new SerializationError(`Value is not an object`, value, host); } if (value instanceof Date) { - throw new SerializationError(`Value is a Date`, value); + throw new SerializationError(`Value is a Date`, value, host); } const expectedType = host.lookupType( @@ -570,13 +603,13 @@ export const SERIALIZERS: { [k: string]: Serializer } = { ? [expectedType.fqn] : undefined; const jsiiType = - jsiiTypeFqn(value) ?? + jsiiTypeFqn(value, host.isVisibleType) ?? (spec.isClassType(expectedType) ? expectedType.fqn : EMPTY_OBJECT_FQN); return host.objects.registerObject(value, jsiiType, interfaces); }, deserialize(value, optionalValue, host) { - if (nullAndOk(value, optionalValue)) { + if (nullAndOk(value, optionalValue, host)) { return undefined; } assert(optionalValue !== VOID, 'Encountered unexpected void type!'); @@ -588,6 +621,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `Value does not have the "${TOKEN_REF}" key`, value, + host, ); } @@ -611,6 +645,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { declaredType, )}`, value, + host, ); } } @@ -651,6 +686,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( 'Functions cannot be passed across language boundaries', value, + host, ); } @@ -658,6 +694,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( `A jsii kernel assumption was violated: value is not an object`, value, + host, ); } @@ -686,6 +723,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { throw new SerializationError( 'Set and Map instances cannot be sent across the language boundary', value, + host, ); } @@ -700,7 +738,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // If this is or should be a reference type, pass or make the reference // (Like regular reftype serialization, but without the type derivation to an interface) const jsiiType = - jsiiTypeFqn(value) ?? + jsiiTypeFqn(value, host.isVisibleType) ?? (isByReferenceOnly(value) ? EMPTY_OBJECT_FQN : undefined); if (jsiiType) { return host.objects.registerObject(value, jsiiType); @@ -712,14 +750,17 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // We will serialize by-value, but recurse for serialization so that if // the object contains reference objects, they will be serialized appropriately. // (Basically, serialize anything else as a map of 'any'). - return mapValues(value, (v, key) => - process( - host, - 'serialize', - v, - { type: spec.CANONICAL_ANY }, - `key ${inspect(key)}`, - ), + return mapValues( + value, + (v, key) => + process( + host, + 'serialize', + v, + { type: spec.CANONICAL_ANY }, + `key ${inspect(key)}`, + ), + host, ); }, @@ -751,7 +792,7 @@ export const SERIALIZERS: { [k: string]: Serializer } = { if (isWireEnum(value)) { host.debug('ANY is an Enum'); - return deserializeEnum(value, host.findSymbol); + return deserializeEnum(value, host); } if (isWireMap(value)) { host.debug('ANY is a Map'); @@ -787,14 +828,17 @@ export const SERIALIZERS: { [k: string]: Serializer } = { // At this point again, deserialize by-value. host.debug('ANY is a Map'); - return mapValues(value, (v, key) => - process( - host, - 'deserialize', - v, - { type: spec.CANONICAL_ANY }, - `key ${inspect(key)}`, - ), + return mapValues( + value, + (v, key) => + process( + host, + 'deserialize', + v, + { type: spec.CANONICAL_ANY }, + `key ${inspect(key)}`, + ), + host, ); }, }, @@ -808,24 +852,26 @@ function deserializeDate(value: WireDate): Date { return new Date(value[TOKEN_DATE]); } -function deserializeEnum(value: WireEnum, lookup: SymbolLookup) { +function deserializeEnum(value: WireEnum, host: SerializerHost) { const enumLocator = value[TOKEN_ENUM]; const sep = enumLocator.lastIndexOf('/'); if (sep === -1) { throw new SerializationError( `Invalid enum token value ${inspect(enumLocator)}`, value, + host, ); } const typeName = enumLocator.slice(0, sep); const valueName = enumLocator.slice(sep + 1); - const enumValue = lookup(typeName)[valueName]; + const enumValue = host.findSymbol(typeName)[valueName]; if (enumValue === undefined) { throw new SerializationError( `No such enum member: ${inspect(valueName)}`, value, + host, ); } return enumValue; @@ -843,7 +889,7 @@ export interface TypeSerialization { */ export function serializationType( typeRef: OptionalValueOrVoid, - lookup: TypeLookup, + lookup: LookupType, ): TypeSerialization[] { assert( typeRef != null, @@ -909,7 +955,11 @@ export function serializationType( return [{ serializationClass: SerializationClass.ReferenceType, typeRef }]; } -function nullAndOk(x: unknown, type: OptionalValueOrVoid): boolean { +function nullAndOk( + x: unknown, + type: OptionalValueOrVoid, + host: SerializerHost, +): boolean { if (x != null) { return false; } @@ -918,6 +968,7 @@ function nullAndOk(x: unknown, type: OptionalValueOrVoid): boolean { throw new SerializationError( `A value is required (type is non-optional)`, x, + host, ); } @@ -951,13 +1002,14 @@ function flatMap(xs: T[], fn: (x: T) => U[]): U[] { function mapValues( value: unknown, fn: (value: any, field: string) => any, + host: SerializerHost, ): { [key: string]: any } { if (typeof value !== 'object' || value == null) { - throw new SerializationError(`Value is not an object`, value); + throw new SerializationError(`Value is not an object`, value, host); } if (Array.isArray(value)) { - throw new SerializationError(`Value is an array`, value); + throw new SerializationError(`Value is an array`, value, host); } const out: { [key: string]: any } = {}; @@ -973,7 +1025,7 @@ function mapValues( function propertiesOf( t: spec.Type, - lookup: TypeLookup, + lookup: LookupType, ): { [name: string]: spec.Property } { if (!spec.isClassOrInterfaceType(t)) { return {}; @@ -1009,7 +1061,7 @@ function propertiesOf( function isAssignable( actualTypeFqn: string, requiredType: spec.NamedTypeReference, - lookup: TypeLookup, + lookup: LookupType, ): boolean { // The empty object is assignable to everything if (actualTypeFqn === EMPTY_OBJECT_FQN) { @@ -1042,6 +1094,7 @@ function validateRequiredProps( actualProps: { [key: string]: any }, typeName: string, specProps: { [key: string]: spec.Property }, + host: SerializerHost, ) { // Check for required properties const missingRequiredProps = Object.keys(specProps) @@ -1054,6 +1107,7 @@ function validateRequiredProps( .map((p) => inspect(p)) .join(', ')}`, actualProps, + host, ); } @@ -1143,6 +1197,7 @@ export function process( throw new SerializationError( `${titleize(context)}: Unable to ${serde} value as ${optionalTypeDescr}`, value, + host, errors, { renderValue: true }, ); @@ -1163,6 +1218,7 @@ export class SerializationError extends Error { public constructor( message: string, public readonly value: unknown, + { isVisibleType }: { readonly isVisibleType: (fqn: string) => boolean }, public readonly causes: readonly any[] = [], { renderValue = false }: { renderValue?: boolean } = {}, ) { @@ -1175,6 +1231,7 @@ export class SerializationError extends Error { causes.length > 0 ? '\u{251C}' : '\u{2570}' }\u{2500}\u{2500} \u{1F6D1} Failing value is ${describeTypeOf( value, + isVisibleType, )}`, ...(value == null ? [] @@ -1206,7 +1263,10 @@ export class SerializationError extends Error { } } -function describeTypeOf(value: unknown) { +function describeTypeOf( + value: unknown, + isVisibleType: (fqn: string) => boolean, +) { const type = typeof value; switch (type) { case 'object': @@ -1218,7 +1278,7 @@ function describeTypeOf(value: unknown) { return 'an array'; } - const fqn = jsiiTypeFqn(value as object); + const fqn = jsiiTypeFqn(value as object, isVisibleType); if (fqn != null && fqn !== EMPTY_OBJECT_FQN) { return `an instance of ${fqn}`; } diff --git a/packages/@jsii/kernel/src/serialization.ux.test.ts b/packages/@jsii/kernel/src/serialization.ux.test.ts index 5bb8e0e397..35d207b61d 100644 --- a/packages/@jsii/kernel/src/serialization.ux.test.ts +++ b/packages/@jsii/kernel/src/serialization.ux.test.ts @@ -13,6 +13,9 @@ expect.addSnapshotSerializer({ const findSymbol: jest.MockedFn = jest .fn() .mockName('SerializerHost.findSymbol'); +const isVisibleType: jest.MockedFn = jest + .fn() + .mockName('SerializerHost.isVisibleType'); const lookupType: jest.MockedFn = jest .fn() .mockName('SerializerHost.lookupType'); @@ -21,6 +24,7 @@ const objects = new ObjectTable(lookupType); const host: SerializerHost = { debug: () => void undefined, findSymbol, + isVisibleType, lookupType, objects, }; diff --git a/packages/@scope/jsii-calc-lib/lib/deprecation-removal.ts b/packages/@scope/jsii-calc-lib/lib/deprecation-removal.ts index 75e18b6776..cfd01dfc7d 100644 --- a/packages/@scope/jsii-calc-lib/lib/deprecation-removal.ts +++ b/packages/@scope/jsii-calc-lib/lib/deprecation-removal.ts @@ -1,10 +1,23 @@ +export class VisibleBaseClass { + public readonly propertyPresent: boolean; + + public constructor() { + this.propertyPresent = true; + } +} + export interface IInterface { method(): void; } /** @deprecated do not use me! */ -export class DeprecatedImplementation implements IInterface { - public method(): void { } +export class DeprecatedImplementation + extends VisibleBaseClass + implements IInterface +{ + public method(): void { + /** NOOP */ + } } export class InterfaceFactory { @@ -12,5 +25,5 @@ export class InterfaceFactory { return new DeprecatedImplementation(); } - private constructor() { } + private constructor() {} } diff --git a/packages/@scope/jsii-calc-lib/test/assembly.jsii b/packages/@scope/jsii-calc-lib/test/assembly.jsii index f19503b617..dfca777fbc 100644 --- a/packages/@scope/jsii-calc-lib/test/assembly.jsii +++ b/packages/@scope/jsii-calc-lib/test/assembly.jsii @@ -790,7 +790,7 @@ "kind": "interface", "locationInModule": { "filename": "lib/deprecation-removal.ts", - "line": 1 + "line": 9 }, "methods": [ { @@ -800,7 +800,7 @@ }, "locationInModule": { "filename": "lib/deprecation-removal.ts", - "line": 2 + "line": 10 }, "name": "method" } @@ -818,7 +818,7 @@ "kind": "class", "locationInModule": { "filename": "lib/deprecation-removal.ts", - "line": 10 + "line": 23 }, "methods": [ { @@ -827,7 +827,7 @@ }, "locationInModule": { "filename": "lib/deprecation-removal.ts", - "line": 11 + "line": 24 }, "name": "create", "returns": { @@ -842,6 +842,46 @@ "namespace": "deprecationRemoval", "symbolId": "lib/deprecation-removal:InterfaceFactory" }, + "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass": { + "assembly": "@scope/jsii-calc-lib", + "docs": { + "stability": "deprecated" + }, + "fqn": "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", + "initializer": { + "docs": { + "stability": "deprecated" + }, + "locationInModule": { + "filename": "lib/deprecation-removal.ts", + "line": 4 + } + }, + "kind": "class", + "locationInModule": { + "filename": "lib/deprecation-removal.ts", + "line": 1 + }, + "name": "VisibleBaseClass", + "namespace": "deprecationRemoval", + "properties": [ + { + "docs": { + "stability": "deprecated" + }, + "immutable": true, + "locationInModule": { + "filename": "lib/deprecation-removal.ts", + "line": 2 + }, + "name": "propertyPresent", + "type": { + "primitive": "boolean" + } + } + ], + "symbolId": "lib/deprecation-removal:VisibleBaseClass" + }, "@scope/jsii-calc-lib.submodule.IReflectable": { "assembly": "@scope/jsii-calc-lib", "docs": { @@ -1067,5 +1107,5 @@ } }, "version": "0.0.0", - "fingerprint": "yrpQ+/ynmcWEtNZgCAeq2yp4JpEpln2V/fRhUc5uVUs=" + "fingerprint": "o9wTL2Nj4gErRFWUIu9vurI1HEiTs0FLFa8hjfWpkHg=" } \ No newline at end of file diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-dotnet.test.js.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-dotnet.test.js.snap index ddc5175c17..d6f10a8c69 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-dotnet.test.js.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-dotnet.test.js.snap @@ -1015,7 +1015,8 @@ exports[`Generated code for "@scope/jsii-calc-lib": / 1`] = ` ┃ ┃ ┣━ 📄 BaseFor2647.cs ┃ ┃ ┣━ 📁 DeprecationRemoval ┃ ┃ ┃ ┣━ 📄 IInterface.cs - ┃ ┃ ┃ ┗━ 📄 InterfaceFactory.cs + ┃ ┃ ┃ ┣━ 📄 InterfaceFactory.cs + ┃ ┃ ┃ ┗━ 📄 VisibleBaseClass.cs ┃ ┃ ┣━ 📄 DiamondLeft.cs ┃ ┃ ┣━ 📄 DiamondRight.cs ┃ ┃ ┣━ 📄 EnumFromScopedModule.cs @@ -1247,6 +1248,64 @@ namespace Amazon.JSII.Tests.CalculatorNamespace.LibNamespace.DeprecationRemoval `; +exports[`Generated code for "@scope/jsii-calc-lib": /dotnet/Amazon.JSII.Tests.CalculatorPackageId.LibPackageId/Amazon/JSII/Tests/CalculatorNamespace/LibNamespace/DeprecationRemoval/VisibleBaseClass.cs 1`] = ` +using Amazon.JSII.Runtime.Deputy; + +#pragma warning disable CS0672,CS0809,CS1591 + +namespace Amazon.JSII.Tests.CalculatorNamespace.LibNamespace.DeprecationRemoval +{ + /// + /// Stability: Deprecated + /// + [JsiiClass(nativeType: typeof(Amazon.JSII.Tests.CalculatorNamespace.LibNamespace.DeprecationRemoval.VisibleBaseClass), fullyQualifiedName: "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass")] + [System.Obsolete()] + public class VisibleBaseClass : DeputyBase + { + /// + /// Stability: Deprecated + /// + [System.Obsolete()] + public VisibleBaseClass(): base(_MakeDeputyProps()) + { + } + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static DeputyProps _MakeDeputyProps() + { + return new DeputyProps(System.Array.Empty()); + } + + /// Used by jsii to construct an instance of this class from a Javascript-owned object reference + /// The Javascript-owned object reference + [System.Obsolete()] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + protected VisibleBaseClass(ByRefValue reference): base(reference) + { + } + + /// Used by jsii to construct an instance of this class from DeputyProps + /// The deputy props + [System.Obsolete()] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + protected VisibleBaseClass(DeputyProps props): base(props) + { + } + + /// + /// Stability: Deprecated + /// + [JsiiProperty(name: "propertyPresent", typeJson: "{\\"primitive\\":\\"boolean\\"}")] + [System.Obsolete()] + public virtual bool PropertyPresent + { + get => GetInstanceProperty()!; + } + } +} + +`; + exports[`Generated code for "@scope/jsii-calc-lib": /dotnet/Amazon.JSII.Tests.CalculatorPackageId.LibPackageId/Amazon/JSII/Tests/CalculatorNamespace/LibNamespace/DiamondLeft.cs 1`] = ` using Amazon.JSII.Runtime.Deputy; diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-go.test.js.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-go.test.js.snap index 61f54f6d5f..2200fe6278 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-go.test.js.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-go.test.js.snap @@ -969,7 +969,8 @@ exports[`Generated code for "@scope/jsii-calc-lib": / 1`] = ` ┣━ 📁 deprecationremoval ┃ ┣━ 📄 IInterface.go ┃ ┣━ 📄 InterfaceFactory.go - ┃ ┗━ 📄 main.go + ┃ ┣━ 📄 main.go + ┃ ┗━ 📄 VisibleBaseClass.go ┣━ 📄 DiamondLeft.go ┣━ 📄 DiamondRight.go ┣━ 📄 EnumFromScopedModule.go @@ -2132,6 +2133,65 @@ func InterfaceFactory_Create() IInterface { } +`; + +exports[`Generated code for "@scope/jsii-calc-lib": /go/scopejsiicalclib/deprecationremoval/VisibleBaseClass.go 1`] = ` +package deprecationremoval + +import ( + _jsii_ "github.com/aws/jsii-runtime-go/runtime" + _init_ "github.com/aws/jsii/jsii-calc/go/scopejsiicalclib/jsii" +) + +// Deprecated. +type VisibleBaseClass interface { + // Deprecated. + PropertyPresent() *bool +} + +// The jsii proxy struct for VisibleBaseClass +type jsiiProxy_VisibleBaseClass struct { + _ byte // padding +} + +func (j *jsiiProxy_VisibleBaseClass) PropertyPresent() *bool { + var returns *bool + _jsii_.Get( + j, + "propertyPresent", + &returns, + ) + return returns +} + + +// Deprecated. +func NewVisibleBaseClass() VisibleBaseClass { + _init_.Initialize() + + j := jsiiProxy_VisibleBaseClass{} + + _jsii_.Create( + "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", + nil, // no parameters + &j, + ) + + return &j +} + +// Deprecated. +func NewVisibleBaseClass_Override(v VisibleBaseClass) { + _init_.Initialize() + + _jsii_.Create( + "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", + nil, // no parameters + v, + ) +} + + `; exports[`Generated code for "@scope/jsii-calc-lib": /go/scopejsiicalclib/deprecationremoval/main.go 1`] = ` @@ -2162,6 +2222,16 @@ func init() { return &jsiiProxy_InterfaceFactory{} }, ) + _jsii_.RegisterClass( + "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", + reflect.TypeOf((*VisibleBaseClass)(nil)).Elem(), + []_jsii_.Member{ + _jsii_.MemberProperty{JsiiProperty: "propertyPresent", GoGetter: "PropertyPresent"}, + }, + func() interface{} { + return &jsiiProxy_VisibleBaseClass{} + }, + ) } `; diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-java.test.js.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-java.test.js.snap index 87b61f7be7..1b52ed4577 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-java.test.js.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-java.test.js.snap @@ -1444,7 +1444,8 @@ exports[`Generated code for "@scope/jsii-calc-lib": / 1`] = ` ┃ ┣━ 📄 BaseFor2647.java ┃ ┣━ 📁 deprecation_removal ┃ ┃ ┣━ 📄 IInterface.java - ┃ ┃ ┗━ 📄 InterfaceFactory.java + ┃ ┃ ┣━ 📄 InterfaceFactory.java + ┃ ┃ ┗━ 📄 VisibleBaseClass.java ┃ ┣━ 📄 DiamondLeft.java ┃ ┣━ 📄 DiamondRight.java ┃ ┣━ 📄 EnumFromScopedModule.java @@ -3682,6 +3683,45 @@ public class InterfaceFactory extends software.amazon.jsii.JsiiObject { `; +exports[`Generated code for "@scope/jsii-calc-lib": /java/src/main/java/software/amazon/jsii/tests/calculator/lib/deprecation_removal/VisibleBaseClass.java 1`] = ` +package software.amazon.jsii.tests.calculator.lib.deprecation_removal; + +/** + */ +@javax.annotation.Generated(value = "jsii-pacmak") +@software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Deprecated) +@Deprecated +@software.amazon.jsii.Jsii(module = software.amazon.jsii.tests.calculator.lib.$Module.class, fqn = "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass") +public class VisibleBaseClass extends software.amazon.jsii.JsiiObject { + + protected VisibleBaseClass(final software.amazon.jsii.JsiiObjectRef objRef) { + super(objRef); + } + + protected VisibleBaseClass(final software.amazon.jsii.JsiiObject.InitializationMode initializationMode) { + super(initializationMode); + } + + /** + */ + @software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Deprecated) + @Deprecated + public VisibleBaseClass() { + super(software.amazon.jsii.JsiiObject.InitializationMode.JSII); + software.amazon.jsii.JsiiEngine.getInstance().createNewObject(this); + } + + /** + */ + @software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Deprecated) + @Deprecated + public @org.jetbrains.annotations.NotNull java.lang.Boolean getPropertyPresent() { + return software.amazon.jsii.Kernel.get(this, "propertyPresent", software.amazon.jsii.NativeType.forClass(java.lang.Boolean.class)); + } +} + +`; + exports[`Generated code for "@scope/jsii-calc-lib": /java/src/main/java/software/amazon/jsii/tests/calculator/lib/package-info.java 1`] = ` /** * @@ -3709,6 +3749,7 @@ exports[`Generated code for "@scope/jsii-calc-lib": /java/src/main/resou @scope/jsii-calc-lib.StructWithOnlyOptionals=software.amazon.jsii.tests.calculator.lib.StructWithOnlyOptionals @scope/jsii-calc-lib.deprecationRemoval.IInterface=software.amazon.jsii.tests.calculator.lib.deprecation_removal.IInterface @scope/jsii-calc-lib.deprecationRemoval.InterfaceFactory=software.amazon.jsii.tests.calculator.lib.deprecation_removal.InterfaceFactory +@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass=software.amazon.jsii.tests.calculator.lib.deprecation_removal.VisibleBaseClass @scope/jsii-calc-lib.submodule.IReflectable=software.amazon.jsii.tests.calculator.custom_submodule_name.IReflectable @scope/jsii-calc-lib.submodule.NestingClass=software.amazon.jsii.tests.calculator.custom_submodule_name.NestingClass @scope/jsii-calc-lib.submodule.NestingClass.NestedClass=software.amazon.jsii.tests.calculator.custom_submodule_name.NestingClass$NestedClass diff --git a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.js.snap b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.js.snap index eb50eebd42..084f31c4ed 100644 --- a/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.js.snap +++ b/packages/jsii-pacmak/test/generated-code/__snapshots__/target-python.test.js.snap @@ -2334,9 +2334,33 @@ class InterfaceFactory( return typing.cast(IInterface, jsii.sinvoke(cls, "create", [])) +class VisibleBaseClass( + metaclass=jsii.JSIIMeta, + jsii_type="@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", +): + ''' + :stability: deprecated + ''' + + def __init__(self) -> None: + ''' + :stability: deprecated + ''' + jsii.create(self.__class__, self, []) + + @builtins.property + @jsii.member(jsii_name="propertyPresent") + def property_present(self) -> builtins.bool: + ''' + :stability: deprecated + ''' + return typing.cast(builtins.bool, jsii.get(self, "propertyPresent")) + + __all__ = [ "IInterface", "InterfaceFactory", + "VisibleBaseClass", ] publication.publish() diff --git a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap index 835e0e619b..4d3be9416d 100644 --- a/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/jsii-tree.test.js.snap @@ -3511,6 +3511,12 @@ exports[`jsii-tree --all 1`] = ` │ │ │ └─┬ static create() method (deprecated) │ │ │ ├── static │ │ │ └── returns: @scope/jsii-calc-lib.deprecationRemoval.IInterface + │ │ ├─┬ class VisibleBaseClass (deprecated) + │ │ │ └─┬ members + │ │ │ ├── () initializer (deprecated) + │ │ │ └─┬ propertyPresent property (deprecated) + │ │ │ ├── immutable + │ │ │ └── type: boolean │ │ └─┬ interface IInterface (deprecated) │ │ └─┬ members │ │ └─┬ method() method (deprecated) @@ -4184,6 +4190,7 @@ exports[`jsii-tree --inheritance 1`] = ` │ ├─┬ deprecationRemoval │ │ └─┬ types │ │ ├── class InterfaceFactory + │ │ ├── class VisibleBaseClass │ │ └── interface IInterface │ └─┬ submodule │ └─┬ types @@ -5808,6 +5815,10 @@ exports[`jsii-tree --members 1`] = ` │ │ ├─┬ class InterfaceFactory │ │ │ └─┬ members │ │ │ └── static create() method + │ │ ├─┬ class VisibleBaseClass + │ │ │ └─┬ members + │ │ │ ├── () initializer + │ │ │ └── propertyPresent property │ │ └─┬ interface IInterface │ │ └─┬ members │ │ └── method() method @@ -6354,6 +6365,7 @@ exports[`jsii-tree --types 1`] = ` │ ├─┬ deprecationRemoval │ │ └─┬ types │ │ ├── class InterfaceFactory + │ │ ├── class VisibleBaseClass │ │ └── interface IInterface │ └─┬ submodule │ └─┬ types diff --git a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap index 4e12655772..0ac611c611 100644 --- a/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/tree.test.js.snap @@ -3688,6 +3688,12 @@ exports[`showAll 1`] = ` │ │ │ └─┬ static create() method │ │ │ ├── static │ │ │ └── returns: @scope/jsii-calc-lib.deprecationRemoval.IInterface + │ │ ├─┬ class VisibleBaseClass + │ │ │ └─┬ members + │ │ │ ├── () initializer + │ │ │ └─┬ propertyPresent property + │ │ │ ├── immutable + │ │ │ └── type: boolean │ │ └─┬ interface IInterface │ │ └─┬ members │ │ └─┬ method() method @@ -4321,6 +4327,7 @@ exports[`types 1`] = ` │ ├─┬ deprecationRemoval │ │ └─┬ types │ │ ├── class InterfaceFactory + │ │ ├── class VisibleBaseClass │ │ └── interface IInterface │ └─┬ submodule │ └─┬ types diff --git a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap index 0ad963b291..65e75d7eb7 100644 --- a/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap +++ b/packages/jsii-reflect/test/__snapshots__/type-system.test.js.snap @@ -21,6 +21,7 @@ exports[`TypeSystem.classes lists all the classes in the typesystem 1`] = ` "@scope/jsii-calc-lib.NumericValue", "@scope/jsii-calc-lib.Operation", "@scope/jsii-calc-lib.deprecationRemoval.InterfaceFactory", + "@scope/jsii-calc-lib.deprecationRemoval.VisibleBaseClass", "@scope/jsii-calc-lib.submodule.NestingClass", "@scope/jsii-calc-lib.submodule.NestingClass.NestedClass", "@scope/jsii-calc-lib.submodule.Reflector", From 565e04a949e91c6836d9776ebc3fe08744fb9df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BB=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier?= Date: Mon, 17 Jul 2023 14:39:19 +0200 Subject: [PATCH 2/6] also fix that exports is not being used --- packages/@jsii/kernel/src/kernel.ts | 20 ++++++++++++++----- .../src/jsii/_kernel/providers/process.py | 2 ++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/@jsii/kernel/src/kernel.ts b/packages/@jsii/kernel/src/kernel.ts index 4223f3e9f3..bdf582ba38 100644 --- a/packages/@jsii/kernel/src/kernel.ts +++ b/packages/@jsii/kernel/src/kernel.ts @@ -43,7 +43,7 @@ export class Kernel { /** * Set to true for timing data to be emitted. */ - public debugTimingEnabled = false; + public debugTimingEnabled = !!process.env.JSII_DEBUG_TIMING; readonly #assemblies = new Map(); readonly #objects = new ObjectTable(this.#typeInfoForFqn.bind(this)); @@ -138,7 +138,7 @@ export class Kernel { } // read .jsii metadata from the root of the package - let assmSpec; + let assmSpec: spec.Assembly; try { assmSpec = this.#debugTime( () => loadAssemblyFromPath(packageDir), @@ -150,13 +150,23 @@ export class Kernel { ); } + // We do a `require.resolve` call, as otherwise, requiring with a directory will cause any `exports` from + // `package.json` to be ignored, preventing injection of a "lazy index" entry point. + const entryPoint = this.#require!.resolve(assmSpec.name, { + paths: [this.#installDir!], + }); // load the module and capture its closure const closure = this.#debugTime( - () => this.#require!(packageDir), - `require(${packageDir})`, + () => this.#require!(entryPoint), + `require(${entryPoint})`, ); const assm = new Assembly(assmSpec, closure); - this.#addAssembly(assm); + this.#debugTime( + () => this.#addAssembly(assm), + `registerAssembly({ name: ${assm.metadata.name}, types: ${ + Object.keys(assm.metadata.types ?? {}).length + } })`, + ); return { assembly: assmSpec.name, diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py index f595ebe6d1..0e6fb9fb64 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py @@ -313,6 +313,8 @@ def handshake(self) -> None: # TODO: Replace with proper error. assert ( resp.hello == f"@jsii/runtime@{__jsii_runtime_version__}" + # Transparently allow development versions of the runtime to be used. + or resp.hello == f"@jsii/runtime@0.0.0" ), f"Invalid JSII Runtime Version: {resp.hello!r}" def send( From 19c78715109f2c7207344377f62da171a7114908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BB=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier?= Date: Mon, 17 Jul 2023 15:34:56 +0200 Subject: [PATCH 3/6] default-enable the package cache, set --preserve-symlinks --- packages/@jsii/kernel/src/link.ts | 24 ++++++++++++++++++-- packages/@jsii/kernel/src/tar-cache/index.ts | 7 ++++-- packages/@jsii/runtime/bin/jsii-runtime.ts | 8 ++++++- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/packages/@jsii/kernel/src/link.ts b/packages/@jsii/kernel/src/link.ts index 51f95aa4f1..8d518209da 100644 --- a/packages/@jsii/kernel/src/link.ts +++ b/packages/@jsii/kernel/src/link.ts @@ -1,5 +1,19 @@ -import { copyFileSync, linkSync, mkdirSync, readdirSync, statSync } from 'fs'; -import { join } from 'path'; +import { + copyFileSync, + linkSync, + mkdirSync, + readdirSync, + statSync, + symlinkSync, +} from 'fs'; +import { dirname, join } from 'path'; + +/** + * If `node` is started with `--preserve-symlinks`, the module loaded will + * preserve symbolic links instead of resolving them, making it possible to + * symbolically link packages in place instead of fully copying them. + */ +const PRESERVE_SYMLINKS = process.execArgv.includes('--preserve-symlinks'); /** * Creates directories containing hard links if possible, and falls back on @@ -9,6 +23,12 @@ import { join } from 'path'; * @param destination is the new file or directory to create. */ export function link(existing: string, destination: string): void { + if (PRESERVE_SYMLINKS) { + mkdirSync(dirname(destination), { recursive: true }); + symlinkSync(existing, destination); + return; + } + const stat = statSync(existing); if (!stat.isDirectory()) { try { diff --git a/packages/@jsii/kernel/src/tar-cache/index.ts b/packages/@jsii/kernel/src/tar-cache/index.ts index b1cf0f5319..b94939f835 100644 --- a/packages/@jsii/kernel/src/tar-cache/index.ts +++ b/packages/@jsii/kernel/src/tar-cache/index.ts @@ -24,7 +24,8 @@ export interface ExtractResult { } let packageCacheEnabled = - process.env.JSII_RUNTIME_PACKAGE_CACHE?.toLocaleLowerCase() === 'enabled'; + (process.env.JSII_RUNTIME_PACKAGE_CACHE?.toLocaleLowerCase() ?? 'enabled') === + 'enabled'; /** * Extracts the content of a tarball, possibly caching it on disk. @@ -41,7 +42,6 @@ export function extract( options: ExtractOptions, ...comments: readonly string[] ): ExtractResult { - mkdirSync(outDir, { recursive: true }); try { return (packageCacheEnabled ? extractViaCache : extractToOutDir)( file, @@ -100,6 +100,9 @@ function extractToOutDir( cwd: string, options: ExtractOptions = {}, ): { cache?: undefined } { + // The output directory must already exist... + mkdirSync(cwd, { recursive: true }); + // !!!IMPORTANT!!! // Extract directly into the final target directory, as certain antivirus // software configurations on Windows will make a `renameSync` operation diff --git a/packages/@jsii/runtime/bin/jsii-runtime.ts b/packages/@jsii/runtime/bin/jsii-runtime.ts index 111bb70e95..0b4fa0347a 100644 --- a/packages/@jsii/runtime/bin/jsii-runtime.ts +++ b/packages/@jsii/runtime/bin/jsii-runtime.ts @@ -12,7 +12,13 @@ import { Duplex } from 'stream'; // - FD#3 is the communication pipe to read & write jsii API messages const child = spawn( process.execPath, - [...process.execArgv, resolve(__dirname, '..', 'lib', 'program.js')], + [ + ...process.execArgv, + // Instruct the module loader to NOT resolve symbolic links, so we don't + // have to copy modules around all the time (which is expensive to do). + '--preserve-symlinks', + resolve(__dirname, '..', 'lib', 'program.js'), + ], { stdio: ['ignore', 'pipe', 'pipe', 'pipe'] }, ); From cfe40a635719af394e874fe82805aa105ad30683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BB=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier?= Date: Tue, 18 Jul 2023 13:03:54 +0200 Subject: [PATCH 4/6] undo unnecessary change --- packages/@jsii/kernel/src/kernel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@jsii/kernel/src/kernel.ts b/packages/@jsii/kernel/src/kernel.ts index ca1518f9cd..bd8edec44b 100644 --- a/packages/@jsii/kernel/src/kernel.ts +++ b/packages/@jsii/kernel/src/kernel.ts @@ -42,7 +42,7 @@ export class Kernel { /** * Set to true for timing data to be emitted. */ - public debugTimingEnabled = !!process.env.JSII_DEBUG_TIMING; + public debugTimingEnabled = false; readonly #assemblies = new Map(); readonly #objects = new ObjectTable(this.#typeInfoForFqn.bind(this)); From d5ae229d24e744307289937555298408b3e3edc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=A7=91=F0=9F=8F=BB=E2=80=8D=F0=9F=92=BB=20Romain=20M?= =?UTF-8?q?arcadier?= Date: Tue, 18 Jul 2023 13:24:44 +0200 Subject: [PATCH 5/6] add some info in CONTRIBUTING.md --- CONTRIBUTING.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5a5b2f1c1f..5d313502da 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,6 +89,71 @@ Each one of these scripts can be executed either from the root of the repo using `npx lerna run