def compare(self): # 1. If original service is None, then a new service is added. if self.service_original is None: FindingContainer.addFinding( category=FindingCategory.SERVICE_ADDITION, location=f"{self.service_update.proto_file_name} Line: {self.service_update.source_code_line}", message=f"A new service {self.service_update.name} is added.", actionable=False, ) return # 2. If updated service is None, then the original service is removed. if self.service_update is None: FindingContainer.addFinding( category=FindingCategory.SERVICE_REMOVAL, location=f"{self.service_original.proto_file_name} Line: {self.service_original.source_code_line}", message=f"A service {self.service_original.name} is removed", actionable=True, ) return self.messages_map_original = self.service_original.messages_map self.messages_map_update = self.service_update.messages_map # 3. Check the methods list self._compareRpcMethods( self.service_original, self.service_update, self.messages_map_original, self.messages_map_update, )
def _compare(self, message_original, message_update): # 1. If original message is None, then a new message is added. if self.message_original is None: msg = 'A new message {} is added.'.format(self.message_update.name) FindingContainer.addFinding(FindingCategory.MESSAGE_ADDITION, "", msg, False) return # 2. If updated message is None, then the original message is removed. if self.message_update is None: msg = 'A message {} is removed'.format(self.message_original.name) FindingContainer.addFinding(FindingCategory.MESSAGE_REMOVAL, "", msg, True) return # 3. Check breaking changes in each fields. Note: Fields are identified by number, not by name. # Descriptor.fields_by_number (dict int -> FieldDescriptor) indexed by number. if message_original.fields_by_number or message_update.fields_by_number: self._compareNestedFields(message_original.fields_by_number, message_update.fields_by_number) # 4. Check breaking changes in nested message. # Descriptor.nested_types_by_name (dict str -> Descriptor) indexed by name. # Recursively call _compare for nested message type comparison. if (message_original.nested_types_by_name or message_update.nested_types_by_name): self._compareNestedMessages(message_original.nested_types_by_name, message_update.nested_types_by_name)
def _compare(self, message_original, message_update): # 1. If original message is None, then a new message is added. if message_original is None: FindingContainer.addFinding( category=FindingCategory.MESSAGE_ADDITION, location= f"{message_update.proto_file_name} Line: {message_update.source_code_line}", message=f"A new message {message_update.name} is added.", actionable=False, ) return # 2. If updated message is None, then the original message is removed. if message_update is None: FindingContainer.addFinding( category=FindingCategory.MESSAGE_REMOVAL, location= f"{message_original.proto_file_name} Line: {message_original.source_code_line}", message=f"A message {message_original.name} is removed", actionable=True, ) return self.global_resources_original = self.message_original.file_resources self.global_resources_update = self.message_update.file_resources # 3. Check breaking changes in each fields. Note: Fields are # identified by number, not by name. Descriptor.fields_by_number # (dict int -> FieldDescriptor) indexed by number. if message_original.fields or message_update.fields: self._compare_nested_fields( message_original.fields, message_update.fields, ) # 4. Check breaking changes in nested message. # Descriptor.nested_types_by_name (dict str -> Descriptor) # indexed by name. Recursively call _compare for nested # message type comparison. if message_original.nested_messages or message_update.nested_messages: self._compare_nested_messages( message_original.nested_messages, message_update.nested_messages, ) # 5. Check breaking changes in nested enum. if message_original.nested_enums or message_update.nested_enums: self._compare_nested_enums( message_original.nested_enums, message_update.nested_enums, ) # 6. Check `google.api.resource` annotation. self._compare_resources(message_original.resource, message_update.resource)
def _compare_method_signatures(self, method_original, method_update): signatures_original = method_original.method_signatures.value signatures_update = method_update.method_signatures.value if len(signatures_original) > len(signatures_update): FindingContainer.addFinding( category=FindingCategory.METHOD_SIGNATURE_CHANGE, location=f"{method_original.proto_file_name} Line: {method_original.method_signatures.source_code_line}", message="An existing method_signature is removed.", actionable=True, ) for old_sig, new_sig in zip(signatures_original, signatures_update): if old_sig != new_sig: FindingContainer.addFinding( category=FindingCategory.METHOD_SIGNATURE_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.method_signatures.source_code_line}", message=f"An existing method_signature is changed from '{old_sig}' to '{new_sig}'.", actionable=True, )
def compare(self): # 1. If original EnumDescriptor is None, then a new # EnumDescriptor is added. if self.enum_original is None: FindingContainer.addFinding( category=FindingCategory.ENUM_ADDITION, location= f"{self.enum_update.proto_file_name} Line: {self.enum_update.source_code_line}", message=f"A new Enum {self.enum_update.name} is added.", actionable=False, ) # 2. If updated EnumDescriptor is None, then the original # EnumDescriptor is removed. elif self.enum_update is None: FindingContainer.addFinding( category=FindingCategory.ENUM_REMOVAL, location= f"{self.enum_original.proto_file_name} Line: {self.enum_original.source_code_line}", message=f"An Enum {self.enum_original.name} is removed", actionable=True, ) # 3. If the EnumDescriptors have the same name, check the values # of them stay the same. Enum values are identified by number, # not by name. else: enum_values_dict_original = self.enum_original.values enum_values_dict_update = self.enum_update.values enum_values_keys_set_original = set( enum_values_dict_original.keys()) enum_values_keys_set_update = set(enum_values_dict_update.keys()) # Compare Enum values that only exist in original version for number in enum_values_keys_set_original - enum_values_keys_set_update: EnumValueComparator(enum_values_dict_original[number], None).compare() # Compare Enum values that only exist in update version for number in enum_values_keys_set_update - enum_values_keys_set_original: EnumValueComparator(None, enum_values_dict_update[number]).compare() # Compare Enum values that exist both in original and update versions for number in enum_values_keys_set_original & enum_values_keys_set_update: EnumValueComparator(enum_values_dict_original[number], enum_values_dict_update[number]).compare()
def compare(self): # 1. If original service is None, then a new service is added. if self.service_original is None: msg = 'A new service {} is added.'.format(self.service_update.name) FindingContainer.addFinding(FindingCategory.SERVICE_ADDITION, "", msg, False) return # 2. If updated service is None, then the original service is removed. if self.service_update is None: msg = 'A service {} is removed'.format(self.service_original.name) FindingContainer.addFinding(FindingCategory.SERVICE_REMOVAL, "", msg, True) return # 3. TODO(xiaozhenliu): method_signature annotation # 4. TODO(xiaozhenliu): LRO operation_info annotation # 5. TODO(xiaozhenliu): google.api.http annotation # 6. Check the methods list self._compareRpcMethods(self.service_original, self.service_update)
def compare(self): # 1. If original EnumValue is None, then a new EnumValue is added. if self.enum_value_original is None: FindingContainer.addFinding( category=FindingCategory.ENUM_VALUE_ADDITION, location=f"{self.enum_value_update.proto_file_name} Line: {self.enum_value_update.source_code_line}", message=f"A new EnumValue {self.enum_value_update.name} is added.", actionable=False, ) # 2. If updated EnumValue is None, then the original EnumValue is removed. elif self.enum_value_update is None: FindingContainer.addFinding( category=FindingCategory.ENUM_VALUE_REMOVAL, location=f"{self.enum_value_original.proto_file_name} Line: {self.enum_value_original.source_code_line}", message=f"An EnumValue {self.enum_value_original.name} is removed", actionable=True, ) # 3. If both EnumValueDescriptors are existing, check if the name is changed. elif self.enum_value_original.name != self.enum_value_update.name: FindingContainer.addFinding( category=FindingCategory.ENUM_VALUE_NAME_CHANGE, location=f"{self.enum_value_update.proto_file_name} Line: {self.enum_value_update.source_code_line}", message=f"Name of the EnumValue is changed, the original is {self.enum_value_original.name}, but the updated is {self.enum_value_update.name}", actionable=True, )
def _is_parent_type(self, child_type, parent_type, original_is_child): if original_is_child: parent_resources = ( self.global_resources_original.get_parent_resources_by_child_type( child_type ) ) else: parent_resources = ( self.global_resources_update.get_parent_resources_by_child_type( child_type ) ) if parent_type not in [parent.type for parent in parent_resources]: # Resulting referenced resource patterns cannot be resolved identical. FindingContainer.addFinding( FindingCategory.RESOURCE_REFERENCE_CHANGE, "", f"The child_type '{child_type}' and type '{parent_type}' of " f"resource reference option in field '{self.field_original.name}' " "cannot be resolved to the identical resource.", True, )
def _compare_resource_reference(self, field_original, field_update): resource_ref_original = self.field_original.resource_reference resource_ref_update = self.field_update.resource_reference # No resource_reference annotations found for the field in both versions. if not resource_ref_original and not resource_ref_update: return # A `google.api.resource_reference` annotation is added. if not resource_ref_original and resource_ref_update: FindingContainer.addFinding( FindingCategory.RESOURCE_REFERENCE_ADDITION, "", f"A resource reference option is added to the field {field_original.name}", False, ) return # Resource annotation is removed, check if it is added as a message resource. if resource_ref_original and not resource_ref_update: if not self._resource_ref_in_local(resource_ref_original): FindingContainer.addFinding( FindingCategory.RESOURCE_REFERENCE_REMOVAL, "", f"A resource reference option of field '{field_original.name}' is removed.", True, ) return # Resource annotation is both existing in the field for original and update versions. # They both use `type` or `child_type`. if field_original.child_type == field_update.child_type: original_type = ( resource_ref_original.type or resource_ref_original.child_type ) update_type = resource_ref_update.type or resource_ref_update.child_type if original_type != update_type: FindingContainer.addFinding( FindingCategory.RESOURCE_REFERENCE_CHANGE, "", f"The type of resource reference option in field '{field_original.name}' is changed from '{original_type}' to '{update_type}'.", True, ) return # The `type` is changed to `child_type` or `child_type` is changed to `type`, but # resulting referenced resource patterns can be resolved to be identical, # in that case it is not considered breaking. # Register the message-level resource into the global resource database, # so that we can query the parent resources for child_type. self._register_local_resource() if field_original.child_type: self._is_parent_type( resource_ref_original.child_type, resource_ref_update.type, True ) if field_update.child_type: self._is_parent_type( resource_ref_update.child_type, resource_ref_original.type, False )
def _compare_lro_annotations(self, method_original, method_update): lro_original = method_original.lro_annotation lro_update = method_update.lro_annotation if not lro_original and not lro_update: return # LRO operation_info annotation addition. if not lro_original and lro_update: FindingContainer.addFinding( category=FindingCategory.LRO_ANNOTATION_ADDITION, location=f"{method_update.proto_file_name} Line: {method_update.lro_annotation.source_code_line}", message="A LRO operation_info annotation is added.", actionable=False, ) return # LRO operation_info annotation removal. if lro_original and not lro_update: FindingContainer.addFinding( category=FindingCategory.LRO_ANNOTATION_REMOVAL, location=f"{method_original.proto_file_name} Line: {method_original.lro_annotation.source_code_line}", message="A LRO operation_info annotation is removed.", actionable=False, ) return # The response_type value of LRO operation_info is changed. if lro_original.value["response_type"] != lro_update.value["response_type"]: FindingContainer.addFinding( category=FindingCategory.LRO_RESPONSE_CHANGE, location=f"{method_update.proto_file_name} Line: {lro_update.source_code_line}", message=f"The response_type of LRO operation_info annotation is changed from {lro_original.value['response_type']} to {lro_update.value['response_type']}", actionable=True, ) # The metadata_type value of LRO operation_info is changed. if lro_original.value["metadata_type"] != lro_update.value["metadata_type"]: FindingContainer.addFinding( category=FindingCategory.LRO_METADATA_CHANGE, location=f"{method_update.proto_file_name} Line: {lro_update.source_code_line}", message=f"The metadata_type of LRO operation_info annotation is changed from {lro_original.value['metadata_type']} to {lro_update.value['metadata_type']}", actionable=True, )
def compare(self): # 1. If original EnumDescriptor is None, then a new EnumDescriptor is added. if self.enum_original is None: msg = 'A new Enum {} is added.'.format(self.enum_update.name) FindingContainer.addFinding(FindingCategory.ENUM_ADDITION, "", msg, False) # 2. If updated EnumDescriptor is None, then the original EnumDescriptor is removed. elif self.enum_update is None: msg = 'An Enum {} is removed'.format(self.enum_original.name) FindingContainer.addFinding(FindingCategory.ENUM_REMOVAL, "", msg, True) # 3. If both EnumDescriptors are existing, check if the name is changed. elif self.enum_original.name != self.enum_update.name: msg = 'Name of the Enum is changed, the original is {}, but the updated is {}'.format( self.enum_original.name, self.enum_update.name) FindingContainer.addFinding(FindingCategory.ENUM_NAME_CHANGE, "", msg, True) # 4. If the EnumDescriptors have the same name, check the values of them stay the same. # Enum values are identified by number, not by name. else: enum_values_dict_original = { x.number: x for x in self.enum_original.values } enum_values_dict_update = { x.number: x for x in self.enum_update.values } # Compare Enum values that only exist in original version for number in list( set(enum_values_dict_original.keys()) - set(enum_values_dict_update.keys())): EnumValueComparator(enum_values_dict_original[number], None).compare() # Compare Enum values that only exist in update version for number in list( set(enum_values_dict_update.keys()) - set(enum_values_dict_original.keys())): EnumValueComparator(None, enum_values_dict_update[number]).compare() # Compare Enum values that exist both in original and update versions for number in list( set(enum_values_dict_update.keys()) & set(enum_values_dict_original.keys())): EnumValueComparator(enum_values_dict_original[number], enum_values_dict_update[number]).compare()
def _compare_resources(self, fs_original, fs_update): resources_original = fs_original.resources_database resources_update = fs_update.resources_database resources_types_original = set(resources_original.types.keys()) resources_types_update = set(resources_update.types.keys()) # 1. Patterns of file-level resource definitions have changed. # TODO(xiaozhenliu): add source code information for file-level resource definition. for resource_type in resources_types_original & resources_types_update: patterns_original = resources_original.types[resource_type].pattern patterns_update = resources_update.types[resource_type].pattern # An existing pattern is removed. if len(patterns_original) > len(patterns_update): FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_CHANGE, location="", message= f"An existing pattern value of the resource definition '{resource_type}' is removed.", actionable=True, ) # An existing pattern value is changed. # A new pattern value appended to the pattern list is not consider breaking change. for old_pattern, new_pattern in zip(patterns_original, patterns_update): if old_pattern != new_pattern: FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_CHANGE, location="", message= f"Pattern value of the resource definition '{resource_type}' is updated from '{old_pattern}' to '{new_pattern}'.", actionable=True, ) # 2. File-level resource definitions addition. for resource_type in resources_types_update - resources_types_original: FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_ADDITION, location="", message= f"A file-level resource definition '{resource_type}' has been added.", actionable=False, )
def compare(self): # 1. If original EnumValue is None, then a new EnumValue is added. if self.enum_value_original is None: msg = 'A new EnumValue {} is added.'.format( self.enum_value_update.name) FindingContainer.addFinding(FindingCategory.ENUM_VALUE_ADDITION, "", msg, False) # 2. If updated EnumValue is None, then the original EnumValue is removed. elif self.enum_value_update is None: msg = ('An EnumValue {} is removed'.format( self.enum_value_original.name)) FindingContainer.addFinding(FindingCategory.ENUM_VALUE_REMOVAL, "", msg, True) # 3. If both EnumValueDescriptors are existing, check if the name is changed. elif self.enum_value_original.name != self.enum_value_update.name: msg = ( 'Name of the EnumValue is changed, the original is {}, but the updated is {}' .format(self.enum_value_original.name, self.enum_value_update.name)) FindingContainer.addFinding(FindingCategory.ENUM_VALUE_NAME_CHANGE, "", msg, True)
def _compare_http_annotation(self, method_original, method_update): """Compare the fields `http_method, http_uri, http_body` of google.api.http annotation.""" http_annotation_original = method_original.http_annotation.value http_annotation_update = method_update.http_annotation.value if not http_annotation_original or not http_annotation_update: # (Aip127) APIs must provide HTTP definitions for each RPC that they define, # except for bi-directional streaming RPCs, so the http_annotation addition/removal indicates # streaming state changes of the RPC, which is a breaking change. if http_annotation_original and not http_annotation_update: FindingContainer.addFinding( category=FindingCategory.HTTP_ANNOTATION_REMOVAL, location=f"{method_original.proto_file_name} Line: {method_original.http_annotation.source_code_line}", message="A google.api.http annotation is removed.", actionable=True, ) if not http_annotation_original and http_annotation_update: FindingContainer.addFinding( category=FindingCategory.HTTP_ANNOTATION_ADDITION, location=f"{method_update.proto_file_name} Line: {method_update.http_annotation.source_code_line}", message="A google.api.http annotation is added.", actionable=False, ) return for annotation in ( ("http_method", "None", "An existing http method is changed."), ("http_uri", "None", "An existing http method URI is changed."), ("http_body", "None", "An existing http method body is changed."), ): # TODO (xiaozhenliu): this should allow version updates. For example, # from `v1/example:foo` to `v1beta1/example:foo` is not a breaking change. if http_annotation_original.get( annotation[0], annotation[1] ) != http_annotation_update.get(annotation[0], annotation[1]): FindingContainer.addFinding( category=FindingCategory.HTTP_ANNOTATION_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.http_annotation.source_code_line}", message=annotation[2], actionable=True, )
def compare(self): # 1. If original FieldDescriptor is None, then a # new FieldDescriptor is added. if self.field_original is None: FindingContainer.addFinding( category=FindingCategory.FIELD_ADDITION, location=f"{self.field_update.proto_file_name} Line: {self.field_update.source_code_line}", message=f"A new Field {self.field_update.name} is added.", actionable=False, ) return # 2. If updated FieldDescriptor is None, then # the original FieldDescriptor is removed. if self.field_update is None: FindingContainer.addFinding( category=FindingCategory.FIELD_REMOVAL, location=f"{self.field_original.proto_file_name} Line: {self.field_original.source_code_line}", message=f"A Field {self.field_original.name} is removed", actionable=True, ) return self.global_resources_original = self.field_original.file_resources self.global_resources_update = self.field_update.file_resources self.local_resource_original = self.field_original.message_resource self.local_resource_update = self.field_update.message_resource # 3. If both FieldDescriptors are existing, check # if the name is changed. if self.field_original.name != self.field_update.name: FindingContainer.addFinding( category=FindingCategory.FIELD_NAME_CHANGE, location=f"{self.field_update.proto_file_name} Line: {self.field_update.source_code_line}", message=f"Name of the Field is changed, the original is {self.field_original.name}, but the updated is {self.field_update.name}", actionable=True, ) return # 4. If the FieldDescriptors have the same name, check if the # repeated state of them stay the same. # TODO(xiaozhenliu): add location information for field.label if self.field_original.label != self.field_update.label: FindingContainer.addFinding( category=FindingCategory.FIELD_REPEATED_CHANGE, location="", message=f"Repeated state of the Field is changed, the original is {self.field_original.label}, but the updated is {self.field_update.label}", actionable=True, ) # 5. If the FieldDescriptors have the same repeated state, # check if the type of them stay the same. # TODO(xiaozhenliu): add location information for field.type if self.field_original.proto_type != self.field_update.proto_type: FindingContainer.addFinding( category=FindingCategory.FIELD_TYPE_CHANGE, location="", message=f"Type of the field is changed, the original is {self.field_original.proto_type}, but the updated is {self.field_update.proto_type}", actionable=True, ) # 6. Check the oneof_index of the field. if self.field_original.oneof != self.field_update.oneof: location = f"{self.field_update.proto_file_name} Line: {self.field_update.source_code_line}" if self.field_original.oneof: msg = f"The existing field {self.field_original.name} is moved out of One-of." FindingContainer.addFinding( category=FindingCategory.FIELD_ONEOF_REMOVAL, location=location, message=msg, actionable=True, ) else: msg = f"The existing field {self.field_original.name} is moved into One-of." FindingContainer.addFinding( category=FindingCategory.FIELD_ONEOF_ADDITION, location=location, message=msg, actionable=True, ) # 6. Check `google.api.resource_reference` annotation. # TODO(xiaozhenliu): add location information for field.resource_reference. self._compare_resource_reference(self.field_original, self.field_update)
def _compareRpcMethods( self, service_original, service_update, messages_map_original, messages_map_update, ): methods_original = service_original.methods methods_update = service_update.methods methods_original_keys = set(methods_original.keys()) methods_update_keys = set(methods_update.keys()) # 3.1 An RPC method is removed. for name in methods_original_keys - methods_update_keys: removed_method = methods_original[name] FindingContainer.addFinding( category=FindingCategory.METHOD_REMOVAL, location=f"{removed_method.proto_file_name} Line: {removed_method.source_code_line}", message=f"An rpc method {name} is removed", actionable=True, ) # 3.2 An RPC method is added. for name in methods_update_keys - methods_original_keys: added_method = methods_update[name] FindingContainer.addFinding( category=FindingCategory.METHOD_ADDTION, location=f"{added_method.proto_file_name} Line: {added_method.source_code_line}", message=f"An rpc method {name} is added", actionable=False, ) for name in methods_update_keys & methods_original_keys: method_original = methods_original[name] method_update = methods_update[name] # 3.3 The request type of an RPC method is changed. input_type_original = method_original.input.value input_type_update = method_update.input.value if input_type_original != input_type_update: FindingContainer.addFinding( category=FindingCategory.METHOD_INPUT_TYPE_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.input.source_code_line}", message=f"Input type of method {name} is changed from {input_type_original} to {input_type_update}", actionable=True, ) # 3.4 The response type of an RPC method is changed. response_type_original = method_original.output.value response_type_update = method_update.output.value if response_type_original != response_type_update: FindingContainer.addFinding( category=FindingCategory.METHOD_RESPONSE_TYPE_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.output.source_code_line}", message=f"Output type of method {name} is changed from {response_type_original} to {response_type_update}", actionable=True, ) # 3.5 The request streaming state of an RPC method is changed. if ( method_original.client_streaming.value != method_update.client_streaming.value ): FindingContainer.addFinding( category=FindingCategory.METHOD_CLIENT_STREAMING_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.client_streaming.source_code_line}", message=f"The request streaming type of method {name} is changed", actionable=True, ) # 3.6 The response streaming state of an RPC method is changed. if ( method_original.server_streaming.value != method_update.server_streaming.value ): FindingContainer.addFinding( category=FindingCategory.METHOD_SERVER_STREAMING_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.server_streaming.source_code_line}", message=f"The response streaming type of method {name} is changed", actionable=True, ) # 3.7 The paginated response of an RPC method is changed. if method_original.paged_result_field != method_update.paged_result_field: FindingContainer.addFinding( category=FindingCategory.METHOD_PAGINATED_RESPONSE_CHANGE, location=f"{method_update.proto_file_name} Line: {method_update.source_code_line}", message=f"The paginated response of method {name} is changed", actionable=True, ) # TODO(xiaozhenliu): add source code information for annotations. # The customized annotation options share the same field number (1000) # in MethodDescriptorProto.options. # 3.8 The method_signature annotation is changed. self._compare_method_signatures(method_original, method_update) # 3.9 The LRO operation_info annotation is changed. self._compare_lro_annotations(method_original, method_update) # 3.10 The google.api.http annotation is changed. self._compare_http_annotation(method_original, method_update)
def _compareRpcMethods(self, service_original, service_update): methods_original = {x.name: x for x in service_original.method} methods_update = {x.name: x for x in service_update.method} # 6.1 An RPC method is removed. for name in list( set(methods_original.keys()) - set(methods_update.keys())): msg = 'An rpc method {} is removed'.format(name) FindingContainer.addFinding(FindingCategory.METHOD_REMOVAL, "", msg, True) # 6.2 An RPC method is added. for name in list( set(methods_original.keys()) - set(methods_update.keys())): msg = 'An rpc method {} is added'.format(name) FindingContainer.addFinding(FindingCategory.METHOD_ADDTION, "", msg, False) for name in list( set(methods_original.keys()) & set(methods_update.keys())): method_original = methods_original[name] method_update = methods_update[name] # 6.3 The request type of an RPC method is changed. if method_original.input_type.name != method_update.input_type.name: msg = 'Input type of method {} is changed from {} to {}'.format( name, method_original.input_type.name, method_update.input_type.name) FindingContainer.addFinding( FindingCategory.METHOD_INPUT_TYPE_CHANGE, "", msg, True) # 6.4 The request type of an RPC method is changed. if method_original.output_type.name != method_update.output_type.name: msg = 'Output type of method {} is changed from {} to {}'.format( name, method_original.output_type.name, method_update.output_type.name) FindingContainer.addFinding( FindingCategory.METHOD_RESPONSE_TYPE_CHANGE, "", msg, True) # 6.5 The request streaming state of an RPC method is changed. if method_original.client_streaming != method_update.client_streaming: msg = 'The request streaming type of method {} is changed'.format( name) FindingContainer.addFinding( FindingCategory.METHOD_CLIENT_STREAMING_CHANGE, "", msg, True) # 6.6 The response streaming state of an RPC method is changed. if method_original.server_streaming != method_update.server_streaming: msg = 'The response streaming type of method {} is changed'.format( name) FindingContainer.addFinding( FindingCategory.METHOD_SERVER_STREAMING_CHANGE, "", msg, True) # 6.7 The paginated response of an RPC method is changed. if self._isPaginatedResponse( method_original) != self._isPaginatedResponse( method_update): msg = 'The paginated response of method {} is changed'.format( name) FindingContainer.addFinding( FindingCategory.METHOD_PAGINATED_RESPONSE_CHANGE, "", msg, True)
def compare(self): # 1. If original FieldDescriptor is None, then a new FieldDescriptor is added. if self.field_original is None: msg = 'A new Field {} is added.'.format(self.field_update.name) FindingContainer.addFinding(FindingCategory.FIELD_ADDITION, "", msg, False) return # 2. If updated FieldDescriptor is None, then the original FieldDescriptor is removed. if self.field_update is None: msg = 'A Field {} is removed'.format(self.field_original.name) FindingContainer.addFinding(FindingCategory.FIELD_REMOVAL, "", msg, True) return # 3. If both FieldDescriptors are existing, check if the name is changed. if self.field_original.name != self.field_update.name: msg = 'Name of the Field is changed, the original is {}, but the updated is {}'.format( self.field_original.name, self.field_update.name) FindingContainer.addFinding(FindingCategory.FIELD_NAME_CHANGE, "", msg, True) return # 4. If the EnumDescriptors have the same name, check if the repeated state of them stay the same. if self.field_original.label != self.field_update.label: option_original = FieldDescriptorProto().Label.Name( self.field_original.label) option_update = FieldDescriptorProto().Label.Name( self.field_update.label) msg = 'Repeated state of the Field is changed, the original is {}, but the updated is {}'.format( option_original, option_update) FindingContainer.addFinding(FindingCategory.FIELD_REPEATED_CHANGE, "", msg, True) # 5. If the EnumDescriptors have the same repeated state, check if the type of them stay the same. if self.field_original.type != self.field_update.type: type_original = FieldDescriptorProto().Type.Name( self.field_original.type) type_update = FieldDescriptorProto().Type.Name( self.field_update.type) msg = 'Type of the Field is changed, the original is {}, but the updated is {}'.format( type_original, type_update) FindingContainer.addFinding(FindingCategory.FIELD_TYPE_CHANGE, "", msg, True) # 6. Check the existing field is moved out of one-of or moved into one-of . if self.field_original.containing_oneof != self.field_update.containing_oneof: if self.field_original.containing_oneof != None: msg = 'The Field {} is moved out of one-of'.format( self.field_original.name) FindingContainer.addFinding( FindingCategory.FIELD_ONEOF_REMOVAL, "", msg, True) else: msg = 'The Field {} is moved into one-of'.format( self.field_update.name) FindingContainer.addFinding( FindingCategory.FIELD_ONEOF_ADDITION, "", msg, True)
def _compare_resources( self, resource_original, resource_update, ): if not resource_original and not resource_update: return # 1. A new resource definition is added. if not resource_original and resource_update: FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_ADDITION, location="", message= f"A message-level resource definition {resource_update.type} has been added.", actionable=False, ) return # 2. Message-level resource definitions removal may not be breaking change since # the resource could be moved to file-level resource definition. # 3. Note that the type change of an existing resource definition is like one resource # is removed and another one is added. if (resource_original and not resource_update) or ( resource_original.type != resource_update.type): if not self.global_resources_update: FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_REMOVAL, location="", message= f"A message-level resource definition {resource_original.type} has been removed.", actionable=True, ) return # Check if the removed resource is in the global file-level resource database. if resource_original.type not in self.global_resources_update.types: FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_REMOVAL, location="", message= f"A message-level resource definition {resource_original.type} has been removed.", actionable=True, ) else: # Check the patterns of existing file-level resource are compatible with # the patterns of the removed message-level resource. global_resource_pattern = self.global_resources_update.types[ resource_original.type].pattern # If there is pattern removal, or pattern value change. Then the global file-level resource # can not replace the original message-level resource. if self._compatible_patterns(resource_original.pattern, global_resource_pattern): FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_REMOVAL, location="", message= f"A message-level resource definition {resource_original.type} has been removed.", actionable=True, ) return # Resource is existing in both original and update versions. # 3. Patterns of message-level resource definitions have changed. if self._compatible_patterns(resource_original.pattern, resource_update.pattern): FindingContainer.addFinding( category=FindingCategory.RESOURCE_DEFINITION_CHANGE, location="", message= f"The pattern of message-level resource definition has changed from {resource_original.pattern} to {resource_update.pattern}.", actionable=True, )