Skip to content

Commit 2399608

Browse files
authored
fix(java): invalid code when multi-inheriting optional properties (#2591)
The generated code for interfaces includes default implementations for optional properties (since adding a new optional property is a non breaking change in TypeScript, this allows us to preserve this property in Java, too). However, if two distinct super-interfaces declare the same property, the child inherits two unrelated default implementations for the same method, which results in dispatch ambiguity that is illegal. In order to remove the ambiguity, a new default implementation must be provided on the child interface, so that the dispatch is unambiguous again. Fixes #22556 --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent ead4ffa commit 2399608

File tree

13 files changed

+1577
-3
lines changed

13 files changed

+1577
-3
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// This is half of the contraption, the rest is in jsii-calc
2+
//
3+
// @see https://github.com/aws/jsii/issues/2256
4+
5+
/**
6+
* This struct is intentionally private. Any type that implements it will get
7+
* a copy of it's properties hoisted in by jsii.
8+
*/
9+
interface InternalDiamondTip {
10+
readonly hoistedTop?: string;
11+
}
12+
13+
export interface DiamondLeft extends InternalDiamondTip {
14+
readonly left?: number;
15+
}
16+
17+
export interface DiamondRight extends InternalDiamondTip {
18+
readonly right?: boolean;
19+
}

packages/@scope/jsii-calc-lib/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,4 @@ export interface IThreeLevelsInterface extends base.IBaseInterface {
110110
}
111111

112112
export * as submodule from './submodule';
113+
export * from './duplicate-inherited-prop';

packages/@scope/jsii-calc-lib/test/assembly.jsii

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,102 @@
133133
}
134134
},
135135
"types": {
136+
"@scope/jsii-calc-lib.DiamondLeft": {
137+
"assembly": "@scope/jsii-calc-lib",
138+
"datatype": true,
139+
"docs": {
140+
"stability": "deprecated"
141+
},
142+
"fqn": "@scope/jsii-calc-lib.DiamondLeft",
143+
"kind": "interface",
144+
"locationInModule": {
145+
"filename": "lib/duplicate-inherited-prop.ts",
146+
"line": 13
147+
},
148+
"name": "DiamondLeft",
149+
"properties": [
150+
{
151+
"abstract": true,
152+
"docs": {
153+
"stability": "deprecated"
154+
},
155+
"immutable": true,
156+
"locationInModule": {
157+
"filename": "lib/duplicate-inherited-prop.ts",
158+
"line": 10
159+
},
160+
"name": "hoistedTop",
161+
"optional": true,
162+
"type": {
163+
"primitive": "string"
164+
}
165+
},
166+
{
167+
"abstract": true,
168+
"docs": {
169+
"stability": "deprecated"
170+
},
171+
"immutable": true,
172+
"locationInModule": {
173+
"filename": "lib/duplicate-inherited-prop.ts",
174+
"line": 14
175+
},
176+
"name": "left",
177+
"optional": true,
178+
"type": {
179+
"primitive": "number"
180+
}
181+
}
182+
]
183+
},
184+
"@scope/jsii-calc-lib.DiamondRight": {
185+
"assembly": "@scope/jsii-calc-lib",
186+
"datatype": true,
187+
"docs": {
188+
"stability": "deprecated"
189+
},
190+
"fqn": "@scope/jsii-calc-lib.DiamondRight",
191+
"kind": "interface",
192+
"locationInModule": {
193+
"filename": "lib/duplicate-inherited-prop.ts",
194+
"line": 17
195+
},
196+
"name": "DiamondRight",
197+
"properties": [
198+
{
199+
"abstract": true,
200+
"docs": {
201+
"stability": "deprecated"
202+
},
203+
"immutable": true,
204+
"locationInModule": {
205+
"filename": "lib/duplicate-inherited-prop.ts",
206+
"line": 10
207+
},
208+
"name": "hoistedTop",
209+
"optional": true,
210+
"type": {
211+
"primitive": "string"
212+
}
213+
},
214+
{
215+
"abstract": true,
216+
"docs": {
217+
"stability": "deprecated"
218+
},
219+
"immutable": true,
220+
"locationInModule": {
221+
"filename": "lib/duplicate-inherited-prop.ts",
222+
"line": 18
223+
},
224+
"name": "right",
225+
"optional": true,
226+
"type": {
227+
"primitive": "boolean"
228+
}
229+
}
230+
]
231+
},
136232
"@scope/jsii-calc-lib.EnumFromScopedModule": {
137233
"assembly": "@scope/jsii-calc-lib",
138234
"docs": {
@@ -776,5 +872,5 @@
776872
}
777873
},
778874
"version": "0.0.0",
779-
"fingerprint": "7STgxc8/b+pajng4R4fEptbcOF2aQJBiQkO4bMAWpI4="
875+
"fingerprint": "YlmxPtOusVTgKe2YUzZDpB4UnmnU3zZ0UUdXwSDcgOo="
780876
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { DiamondLeft, DiamondRight } from '@scope/jsii-calc-lib';
2+
3+
// This is half of the contraption, the rest is in @scope/jsii-calc-lib.
4+
//
5+
// @see https://github.com/aws/jsii/issues/2256
6+
7+
export interface DiamondBottom extends DiamondLeft, DiamondRight {
8+
readonly bottom?: Date;
9+
}

packages/jsii-calc/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './calculator';
22
export * from './compliance';
33
export * from './date';
44
export * from './documented';
5+
export * from './duplicate-inherited-prop';
56
export * from './erasures';
67
export * from './nested-class';
78
export * from './stability';

packages/jsii-calc/test/assembly.jsii

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@
209209
"jsii-calc.submodule": {
210210
"locationInModule": {
211211
"filename": "lib/index.ts",
212-
"line": 10
212+
"line": 11
213213
},
214214
"readme": {
215215
"markdown": "Read you, read me\n=================\n\nThis is the readme of the `jsii-calc.submodule` module.\n"
@@ -3821,6 +3821,42 @@
38213821
}
38223822
]
38233823
},
3824+
"jsii-calc.DiamondBottom": {
3825+
"assembly": "jsii-calc",
3826+
"datatype": true,
3827+
"docs": {
3828+
"stability": "stable"
3829+
},
3830+
"fqn": "jsii-calc.DiamondBottom",
3831+
"interfaces": [
3832+
"@scope/jsii-calc-lib.DiamondLeft",
3833+
"@scope/jsii-calc-lib.DiamondRight"
3834+
],
3835+
"kind": "interface",
3836+
"locationInModule": {
3837+
"filename": "lib/duplicate-inherited-prop.ts",
3838+
"line": 7
3839+
},
3840+
"name": "DiamondBottom",
3841+
"properties": [
3842+
{
3843+
"abstract": true,
3844+
"docs": {
3845+
"stability": "stable"
3846+
},
3847+
"immutable": true,
3848+
"locationInModule": {
3849+
"filename": "lib/duplicate-inherited-prop.ts",
3850+
"line": 8
3851+
},
3852+
"name": "bottom",
3853+
"optional": true,
3854+
"type": {
3855+
"primitive": "date"
3856+
}
3857+
}
3858+
]
3859+
},
38243860
"jsii-calc.DiamondInheritanceBaseLevelStruct": {
38253861
"assembly": "jsii-calc",
38263862
"datatype": true,
@@ -14441,5 +14477,5 @@
1444114477
}
1444214478
},
1444314479
"version": "3.20.120",
14444-
"fingerprint": "DELAKIDgUwCEVXhZflQBdllcDRnscledlYW0UXrjzbM="
14480+
"fingerprint": "DPUvV1cBxUeRNl5GK1DVzpU7/802/Hb8znpxKRttRUk="
1444514481
}

