From 1c569022573d7ed8b6a80c28bf605c30284d3d29 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Tue, 16 Oct 2018 15:03:49 +0200 Subject: [PATCH] fix(jsii): use base interfaces for 'datatype' property (#265) If an interface inherits from a non-datatype interface, it should no longer be classified as a datatype interface itself. Making this change requires that information about the base classes has already been determined, so I introduced an ordering mechanism for 'deferred's. Fixes #264. --- packages/jsii-calc/lib/compliance.ts | 13 ++ packages/jsii-calc/test/assembly.jsii | 48 ++++- .../.jsii | 48 ++++- .../IIInterfaceThatShouldNotBeADataType.cs | 18 ++ .../IIInterfaceWithMethods.cs | 17 ++ ...IInterfaceThatShouldNotBeADataTypeProxy.cs | 34 ++++ .../IInterfaceWithMethodsProxy.cs | 24 +++ .../amazon/jsii/tests/calculator/$Module.java | 2 + .../IInterfaceThatShouldNotBeADataType.java | 34 ++++ .../calculator/IInterfaceWithMethods.java | 26 +++ .../expected.jsii-calc/sphinx/jsii-calc.rst | 91 ++++++++++ packages/jsii/lib/assembler.ts | 164 ++++++++++++++---- 12 files changed, 483 insertions(+), 36 deletions(-) create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceThatShouldNotBeADataType.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceWithMethods.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceThatShouldNotBeADataTypeProxy.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceWithMethodsProxy.cs create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceThatShouldNotBeADataType.java create mode 100644 packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceWithMethods.java diff --git a/packages/jsii-calc/lib/compliance.ts b/packages/jsii-calc/lib/compliance.ts index 3d2dcaed07..b90576a6fc 100644 --- a/packages/jsii-calc/lib/compliance.ts +++ b/packages/jsii-calc/lib/compliance.ts @@ -926,3 +926,16 @@ export class ClassWithPrivateConstructorAndAutomaticProperties implements IInter private constructor(public readonly readOnlyString: string, public readWriteString: string) { } } + +export interface IInterfaceWithMethods { + readonly value: string; + doThings(): void; +} + +/** + * Even though this interface has only properties, it is disqualified from being a datatype + * because it inherits from an interface that is not a datatype. + */ +export interface IInterfaceThatShouldNotBeADataType extends IInterfaceWithMethods { + readonly otherValue: string; +} \ No newline at end of file diff --git a/packages/jsii-calc/test/assembly.jsii b/packages/jsii-calc/test/assembly.jsii index d89e2d843d..cca58d3622 100644 --- a/packages/jsii-calc/test/assembly.jsii +++ b/packages/jsii-calc/test/assembly.jsii @@ -1452,6 +1452,52 @@ "kind": "interface", "name": "IFriendlyRandomGenerator" }, + "jsii-calc.IInterfaceThatShouldNotBeADataType": { + "assembly": "jsii-calc", + "docs": { + "comment": "Even though this interface has only properties, it is disqualified from being a datatype\nbecause it inherits from an interface that is not a datatype." + }, + "fqn": "jsii-calc.IInterfaceThatShouldNotBeADataType", + "interfaces": [ + { + "fqn": "jsii-calc.IInterfaceWithMethods" + } + ], + "kind": "interface", + "name": "IInterfaceThatShouldNotBeADataType", + "properties": [ + { + "abstract": true, + "immutable": true, + "name": "otherValue", + "type": { + "primitive": "string" + } + } + ] + }, + "jsii-calc.IInterfaceWithMethods": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IInterfaceWithMethods", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "doThings" + } + ], + "name": "IInterfaceWithMethods", + "properties": [ + { + "abstract": true, + "immutable": true, + "name": "value", + "type": { + "primitive": "string" + } + } + ] + }, "jsii-calc.IInterfaceWithProperties": { "assembly": "jsii-calc", "datatype": true, @@ -3355,5 +3401,5 @@ } }, "version": "0.7.7", - "fingerprint": "16f4wL/B1M7rOOzyAzBEtqlOi2GYhDAU5rctonoha5Y=" + "fingerprint": "vJH1gHlpRxKo77e0kE+6KATwgsZB0VpBcFEo/9OIG7Q=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii index d89e2d843d..cca58d3622 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/.jsii @@ -1452,6 +1452,52 @@ "kind": "interface", "name": "IFriendlyRandomGenerator" }, + "jsii-calc.IInterfaceThatShouldNotBeADataType": { + "assembly": "jsii-calc", + "docs": { + "comment": "Even though this interface has only properties, it is disqualified from being a datatype\nbecause it inherits from an interface that is not a datatype." + }, + "fqn": "jsii-calc.IInterfaceThatShouldNotBeADataType", + "interfaces": [ + { + "fqn": "jsii-calc.IInterfaceWithMethods" + } + ], + "kind": "interface", + "name": "IInterfaceThatShouldNotBeADataType", + "properties": [ + { + "abstract": true, + "immutable": true, + "name": "otherValue", + "type": { + "primitive": "string" + } + } + ] + }, + "jsii-calc.IInterfaceWithMethods": { + "assembly": "jsii-calc", + "fqn": "jsii-calc.IInterfaceWithMethods", + "kind": "interface", + "methods": [ + { + "abstract": true, + "name": "doThings" + } + ], + "name": "IInterfaceWithMethods", + "properties": [ + { + "abstract": true, + "immutable": true, + "name": "value", + "type": { + "primitive": "string" + } + } + ] + }, "jsii-calc.IInterfaceWithProperties": { "assembly": "jsii-calc", "datatype": true, @@ -3355,5 +3401,5 @@ } }, "version": "0.7.7", - "fingerprint": "16f4wL/B1M7rOOzyAzBEtqlOi2GYhDAU5rctonoha5Y=" + "fingerprint": "vJH1gHlpRxKo77e0kE+6KATwgsZB0VpBcFEo/9OIG7Q=" } diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceThatShouldNotBeADataType.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceThatShouldNotBeADataType.cs new file mode 100644 index 0000000000..f5bc2e64d9 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceThatShouldNotBeADataType.cs @@ -0,0 +1,18 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + /// + /// Even though this interface has only properties, it is disqualified from being a datatype + /// because it inherits from an interface that is not a datatype. + /// + [JsiiInterface(typeof(IIInterfaceThatShouldNotBeADataType), "jsii-calc.IInterfaceThatShouldNotBeADataType")] + public interface IIInterfaceThatShouldNotBeADataType : IIInterfaceWithMethods + { + [JsiiProperty("otherValue", "{\"primitive\":\"string\"}")] + string OtherValue + { + get; + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceWithMethods.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceWithMethods.cs new file mode 100644 index 0000000000..ccd793c1da --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IIInterfaceWithMethods.cs @@ -0,0 +1,17 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiInterface(typeof(IIInterfaceWithMethods), "jsii-calc.IInterfaceWithMethods")] + public interface IIInterfaceWithMethods + { + [JsiiProperty("value", "{\"primitive\":\"string\"}")] + string Value + { + get; + } + + [JsiiMethod("doThings", null, "[]")] + void DoThings(); + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceThatShouldNotBeADataTypeProxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceThatShouldNotBeADataTypeProxy.cs new file mode 100644 index 0000000000..2ee8dfa35d --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceThatShouldNotBeADataTypeProxy.cs @@ -0,0 +1,34 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + /// + /// Even though this interface has only properties, it is disqualified from being a datatype + /// because it inherits from an interface that is not a datatype. + /// + [JsiiTypeProxy(typeof(IIInterfaceThatShouldNotBeADataType), "jsii-calc.IInterfaceThatShouldNotBeADataType")] + internal sealed class IInterfaceThatShouldNotBeADataTypeProxy : DeputyBase, IIInterfaceThatShouldNotBeADataType + { + private IInterfaceThatShouldNotBeADataTypeProxy(ByRefValue reference): base(reference) + { + } + + [JsiiProperty("otherValue", "{\"primitive\":\"string\"}")] + public string OtherValue + { + get => GetInstanceProperty(); + } + + [JsiiProperty("value", "{\"primitive\":\"string\"}")] + public string Value + { + get => GetInstanceProperty(); + } + + [JsiiMethod("doThings", null, "[]")] + public void DoThings() + { + InvokeInstanceVoidMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceWithMethodsProxy.cs b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceWithMethodsProxy.cs new file mode 100644 index 0000000000..1c4c8947e2 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/dotnet/Amazon.JSII.Tests.CalculatorPackageId/Amazon/JSII/Tests/CalculatorNamespace/IInterfaceWithMethodsProxy.cs @@ -0,0 +1,24 @@ +using Amazon.JSII.Runtime.Deputy; + +namespace Amazon.JSII.Tests.CalculatorNamespace +{ + [JsiiTypeProxy(typeof(IIInterfaceWithMethods), "jsii-calc.IInterfaceWithMethods")] + internal sealed class IInterfaceWithMethodsProxy : DeputyBase, IIInterfaceWithMethods + { + private IInterfaceWithMethodsProxy(ByRefValue reference): base(reference) + { + } + + [JsiiProperty("value", "{\"primitive\":\"string\"}")] + public string Value + { + get => GetInstanceProperty(); + } + + [JsiiMethod("doThings", null, "[]")] + public void DoThings() + { + InvokeInstanceVoidMethod(new object[]{}); + } + } +} \ No newline at end of file diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java index 6f2ff3c467..aec0d95abc 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/$Module.java @@ -40,6 +40,8 @@ protected Class resolveClass(final String fqn) throws ClassNotFoundException case "jsii-calc.GiveMeStructs": return software.amazon.jsii.tests.calculator.GiveMeStructs.class; case "jsii-calc.IFriendlier": return software.amazon.jsii.tests.calculator.IFriendlier.class; case "jsii-calc.IFriendlyRandomGenerator": return software.amazon.jsii.tests.calculator.IFriendlyRandomGenerator.class; + case "jsii-calc.IInterfaceThatShouldNotBeADataType": return software.amazon.jsii.tests.calculator.IInterfaceThatShouldNotBeADataType.class; + case "jsii-calc.IInterfaceWithMethods": return software.amazon.jsii.tests.calculator.IInterfaceWithMethods.class; case "jsii-calc.IInterfaceWithProperties": return software.amazon.jsii.tests.calculator.IInterfaceWithProperties.class; case "jsii-calc.IInterfaceWithPropertiesExtension": return software.amazon.jsii.tests.calculator.IInterfaceWithPropertiesExtension.class; case "jsii-calc.IRandomNumberGenerator": return software.amazon.jsii.tests.calculator.IRandomNumberGenerator.class; diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceThatShouldNotBeADataType.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceThatShouldNotBeADataType.java new file mode 100644 index 0000000000..ce0b151763 --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceThatShouldNotBeADataType.java @@ -0,0 +1,34 @@ +package software.amazon.jsii.tests.calculator; + +/** + * Even though this interface has only properties, it is disqualified from being a datatype + * because it inherits from an interface that is not a datatype. + */ +@javax.annotation.Generated(value = "jsii-pacmak") +public interface IInterfaceThatShouldNotBeADataType extends software.amazon.jsii.JsiiSerializable, software.amazon.jsii.tests.calculator.IInterfaceWithMethods { + java.lang.String getOtherValue(); + + /** + * A proxy class which represents a concrete javascript instance of this type. + */ + final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements software.amazon.jsii.tests.calculator.IInterfaceThatShouldNotBeADataType { + protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + + @Override + public java.lang.String getOtherValue() { + return this.jsiiGet("otherValue", java.lang.String.class); + } + + @Override + public java.lang.String getValue() { + return this.jsiiGet("value", java.lang.String.class); + } + + @Override + public void doThings() { + this.jsiiCall("doThings", Void.class); + } + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceWithMethods.java b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceWithMethods.java new file mode 100644 index 0000000000..2dba1ed12b --- /dev/null +++ b/packages/jsii-pacmak/test/expected.jsii-calc/java/src/main/java/software/amazon/jsii/tests/calculator/IInterfaceWithMethods.java @@ -0,0 +1,26 @@ +package software.amazon.jsii.tests.calculator; + +@javax.annotation.Generated(value = "jsii-pacmak") +public interface IInterfaceWithMethods extends software.amazon.jsii.JsiiSerializable { + java.lang.String getValue(); + void doThings(); + + /** + * A proxy class which represents a concrete javascript instance of this type. + */ + final static class Jsii$Proxy extends software.amazon.jsii.JsiiObject implements software.amazon.jsii.tests.calculator.IInterfaceWithMethods { + protected Jsii$Proxy(final software.amazon.jsii.JsiiObject.InitializationMode mode) { + super(mode); + } + + @Override + public java.lang.String getValue() { + return this.jsiiGet("value", java.lang.String.class); + } + + @Override + public void doThings() { + this.jsiiCall("doThings", Void.class); + } + } +} diff --git a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst index 92792836ca..df7f522b5f 100644 --- a/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst +++ b/packages/jsii-pacmak/test/expected.jsii-calc/sphinx/jsii-calc.rst @@ -1556,6 +1556,97 @@ IFriendlyRandomGenerator (interface) :abstract: Yes +IInterfaceThatShouldNotBeADataType (interface) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: IInterfaceThatShouldNotBeADataType + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.IInterfaceThatShouldNotBeADataType; + + .. code-tab:: javascript + + // IInterfaceThatShouldNotBeADataType is an interface + + .. code-tab:: typescript + + import { IInterfaceThatShouldNotBeADataType } from 'jsii-calc'; + + + + Even though this interface has only properties, it is disqualified from being a datatype because it inherits from an interface that is not a datatype. + + + :extends: :py:class:`~jsii-calc.IInterfaceWithMethods`\ + + + .. py:attribute:: otherValue + + :type: string *(readonly)* *(abstract)* + + + .. py:method:: doThings() + + *Inherited from* :py:meth:`jsii-calc.IInterfaceWithMethods ` + + :abstract: Yes + + + .. py:attribute:: value + + *Inherited from* :py:attr:`jsii-calc.IInterfaceWithMethods ` + + :type: string *(readonly)* *(abstract)* + + +IInterfaceWithMethods (interface) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. py:class:: IInterfaceWithMethods + + **Language-specific names:** + + .. tabs:: + + .. code-tab:: c# + + using Amazon.JSII.Tests.CalculatorNamespace; + + .. code-tab:: java + + import software.amazon.jsii.tests.calculator.IInterfaceWithMethods; + + .. code-tab:: javascript + + // IInterfaceWithMethods is an interface + + .. code-tab:: typescript + + import { IInterfaceWithMethods } from 'jsii-calc'; + + + + + + .. py:attribute:: value + + :type: string *(readonly)* *(abstract)* + + + .. py:method:: doThings() + + :abstract: Yes + + IInterfaceWithProperties (interface) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/jsii/lib/assembler.ts b/packages/jsii/lib/assembler.ts index e12355e137..c289900ca9 100644 --- a/packages/jsii/lib/assembler.ts +++ b/packages/jsii/lib/assembler.ts @@ -11,6 +11,7 @@ import literate = require('./literate'); import { ProjectInfo } from './project-info'; import utils = require('./utils'); import { Validator } from './validator'; +import { NamedTypeReference, isInterfaceType } from 'jsii-spec'; // tslint:disable:no-var-requires Modules without TypeScript definitions const sortJson = require('sort-json'); @@ -23,7 +24,7 @@ const LOG = log4js.getLogger('jsii/assembler'); */ export class Assembler implements Emitter { private _diagnostics = new Array(); - private _deferred = new Array<() => void>(); + private _deferred = new Array(); private _types: { [fqn: string]: spec.Type }; /** @@ -75,9 +76,8 @@ export class Assembler implements Emitter { await this._visitNode(node.declarations[0]); } } - for (const deferred of this._deferred) { - deferred(); - } + + this.callDeferredsInOrder(); // Skip emitting if any diagnostic message is an error if (this._diagnostics.find(diag => diag.category === ts.DiagnosticCategory.Error) != null) { @@ -87,9 +87,8 @@ export class Assembler implements Emitter { try { return { diagnostics: this._diagnostics, emitSkipped: true }; } finally { - // Clearing ``this._diagnostics`` and ``this._deferred`` to allow contents to be garbage-collected. + // Clearing ``this._diagnostics`` to allow contents to be garbage-collected. delete this._diagnostics; - delete this._deferred; } } @@ -128,9 +127,8 @@ export class Assembler implements Emitter { // Clearing ``this._types`` to allow contents to be garbage-collected. delete this._types; - // Clearing ``this._diagnostics`` and ``this._deferred`` to allow contents to be garbage-collected. + // Clearing ``this._diagnostics`` to allow contents to be garbage-collected. delete this._diagnostics; - delete this._deferred; } async function _loadReadme(this: Assembler) { @@ -144,15 +142,45 @@ export class Assembler implements Emitter { } } + /** + * Defer a callback until a (set of) types are available + * + * This is a helper function around _defer() which encapsulates the _dereference + * action (which is basically the majority use case for _defer anyway). + * + * Will not invoke the function with any 'undefined's; an error will already have been emitted in + * that case anyway. + */ + // tslint:disable-next-line:max-line-length + private _deferUntilTypesAvailable(fqn: string, baseTypes: NamedTypeReference[], referencingNode: ts.Node, cb: (...xs: spec.Type[]) => void) { + // We can do this one eagerly + if (baseTypes.length === 0) { + cb(); + return; + } + + this._defer(fqn, baseTypes.map(x => x.fqn), () => { + const resolved = baseTypes.map(x => this._dereference(x, referencingNode)).filter(x => x !== undefined); + if (resolved.length > 0) { + return cb(...resolved as spec.Type[]); + } + }); + } + /** * Defer checks for after the program has been entirely processed; useful for verifying type references that may not * have been discovered yet, and verifying properties about them. * + * The callback is guaranteed to be executed only after all deferreds for all types in 'dependedFqns' have + * been executed. + * + * @param fqn FQN of the current type. + * @param deps List of FQNs of types this callback depends on. All deferreds for all * @param cb the function to be called in a deferred way. It will be bound with ``this``, so it can depend on using * ``this``. */ - private _defer(cb: () => void) { - this._deferred.push(cb.bind(this)); + private _defer(fqn: string, dependedFqns: string[], cb: () => void) { + this._deferred.push({ fqn, dependedFqns, cb: cb.bind(this) }); } /** @@ -299,9 +327,11 @@ export class Assembler implements Emitter { LOG.trace(`Processing class: ${colors.gray(namespace.join('.'))}.${colors.cyan(type.symbol.name)}`); } + const fqn = `${[this.projectInfo.name, ...namespace].join('.')}.${type.symbol.name}`; + const jsiiType: spec.ClassType = { assembly: this.projectInfo.name, - fqn: `${[this.projectInfo.name, ...namespace].join('.')}.${type.symbol.name}`, + fqn, kind: spec.TypeKind.Class, name: type.symbol.name, namespace: namespace.join('.') @@ -322,9 +352,8 @@ export class Assembler implements Emitter { `Base type of ${jsiiType.fqn} is not a named type (${spec.describeTypeReference(ref)})`); continue; } - this._defer(() => { - const deref = this._dereference(ref, base.symbol.valueDeclaration); - if (deref && !spec.isClassType(deref)) { + this._deferUntilTypesAvailable(fqn, [ref], base.symbol.valueDeclaration, (deref) => { + if (!spec.isClassType(deref)) { this._diagnostic(base.symbol.valueDeclaration, ts.DiagnosticCategory.Error, `Base type of ${jsiiType.fqn} is not a class (${spec.describeTypeReference(ref)})`); @@ -349,9 +378,8 @@ export class Assembler implements Emitter { `Interface of ${jsiiType.fqn} is not a named type (${spec.describeTypeReference(typeRef)})`); continue; } - this._defer(() => { - const deref = this._dereference(typeRef, expression); - if (deref && !spec.isInterfaceType(deref)) { + this._deferUntilTypesAvailable(fqn, [typeRef], expression, (deref) => { + if (!spec.isInterfaceType(deref)) { this._diagnostic(expression, ts.DiagnosticCategory.Error, `Implements clause of ${jsiiType.fqn} uses ${spec.describeTypeReference(typeRef)} as an interface`); @@ -410,16 +438,13 @@ export class Assembler implements Emitter { } } } else if (jsiiType.base) { - this._defer(() => { - const baseType = this._dereference(jsiiType.base!, type.symbol.valueDeclaration); - if (baseType) { - if (spec.isClassType(baseType)) { - jsiiType.initializer = baseType.initializer; - } else { - this._diagnostic(type.symbol.valueDeclaration, - ts.DiagnosticCategory.Error, - `Base type of ${jsiiType.fqn} (${jsiiType.base!.fqn}) is not a class`); - } + this._deferUntilTypesAvailable(fqn, [jsiiType.base!], type.symbol.valueDeclaration, (baseType) => { + if (spec.isClassType(baseType)) { + jsiiType.initializer = baseType.initializer; + } else { + this._diagnostic(type.symbol.valueDeclaration, + ts.DiagnosticCategory.Error, + `Base type of ${jsiiType.fqn} (${jsiiType.base!.fqn}) is not a class`); } }); } else { @@ -499,13 +524,16 @@ export class Assembler implements Emitter { LOG.trace(`Processing interface: ${colors.gray(namespace.join('.'))}.${colors.cyan(type.symbol.name)}`); } + const fqn = `${[this.projectInfo.name, ...namespace].join('.')}.${type.symbol.name}`; + const jsiiType: spec.InterfaceType = { assembly: this.projectInfo.name, - fqn: `${[this.projectInfo.name, ...namespace].join('.')}.${type.symbol.name}`, + fqn, kind: spec.TypeKind.Interface, name: type.symbol.name, namespace: namespace.join('.') }; + for (const base of (type.getBaseTypes() || [])) { const ref = await this._typeReference(base, type.symbol.valueDeclaration); if (!spec.isNamedTypeReference(ref)) { @@ -514,9 +542,8 @@ export class Assembler implements Emitter { `Base type of ${jsiiType.fqn} is not a named type (${spec.describeTypeReference(ref)})`); continue; } - this._defer(() => { - const baseType = this._dereference(ref, base.symbol.valueDeclaration); - if (baseType && !spec.isInterfaceType(baseType)) { + this._deferUntilTypesAvailable(fqn, [ref], base.symbol.valueDeclaration, (baseType) => { + if (!spec.isInterfaceType(baseType)) { // tslint:disable:max-line-length this._diagnostic(base.symbol.valueDeclaration, ts.DiagnosticCategory.Error, @@ -545,9 +572,20 @@ export class Assembler implements Emitter { `Ignoring un-handled ${ts.SyntaxKind[member.valueDeclaration.kind]} member`); } } - if ((jsiiType.methods || []).length === 0 && (jsiiType.properties || []).length > 0) { - jsiiType.datatype = true; - } + + // Calculate datatype based on the datatypeness of this interface and all of its parents + // To keep the spec minimal the actual values of the attribute are "true" or "undefined" (to represent "false"). + // tslint:disable-next-line:no-console + this._deferUntilTypesAvailable(fqn, jsiiType.interfaces || [], type.symbol.valueDeclaration, (...bases: spec.Type[]) => { + if ((jsiiType.methods || []).length === 0 && (jsiiType.properties || []).length > 0) { + jsiiType.datatype = true; + } + for (const base of bases) { + if (isInterfaceType(base) && !base.datatype) { + jsiiType.datatype = undefined; + } + } + }); return _sortMembers(this._visitDocumentation(type.symbol, jsiiType)); } @@ -780,6 +818,41 @@ export class Assembler implements Emitter { : { union: { types }, optional }; } } + + private callDeferredsInOrder() { + // Do a topological call order of all deferreds. + while (this._deferred.length > 0) { + // All fqns in dependency lists that don't have any pending + // deferreds themselves can be executed now, so are removed from + // dependency lists. + const pendingFqns = new Set(this._deferred.map(x => x.fqn)); + for (const deferred of this._deferred) { + restrictDependenciesTo(deferred, pendingFqns); + } + + // Invoke all deferreds with no more dependencies and remove them from the list. + let invoked = false; + for (let i = 0; i < this._deferred.length; i++) { + if (this._deferred[i].dependedFqns.length === 0) { + const deferred = this._deferred.splice(i, 1)[0]; + deferred.cb(); + invoked = true; + } + } + + if (!invoked) { + // Apparently we're stuck. Complain loudly. + throw new Error(`Could not invoke any more deferreds, cyclic dependency? Remaining: ${JSON.stringify(this._deferred, undefined, 2)}`); + } + } + + /** + * Retain only elements in the dependencyfqn that are also in the set + */ + function restrictDependenciesTo(def: DeferredRecord, fqns: Set) { + def.dependedFqns = def.dependedFqns.filter(fqns.has.bind(fqns)); + } + } } /** @@ -913,3 +986,26 @@ function _toDependencies(assemblies: ReadonlyArray): { [name: str } return result; } + +/** + * Deferred processing that needs to happen in a second, ordered pass + */ +interface DeferredRecord { + /** + * The FQN of the type the action will be executed on + */ + fqn: string; + + /** + * Dependency FQNs of the types that need to be processed before analysis. + * + * All deferred analysis actions for the types listed here must be complete + * before this analysis action can run. + */ + dependedFqns: string[]; + + /** + * Callback representing the action to run. + */ + cb: () => void; +} \ No newline at end of file