diff --git a/packages/@jsii/python-runtime/src/jsii/_reference_map.py b/packages/@jsii/python-runtime/src/jsii/_reference_map.py index 8d5a78ec89..e10db5ec09 100644 --- a/packages/@jsii/python-runtime/src/jsii/_reference_map.py +++ b/packages/@jsii/python-runtime/src/jsii/_reference_map.py @@ -142,7 +142,7 @@ def resolve(self, kernel, ref): except KeyError: pass - # If we got to this point, then we didn't have a referene for this, in that case + # If we got to this point, then we didn't have a reference for this, in that case # we want to create a new instance, but we need to create it in such a way that # we don't try to recreate the type inside of the JSII interface. class_fqn = ref.ref.rsplit("@", 1)[0] @@ -242,7 +242,16 @@ def resolve_id(self, id: str) -> Any: return self._refs[id] def build_interface_proxies_for_ref(self, ref: ObjRef) -> List[Any]: - ifaces = [_obtain_interface(fqn) for fqn in ref.interfaces or []] + fqns = ref.interfaces or [] + for fqn in fqns: + if fqn not in _data_types and fqn not in _interfaces: + _try_import_type_module(fqn) + + # A struct FQN can show up in the ref.interfaces when a value is typed as + # a union of a behavioral interface and a struct (e.g. foo: IResolvable | SomeProperty + # that appears on many L1 constructs). Skip structs instead of treating them + # as an unknown behavioral interface. + ifaces = [_obtain_interface(fqn) for fqn in fqns if fqn not in _data_types] classes = [iface.__jsii_proxy_class__() for iface in ifaces] # If there's no classes, use an Opaque reference to make sure the diff --git a/packages/@jsii/python-runtime/tests/test_compliance.py b/packages/@jsii/python-runtime/tests/test_compliance.py index d4f3682626..e92955b15d 100644 --- a/packages/@jsii/python-runtime/tests/test_compliance.py +++ b/packages/@jsii/python-runtime/tests/test_compliance.py @@ -87,10 +87,13 @@ ) from jsii_calc.submodule.isolated import Kwargs from jsii_calc.submodule.child import SomeEnum +from jsii_calc.union import Resolvable from scope.jsii_calc_lib import IFriendly, EnumFromScopedModule, Number from scope.jsii_calc_lib.custom_submodule_name import IReflectable, ReflectableEntry from scope.jsii_calc_lib.deprecation_removal import InterfaceFactory +from jsii._reference_map import InterfaceDynamicProxy + # Note: The names of these test functions have been chosen to map as closely to the # Java Compliance tests as possible. # Note: While we could write more expressive and better tests using the functionality @@ -1447,6 +1450,95 @@ def get(self, _ref, _name): assert isinstance(result, _reference_map._data_types[struct_fqn]) +def test_known_class_ref_with_struct_in_interfaces_resolves(): + """ + Verifies resolving an object reference whose primary type is a known + class, but whose ``interfaces`` list contains an unrelated struct + FQN, does not raise an Unknown interface exception. And that the known + class instance ends up as the proxy's primary delegate. + + This shape occurs for properties typed as a union of a behavioral + interface and a struct, such as ``ConsumesUnion.union_property`` + (``IResolvable | UnionResolvableStruct`` below), which mirrors AWS CDK + shapes like ``CfnLaunchTemplate.launchTemplateData: IResolvable | + LaunchTemplateDataProperty``. + + The shape is produced by the kernel as follows: + + - The kernel tries each union member's serializer in a fixed + priority order and uses the first one that doesn't throw an exception. + + - SerializationClass.Void, + - SerializationClass.Date, + - SerializationClass.Scalar, + - SerializationClass.Json, + - SerializationClass.Enum, + - SerializationClass.Array, + - SerializationClass.Map, + - SerializationClass.Struct, + - SerializationClass.ReferenceType, + - SerializationClass.Any, + + ``Struct`` serialization checks that the value is a non-null, + non-array, non-``Date`` object. Any object value matching that + criteria is serialized as a struct even if the real type is actually + the ``interface`` half of the union. From the example above, setting + the ``launchTemplateData`` property to ``Fn.condition_if`` sets the true + class as ``Intrinsic`` which implements the ``IResolvable`` interface. + + - When Struct.serialize calls registerObject(value, "Object", ["...SomeDataProperty"]), + and the value was already registered under it's true class (``Intrinsic``), the identity + cache merges interfaces into the existing entry instead of creating a new one. That is + how a reference whose primary type is a known class ends up with a unrelated struct + FQN riding along in ``ref.interfaces``. + + class_fqn='aws-cdk-lib.Intrinsic' ref.interfaces=['aws-cdk-lib.ICfnRuleConditionExpression'] + ... + class_fqn='aws-cdk-lib.Intrinsic' ref.interfaces=['aws-cdk-lib.ICfnRuleConditionExpression', 'aws-cdk-lib.aws_ec2.CfnLaunchTemplate.LaunchTemplateDataProperty'] + + ``resolve()``'s "known class" branch (``class_fqn in _types``) used to + invoke ``_obtain_interface`` for every item in ``ref.interfaces``. If + ``ref.interfaces`` contained an entry for a struct registered in + ``_data_types``, then ``_obtain_interface`` would be unable to find + the interace and raise a ``ValueError``. To avoid the issue, the logic + now filters out any FQN in the ``_data_types`` map before invoking + ``_obtain_interface``. + + This test simulates that exact shape with a synthetic ``ObjRef`` (a known + class FQN as the primary type, an unrelated struct FQN in ``interfaces``) + and checks not just that resolution succeeds, but that it produces the + *correct* result: the known ``Resolvable`` instance as the proxy's + primary delegate, and the struct FQN represented by an opaque fallback + delegate rather than being dropped, misidentified, or raising. + """ + from jsii import _reference_map + from jsii._kernel.types import ObjRef + + # FQNs of ConsumesUnion.union_property's `IResolvable | UnionResolvableStruct` + # union members (see packages/jsii-calc/lib/union.ts). + class_fqn = "jsii-calc.union.Resolvable" + struct_fqn = "jsii-calc.union.UnionResolvableStruct" + + # Simulate the kernel returning a reference to a known Resolvable instance + # that also lists the struct's FQN in its interfaces -- the shape produced + # for union-typed values. + ref = ObjRef(ref=f"{class_fqn}@90127", interfaces=[struct_fqn]) + + # This must NOT raise: the struct FQN is recognized and skipped rather + # than treated as an (unknown) behavioral interface. + result = _reference_map.resolve_reference(jsii.kernel, ref) + assert result is not None + assert isinstance(result, InterfaceDynamicProxy) + + # The known Resolvable instance should be the proxy's + # primary delegate, with the struct FQN represented as + # an opaque fallback rather than an (unknown) behavioral + # interface. + assert isinstance(result._delegates[0], Resolvable) + assert isinstance(result._delegates[1], _reference_map.Opaque) + assert result._delegates[1].__jsii_ref__ == ref + + def test_stripped_deprecated_member_can_be_received(): assert InterfaceFactory.create() is not None diff --git a/packages/jsii-calc/lib/union.ts b/packages/jsii-calc/lib/union.ts index ef4bf22b8f..692ffb8cf1 100644 --- a/packages/jsii-calc/lib/union.ts +++ b/packages/jsii-calc/lib/union.ts @@ -12,10 +12,16 @@ export class Resolvable implements IResolvable { } } +export interface UnionResolvableStruct { + readonly value: string; +} + export class ConsumesUnion { public static unionType(param: IResolvable | Resolvable | IFriendly) { void param; } + public unionProperty?: IResolvable | UnionResolvableStruct; + private constructor() {} }