packages/jsii-pacmak/lib/targets/java.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,8 @@ class JavaGenerator extends Generator {
833833
}
834834

835835
protected onEndInterface(ifc: spec.InterfaceType) {
836+
this.emitMultiplyInheritedOptionalProperties(ifc);
837+
836838
if (ifc.datatype) {
837839
this.emitDataType(ifc);
838840
} else {
@@ -883,6 +885,9 @@ class JavaGenerator extends Generator {
883885
this.addJavaDocs(prop);
884886
this.emitStabilityAnnotations(prop);
885887
if (prop.optional) {
888+
if (prop.overrides) {
889+
this.code.line('@Override');
890+
}
886891
this.code.openBlock(`default ${getterType} get${propName}()`);
887892
this.code.line('return null;');
888893
this.code.closeBlock();
@@ -896,6 +901,9 @@ class JavaGenerator extends Generator {
896901
this.code.line();
897902
this.addJavaDocs(prop);
898903
if (prop.optional) {
904+
if (prop.overrides) {
905+
this.code.line('@Override');
906+
}
899907
this.code.line('@software.amazon.jsii.Optional');
900908
this.code.openBlock(
901909
`default void set${propName}(final ${type} value)`,
@@ -911,6 +919,61 @@ class JavaGenerator extends Generator {
911919
}
912920
}
913921

922+
/**
923+
* Emits a local default implementation for optional properties inherited from
924+
* multiple distinct parent types. This remvoes the default method dispatch
925+
* ambiguity that would otherwise exist.
926+
*
927+
* @param ifc the interface to be processed.
928+
929+
*
930+
* @see https://github.com/aws/jsii/issues/2256
931+
*/
932+
private emitMultiplyInheritedOptionalProperties(ifc: spec.InterfaceType) {
933+
if (ifc.interfaces == null || ifc.interfaces.length <= 1) {
934+
// Nothing to do if we don't have parent interfaces, or if we have exactly one
935+
return;
936+
}
937+
const inheritedOptionalProps = ifc.interfaces
938+
.map(allOptionalProps.bind(this))
939+
// Calculate how many direct parents brought a given optional property
940+
.reduce((histogram, entry) => {
941+
for (const [name, spec] of Object.entries(entry)) {
942+
histogram[name] = histogram[name] ?? { spec, count: 0 };
943+
histogram[name].count += 1;
944+
}
945+
return histogram;
946+
}, {} as Record<string, { readonly spec: spec.Property; count: number }>);
947+
948+
const localProps = new Set(ifc.properties?.map((prop) => prop.name) ?? []);
949+
for (const { spec, count } of Object.values(inheritedOptionalProps)) {
950+
if (count < 2 || localProps.has(spec.name)) {
951+
continue;
952+
}
953+
this.onInterfaceProperty(ifc, spec);
954+
}
955+
956+
function allOptionalProps(this: JavaGenerator, fqn: string) {
957+
const type = this.findType(fqn) as spec.InterfaceType;
958+
const result: Record<string, spec.Property> = {};
959+
for (const prop of type.properties ?? []) {
960+
// Adding artifical "overrides" here for code-gen quality's sake.
961+
result[prop.name] = { ...prop, overrides: type.fqn };
962+
}
963+
// Include optional properties of all super interfaces in the result
964+
for (const base of type.interfaces ?? []) {
965+
for (const [name, prop] of Object.entries(
966+
allOptionalProps.call(this, base),
967+
)) {
968+
if (!(name in result)) {
969+
result[name] = prop;
970+
}
971+
}
972+
}
973+
return result;
974+
}
975+
}
976+
914977
private emitPackageInfo(mod: spec.Assembly) {
915978
if (!mod.docs) {
916979
return;

0 commit comments

Comments
 (0)