def _find_object_dependency_cycles(ir): """Finds dependency cycles in types in the ir.""" dependencies, find_dependency_errors = _find_dependencies(ir) if find_dependency_errors: return find_dependency_errors errors = [] cycles = _find_cycles(dict(dependencies)) for cycle in cycles: # TODO(bolms): This lists the entire strongly-connected component in a # fairly arbitrary order. This is simple, and handles components that # aren't simple cycles, but may not be the most user-friendly way to # present this information. cycle_list = sorted(list(cycle)) node_object = ir_util.find_object(cycle_list[0], ir) error_group = [ error.error(cycle_list[0][0], node_object.source_location, "Dependency cycle\n" + node_object.name.name.text) ] for node in cycle_list[1:]: node_object = ir_util.find_object(node, ir) error_group.append( error.note(node[0], node_object.source_location, node_object.name.name.text)) errors.append(error_group) return errors
def _verify_requires_attribute_on_field(field, source_file_name, ir, errors): """Verifies that [requires] is valid on the given field.""" requires_attr = ir_util.get_attribute(field.attribute, attributes.REQUIRES) if not requires_attr: return if ir_util.field_is_virtual(field): field_expression_type = field.read_transform.type else: if not field.type.HasField("atomic_type"): errors.append([ error.error(source_file_name, requires_attr.source_location, "Attribute 'requires' is only allowed on integer, " "enumeration, or boolean fields, not arrays."), error.note(source_file_name, field.type.source_location, "Field type."), ]) return field_type = ir_util.find_object(field.type.atomic_type.reference, ir) assert field_type, "Field type should be non-None after name resolution." field_expression_type = ( type_check.unbounded_expression_type_for_physical_type(field_type)) if field_expression_type.WhichOneof("type") not in ( "integer", "enumeration", "boolean"): errors.append([error.error( source_file_name, requires_attr.source_location, "Attribute 'requires' is only allowed on integer, enumeration, or " "boolean fields.")])
def _set_expression_type_from_physical_type_reference(expression, type_reference, ir): """Sets the type of an expression to match a physical type.""" field_type = ir_util.find_object(type_reference, ir) assert field_type, "Field type should be non-None after name resolution." expression.type.CopyFrom( unbounded_expression_type_for_physical_type(field_type))
def _compute_constraints_of_field_reference(expression, ir): """Computes the constraints of a reference to a structure's field.""" field_path = expression.field_reference.path[-1] field = ir_util.find_object(field_path, ir) if isinstance(field, ir_pb2.Field) and ir_util.field_is_virtual(field): # References to virtual fields should have the virtual field's constraints # copied over. compute_constraints_of_expression(field.read_transform, ir) expression.type.CopyFrom(field.read_transform.type) return # Non-virtual non-integer fields do not (yet) have constraints. if expression.type.WhichOneof("type") == "integer": # TODO(bolms): These lines will need to change when support is added for # fixed-point types. expression.type.integer.modulus = "1" expression.type.integer.modular_value = "0" type_definition = ir_util.find_parent_object(field_path, ir) if isinstance(field, ir_pb2.Field): referrent_type = field.type else: referrent_type = field.physical_type_alias if referrent_type.HasField("size_in_bits"): type_size = ir_util.constant_value(referrent_type.size_in_bits) else: field_size = ir_util.constant_value(field.location.size) if field_size is None: type_size = None else: type_size = field_size * type_definition.addressable_unit assert referrent_type.HasField("atomic_type"), field assert not referrent_type.atomic_type.reference.canonical_name.module_file _set_integer_constraints_from_physical_type(expression, referrent_type, type_size)
def _type_check_constant_reference(expression, source_file_name, ir, errors): """Annotates the type of a constant reference.""" referred_name = expression.constant_reference.canonical_name referred_object = ir_util.find_object(referred_name, ir) if isinstance(referred_object, ir_pb2.EnumValue): expression.type.enumeration.name.CopyFrom( expression.constant_reference) del expression.type.enumeration.name.canonical_name.object_path[-1] elif isinstance(referred_object, ir_pb2.Field): if not ir_util.field_is_virtual(referred_object): errors.append([ error.error( source_file_name, expression.source_location, "Static references to physical fields are not allowed."), error.note( referred_name.module_file, referred_object.source_location, "{} is a physical field.".format( referred_name.object_path[-1])), ]) return _type_check_expression(referred_object.read_transform, referred_name.module_file, ir, errors) expression.type.CopyFrom(referred_object.read_transform.type) else: assert False, "Unexpected constant reference type."
def _check_that_array_base_types_are_fixed_size(type_ir, source_file_name, errors, ir): """Checks that the sizes of array elements are known at compile time.""" if type_ir.base_type.HasField("array_type"): # An array is fixed size if its base_type is fixed size and its array # dimension is constant. This function will be called again on the inner # array, and we do not want to cascade errors if the inner array's base_type # is not fixed size. The array dimensions are separately checked by # _check_that_inner_array_dimensions_are_constant, which will provide an # appropriate error message for that case. return assert type_ir.base_type.HasField("atomic_type") if type_ir.base_type.HasField("size_in_bits"): # If the base_type has a size_in_bits, then it is fixed size. return base_type = ir_util.find_object(type_ir.base_type.atomic_type.reference, ir) base_type_fixed_size = ir_util.get_integer_attribute( base_type.attribute, attributes.FIXED_SIZE) if base_type_fixed_size is None: errors.append([ error.error(source_file_name, type_ir.base_type.atomic_type.source_location, "Array elements must be fixed size.") ])
def _render_atomic_type_name(type_ir, ir, suffix=None): assert type_ir.HasField("atomic_type"), ( "_render_atomic_type_name() requires an atomic type") if not suffix: suffix = "" type_definition = ir_util.find_object(type_ir.atomic_type.reference, ir) if type_definition.name.is_anonymous: return "anonymous type" else: return "type '{}{}'".format(type_definition.name.name.text, suffix)
def _find_module_dependency_cycles(ir): """Finds dependency cycles in modules in the ir.""" dependencies = _find_module_import_dependencies(ir) cycles = _find_cycles(dict(dependencies)) errors = [] for cycle in cycles: cycle_list = sorted(list(cycle)) module = ir_util.find_object(cycle_list[0], ir) error_group = [ error.error(cycle_list[0][0], module.source_location, "Import dependency cycle\n" + module.source_file_name) ] for module_name in cycle_list[1:]: module = ir_util.find_object(module_name, ir) error_group.append( error.note(module_name[0], module.source_location, module.source_file_name)) errors.append(error_group) return errors
def _resolve_field_reference(field_reference, source_file_name, errors, ir): """Resolves the References inside of a FieldReference.""" if field_reference.path[-1].HasField("canonical_name"): # Already done. return previous_field = ir_util.find_object_or_none(field_reference.path[0], ir) previous_reference = field_reference.path[0] for ref in field_reference.path[1:]: while ir_util.field_is_virtual(previous_field): if (previous_field.read_transform.WhichOneof("expression") == "field_reference"): # Pass a separate error list into the recursive _resolve_field_reference # call so that only one copy of the error for a particular reference # will actually surface: in particular, the one that results from a # direct call from traverse_ir_top_down into _resolve_field_reference. new_errors = [] _resolve_field_reference( previous_field.read_transform.field_reference, previous_field.name.canonical_name.module_file, new_errors, ir) # If the recursive _resolve_field_reference was unable to resolve the # field, then bail. Otherwise we get a cascade of errors, where an # error in `x` leads to errors in anything trying to reach a member of # `x`. if not previous_field.read_transform.field_reference.path[ -1].HasField("canonical_name"): return previous_field = ir_util.find_object( previous_field.read_transform.field_reference.path[-1], ir) else: errors.append( noncomposite_subfield_error( source_file_name, previous_reference.source_location, previous_reference.source_name[0].text)) return if previous_field.type.WhichOneof("type") == "array_type": errors.append( array_subfield_error(source_file_name, previous_reference.source_location, previous_reference.source_name[0].text)) return assert previous_field.type.WhichOneof("type") == "atomic_type" member_name = ir_pb2.CanonicalName() member_name.CopyFrom( previous_field.type.atomic_type.reference.canonical_name) member_name.object_path.extend([ref.source_name[0].text]) previous_field = ir_util.find_object_or_none(member_name, ir) if previous_field is None: errors.append( missing_name_error(source_file_name, ref.source_name[0].source_location, ref.source_name[0].text)) return ref.canonical_name.CopyFrom(member_name) previous_reference = ref
def _field_needs_byte_order(field, type_definition, ir): """Returns true if the given field needs a byte_order attribute.""" if ir_util.field_is_virtual(field): # Virtual fields have no physical type, and thus do not need a byte order. return False field_type = ir_util.find_object( ir_util.get_base_type(field.type).atomic_type.reference.canonical_name, ir) assert field_type is not None assert field_type.addressable_unit != ir_pb2.TypeDefinition.NONE return field_type.addressable_unit != type_definition.addressable_unit
def test_explicit_size_too_small(self): ir = _make_ir_from_emb("bits Foo:\n" " 0 [+0] UInt:0 zero_bit\n") error_field = ir.module[0].type[0].structure.field[0] uint_type = ir_util.find_object(error_field.type.atomic_type.reference, ir) uint_requirements = ir_util.get_attribute(uint_type.attribute, attributes.STATIC_REQUIREMENTS) self.assertEqual([[ error.error("m.emb", error_field.source_location, "Requirements of UInt not met."), error.note("", uint_requirements.source_location, "Requirements specified here."), ]], error.filter_errors(constraints.check_constraints(ir)))
def test_explicit_size_too_big(self): ir = _make_ir_from_emb("struct Foo:\n" " 0 [+16] UInt:128 one_twenty_eight_bit\n" ' [byte_order: "LittleEndian"]\n') error_field = ir.module[0].type[0].structure.field[0] uint_type = ir_util.find_object(error_field.type.atomic_type.reference, ir) uint_requirements = ir_util.get_attribute(uint_type.attribute, attributes.STATIC_REQUIREMENTS) self.assertEqual([[ error.error("m.emb", error_field.source_location, "Requirements of UInt not met."), error.note("", uint_requirements.source_location, "Requirements specified here."), ]], error.filter_errors(constraints.check_constraints(ir)))
def _check_physical_type_requirements(type_ir, usage_source_location, size, ir, source_file_name): """Checks that the given atomic `type_ir` is allowed to be `size` bits.""" referenced_type_definition = ir_util.find_object( type_ir.atomic_type.reference, ir) # TODO(bolms): replace this with a check against an automatically-generated # `static_requirements` attribute on enum types. (The main problem is that # the generated attribute would have no source text, so there would be a crash # when trying to display the error.) if referenced_type_definition.HasField("enumeration"): if size is None: return [[ error.error( source_file_name, type_ir.source_location, "Enumeration {} cannot be placed in a dynamically-sized " "field.".format(_render_type(type_ir, ir))) ]] elif size < 1 or size > 64: return [[ error.error( source_file_name, type_ir.source_location, "Enumeration {} cannot be {} bits; enumerations must be between " "1 and 64 bits, inclusive.".format( _render_atomic_type_name(type_ir, ir), size)) ]] if size is None: bindings = {"$is_statically_sized": False} else: bindings = {"$is_statically_sized": True, "$static_size_in_bits": size} requires_attr = ir_util.get_attribute(referenced_type_definition.attribute, attributes.STATIC_REQUIREMENTS) if requires_attr and not ir_util.constant_value(requires_attr.expression, bindings): # TODO(bolms): Figure out a better way to build this error message. # The "Requirements specified here." message should print out the actual # source text of the requires attribute, so that should help, but it's still # a bit generic and unfriendly. return [[ error.error( source_file_name, usage_source_location, "Requirements of {} not met.".format( type_ir.atomic_type.reference.canonical_name. object_path[-1])), error.note( type_ir.atomic_type.reference.canonical_name.module_file, requires_attr.source_location, "Requirements specified here.") ]] return []
def _type_check_passed_parameters(atomic_type, ir, source_file_name, errors): """Checks the types of parameters to a parameterized physical type.""" referenced_type = ir_util.find_object(atomic_type.reference.canonical_name, ir) if (len(referenced_type.runtime_parameter) != len( atomic_type.runtime_parameter)): errors.append([ error.error( source_file_name, atomic_type.source_location, "Type {} requires {} parameter{}; {} parameter{} given.". format( referenced_type.name.name.text, len(referenced_type.runtime_parameter), "" if len(referenced_type.runtime_parameter) == 1 else "s", len(atomic_type.runtime_parameter), "" if len(atomic_type.runtime_parameter) == 1 else "s")), error.note( atomic_type.reference.canonical_name.module_file, referenced_type.source_location, "Definition of type {}.".format( referenced_type.name.name.text)) ]) return for i in range(len(referenced_type.runtime_parameter)): if referenced_type.runtime_parameter[i].type.WhichOneof( "type") not in ("integer", "boolean", "enumeration"): # _type_check_parameter will catch invalid parameter types at the # definition site; no need for another, probably-confusing error at any # usage sites. continue if (atomic_type.runtime_parameter[i].type.WhichOneof("type") != referenced_type.runtime_parameter[i].type.WhichOneof("type")): errors.append([ error.error( source_file_name, atomic_type.runtime_parameter[i].source_location, "Parameter {} of type {} must be {}, not {}.".format( i, referenced_type.name.name.text, _type_name_for_error_messages( referenced_type.runtime_parameter[i].type), _type_name_for_error_messages( atomic_type.runtime_parameter[i].type))), error.note( atomic_type.reference.canonical_name.module_file, referenced_type.runtime_parameter[i].source_location, "Parameter {} of {}.".format( i, referenced_type.name.name.text)) ])
def _compute_constant_value_of_constant_reference(expression, ir): referred_object = ir_util.find_object( expression.constant_reference.canonical_name, ir) if isinstance(referred_object, ir_pb2.EnumValue): compute_constraints_of_expression(referred_object.value, ir) assert ir_util.is_constant(referred_object.value) new_value = str(ir_util.constant_value(referred_object.value)) expression.type.enumeration.value = new_value elif isinstance(referred_object, ir_pb2.Field): assert ir_util.field_is_virtual(referred_object), ( "Non-virtual non-enum-value constant reference should have been caught " "in type_check.py") compute_constraints_of_expression(referred_object.read_transform, ir) expression.type.CopyFrom(referred_object.read_transform.type) else: assert False, "Unexpected constant reference type."
def _check_allowed_in_bits(type_ir, type_definition, source_file_name, ir, errors): if not type_ir.HasField("atomic_type"): return referenced_type_definition = ir_util.find_object( type_ir.atomic_type.reference, ir) if (type_definition.addressable_unit % referenced_type_definition.addressable_unit != 0): assert type_definition.addressable_unit == ir_pb2.TypeDefinition.BIT assert (referenced_type_definition.addressable_unit == ir_pb2.TypeDefinition.BYTE) errors.append([ error.error( source_file_name, type_ir.source_location, "Byte-oriented {} cannot be used in a bits field.".format( _render_type(type_ir, ir))) ])
def _check_constancy_of_constant_references(expression, source_file_name, errors, ir): """Checks that constant_references are constant.""" if expression.WhichOneof("expression") != "constant_reference": return # This is a bit of a hack: really, we want to know that the referred-to object # has no dependencies on any instance variables of its parent structure; i.e., # that its value does not depend on having a view of the structure. if not ir_util.is_constant_type(expression.type): referred_name = expression.constant_reference.canonical_name referred_object = ir_util.find_object(referred_name, ir) errors.append([ error.error(source_file_name, expression.source_location, "Static references must refer to constants."), error.note( referred_name.module_file, referred_object.source_location, "{} is not constant.".format(referred_name.object_path[-1])) ])
def _type_check_local_reference(expression, ir, errors): """Annotates the type of a local reference.""" referrent = ir_util.find_object(expression.field_reference.path[-1], ir) assert referrent, "Local reference should be non-None after name resolution." if isinstance(referrent, ir_pb2.RuntimeParameter): parameter = referrent _set_expression_type_from_physical_type_reference( expression, parameter.physical_type_alias.atomic_type.reference, ir) return field = referrent if ir_util.field_is_virtual(field): _type_check_expression(field.read_transform, expression.field_reference.path[0], ir, errors) expression.type.CopyFrom(field.read_transform.type) return if not field.type.HasField("atomic_type"): expression.type.opaque.CopyFrom(ir_pb2.OpaqueType()) else: _set_expression_type_from_physical_type_reference( expression, field.type.atomic_type.reference, ir)
def _check_type_requirements_for_field(type_ir, type_definition, field, ir, source_file_name, errors): """Checks that the `requires` attribute of each field's type is fulfilled.""" if not type_ir.HasField("atomic_type"): return if field.type.HasField("atomic_type"): field_min_size = (int(field.location.size.type.integer.minimum_value) * type_definition.addressable_unit) field_max_size = (int(field.location.size.type.integer.maximum_value) * type_definition.addressable_unit) field_is_atomic = True else: field_is_atomic = False if type_ir.HasField("size_in_bits"): element_size = ir_util.constant_value(type_ir.size_in_bits) else: element_size = None referenced_type_definition = ir_util.find_object( type_ir.atomic_type.reference, ir) type_is_anonymous = referenced_type_definition.name.is_anonymous type_size_attr = ir_util.get_attribute( referenced_type_definition.attribute, attributes.FIXED_SIZE) if type_size_attr: type_size = ir_util.constant_value(type_size_attr.expression) else: type_size = None if (element_size is not None and type_size is not None and element_size != type_size): errors.append([ error.error( source_file_name, type_ir.size_in_bits.source_location, "Explicit size of {} bits does not match fixed size ({} bits) of " "{}.".format(element_size, type_size, _render_atomic_type_name(type_ir, ir))), error.note( type_ir.atomic_type.reference.canonical_name.module_file, type_size_attr.source_location, "Size specified here.") ]) return # If the type had no size specifier (the ':32' in 'UInt:32'), but the type is # fixed size, then continue as if the type's size were explicitly stated. if element_size is None: element_size = type_size # TODO(bolms): When the full dynamic size expression for types is generated, # add a check that dynamically-sized types can, at least potentially, fit in # their fields. if field_is_atomic and element_size is not None: # If the field has a fixed size, and the (atomic) type contained therein is # also fixed size, then the sizes should match. # # TODO(bolms): Maybe change the case where the field is bigger than # necessary into a warning? if (field_max_size == field_min_size and (element_size > field_max_size or (element_size < field_min_size and not type_is_anonymous))): errors.append([ error.error( source_file_name, type_ir.source_location, "Fixed-size {} cannot be placed in field of size {} bits; " "requires {} bits.".format(_render_type(type_ir, ir), field_max_size, element_size)) ]) return elif element_size > field_max_size: errors.append([ error.error( source_file_name, type_ir.source_location, "Field of maximum size {} bits cannot hold fixed-size {}, which " "requires {} bits.".format(field_max_size, _render_type(type_ir, ir), element_size)) ]) return # If we're here, then field/type sizes are consistent. if (element_size is None and field_is_atomic and field_min_size == field_max_size): # From here down, we just use element_size. element_size = field_min_size errors.extend( _check_physical_type_requirements(type_ir, field.source_location, element_size, ir, source_file_name))
def _add_write_method(field, ir): """Adds an appropriate write_method to field, if applicable. Currently, the "alias" write_method will be added for virtual fields of the form `let v = some_field_reference` when `some_field_reference` is a physical field or a writeable alias. The "physical" write_method will be added for physical fields. The "transform" write_method will be added when the virtual field's value is an easily-invertible function of a single writeable field. All other fields will have the "read_only" write_method; i.e., they will not be writeable. Arguments: field: an ir_pb2.Field to which to add a write_method. ir: The IR in which to look up field_references. Returns: None """ if field.HasField("write_method"): # Do not recompute anything. return if not ir_util.field_is_virtual(field): # If the field is not virtual, writes are physical. field.write_method.physical = True return # A virtual field cannot be a direct alias if it has an additional # requirement. requires_attr = ir_util.get_attribute(field.attribute, attributes.REQUIRES) if (field.read_transform.WhichOneof("expression") != "field_reference" or requires_attr is not None): inverse = _invert_expression(field.read_transform, ir) if inverse: field_reference, function_body = inverse referenced_field = ir_util.find_object( field_reference.field_reference.path[-1], ir) if not isinstance(referenced_field, ir_pb2.Field): reference_is_read_only = True else: _add_write_method(referenced_field, ir) reference_is_read_only = referenced_field.write_method.read_only if not reference_is_read_only: field.write_method.transform.destination.CopyFrom( field_reference.field_reference) field.write_method.transform.function_body.CopyFrom(function_body) else: # If the virtual field's expression is invertible, but its target field # is read-only, it is also read-only. field.write_method.read_only = True else: # If the virtual field's expression is not invertible, it is # read-only. field.write_method.read_only = True return referenced_field = ir_util.find_object( field.read_transform.field_reference.path[-1], ir) if not isinstance(referenced_field, ir_pb2.Field): # If the virtual field aliases a non-field (i.e., a parameter), it is # read-only. field.write_method.read_only = True return _add_write_method(referenced_field, ir) if referenced_field.write_method.read_only: # If the virtual field directly aliases a read-only field, it is read-only. field.write_method.read_only = True return # Otherwise, it can be written as a direct alias. field.write_method.alias.CopyFrom( field.read_transform.field_reference)
def _compute_constraints_of_existence_function(expression, ir): """Computes the constraints of a $has(field) expression.""" field_path = expression.function.args[0].field_reference.path[-1] field = ir_util.find_object(field_path, ir) compute_constraints_of_expression(field.existence_condition, ir) expression.type.CopyFrom(field.existence_condition.type)
def test_find_object(self): ir = ir_pb2.EmbossIr.from_json("""{ "module": [ { "type": [ { "structure": { "field": [ { "name": { "name": { "text": "field" }, "canonical_name": { "module_file": "test.emb", "object_path": [ "Foo", "field" ] } } } ] }, "name": { "name": { "text": "Foo" }, "canonical_name": { "module_file": "test.emb", "object_path": [ "Foo" ] } }, "runtime_parameter": [ { "name": { "name": { "text": "parameter" }, "canonical_name": { "module_file": "test.emb", "object_path": [ "Foo", "parameter" ] } } } ] }, { "enumeration": { "value": [ { "name": { "name": { "text": "QUX" }, "canonical_name": { "module_file": "test.emb", "object_path": [ "Bar", "QUX" ] } } } ] }, "name": { "name": { "text": "Bar" }, "canonical_name": { "module_file": "test.emb", "object_path": [ "Bar" ] } } } ], "source_file_name": "test.emb" }, { "type": [ { "external": {}, "name": { "name": { "text": "UInt" }, "canonical_name": { "module_file": "", "object_path": [ "UInt" ] } } } ], "source_file_name": "" } ] }""") # Test that find_object works with any of its four "name" types. canonical_name_of_foo = ir_pb2.CanonicalName(module_file="test.emb", object_path=["Foo"]) self.assertEqual( ir.module[0].type[0], ir_util.find_object( ir_pb2.Reference(canonical_name=canonical_name_of_foo), ir)) self.assertEqual( ir.module[0].type[0], ir_util.find_object( ir_pb2.NameDefinition(canonical_name=canonical_name_of_foo), ir)) self.assertEqual(ir.module[0].type[0], ir_util.find_object(canonical_name_of_foo, ir)) self.assertEqual(ir.module[0].type[0], ir_util.find_object(("test.emb", "Foo"), ir)) # Test that find_object works with objects other than structs. self.assertEqual(ir.module[0].type[1], ir_util.find_object(("test.emb", "Bar"), ir)) self.assertEqual(ir.module[1].type[0], ir_util.find_object(("", "UInt"), ir)) self.assertEqual(ir.module[0].type[0].structure.field[0], ir_util.find_object(("test.emb", "Foo", "field"), ir)) self.assertEqual( ir.module[0].type[0].runtime_parameter[0], ir_util.find_object(("test.emb", "Foo", "parameter"), ir)) self.assertEqual(ir.module[0].type[1].enumeration.value[0], ir_util.find_object(("test.emb", "Bar", "QUX"), ir)) self.assertEqual(ir.module[0], ir_util.find_object(("test.emb", ), ir)) self.assertEqual(ir.module[1], ir_util.find_object(("", ), ir)) # Test searching for non-present objects. self.assertIsNone(ir_util.find_object_or_none(("not_test.emb", ), ir)) self.assertIsNone( ir_util.find_object_or_none(("test.emb", "NotFoo"), ir)) self.assertIsNone( ir_util.find_object_or_none(("test.emb", "Foo", "not_field"), ir)) self.assertIsNone( ir_util.find_object_or_none( ("test.emb", "Foo", "field", "no_subfield"), ir)) self.assertIsNone( ir_util.find_object_or_none(("test.emb", "Bar", "NOT_QUX"), ir)) self.assertIsNone( ir_util.find_object_or_none( ("test.emb", "Bar", "QUX", "no_subenum"), ir)) # Test that find_parent_object works with any of its four "name" types. self.assertEqual( ir.module[0], ir_util.find_parent_object( ir_pb2.Reference(canonical_name=canonical_name_of_foo), ir)) self.assertEqual( ir.module[0], ir_util.find_parent_object( ir_pb2.NameDefinition(canonical_name=canonical_name_of_foo), ir)) self.assertEqual(ir.module[0], ir_util.find_parent_object(canonical_name_of_foo, ir)) self.assertEqual(ir.module[0], ir_util.find_parent_object(("test.emb", "Foo"), ir)) # Test that find_parent_object works with objects other than structs. self.assertEqual(ir.module[0], ir_util.find_parent_object(("test.emb", "Bar"), ir)) self.assertEqual(ir.module[1], ir_util.find_parent_object(("", "UInt"), ir)) self.assertEqual( ir.module[0].type[0], ir_util.find_parent_object(("test.emb", "Foo", "field"), ir)) self.assertEqual( ir.module[0].type[1], ir_util.find_parent_object(("test.emb", "Bar", "QUX"), ir))