Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions packages/@jsii/python-runtime/src/jsii/_reference_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
92 changes: 92 additions & 0 deletions packages/@jsii/python-runtime/tests/test_compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions packages/jsii-calc/lib/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {}
}
Loading