#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright © 2015 Collabora Ltd. # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. import os import sys from . import ast from .girparser import GIRParser from .girwriter import COMPATIBLE_GIR_VERSION from .collections import OrderedDict _xdg_data_dirs = [x for x in os.environ.get('XDG_DATA_DIRS', '').split(os.pathsep)] _xdg_data_dirs.append(DATADIR) if os.name != 'nt': _xdg_data_dirs.append('/usr/share') class GIRComparator(object): # Output severity levels. OUTPUT_INFO = 0 OUTPUT_FORWARDS_INCOMPATIBLE = 1 OUTPUT_BACKWARDS_INCOMPATIBLE = 2 # TODO WARNING_CATEGORIES = [0, 1, 2] def __init__(self, old_namespace, new_namespace, enabled_warnings=WARNING_CATEGORIES): self._old_namespace = old_namespace self._old_namespaces = {} # includes from _old_namespace self._new_namespace = new_namespace self._new_namespaces = {} # includes from _new_namespace self._output = [] self._enabled_warnings = enabled_warnings self._includepaths = [] # Public API def set_include_paths(self, paths): self._includepaths = list(paths) def print_output(self): """ Print all the info, warning and error messages generated by the most recent call to compare(). """ for (level, message) in self._output: if not self._warning_enabled(level): continue formatted_level = self._format_level(level) fd = self._get_fd_for_level(level) fd.write('%s: %s\n' % (formatted_level, message)) def get_output(self): """ Return all the info, warning and error messages generated by the most recent call to compare(). """ out = [] for (level, message) in self._output: if not self._warning_enabled(level): continue out.append((level, message)) return out def _find_include(self, include): searchdirs = self._includepaths[:] for path in _xdg_data_dirs: searchdirs.append(os.path.join(path, 'gir-1.0')) searchdirs.append(os.path.join(DATADIR, 'gir-1.0')) girname = '%s-%s.gir' % (include.name, include.version) for d in searchdirs: path = os.path.join(d, girname) if os.path.exists(path): return path self._issue_output(self.OUTPUT_INFO, 'Could not find include %r (search path: %r)' % (girname, searchdirs)) return None # Adds items to @namespaces and returns boolean success/fail. def _parse_includes(self, namespace, namespaces): for inc in namespace.includes: # Already parsed it? if inc.name in namespaces: if namespaces[inc.name].version == inc.version: continue # Already parsed a different version. self._issue_output(self.OUTPUT_INFO, 'Incompatible versions of namespace ' '‘%s’ in includes: %s and %s.' % (inc.name, namespaces[inc.name].version, inc.version)) return False # bail immediately # Parse the namespace. try: parser = GIRParser(types_only=True) filename = self._find_include(inc) if filename is None: return False # bail immediately parser.parse(filename) parsed_namespace = parser.get_namespace() namespaces[parsed_namespace.name] = parsed_namespace except Exception as e: print(e) self._issue_output(self.OUTPUT_INFO, 'Failed to parse included namespace ' '‘%s-%s’.' % (inc.name, inc.version)) return False # bail immediately # Parse its includes. if not self._parse_includes(parsed_namespace, namespaces): return False return True def compare(self): """ Compare the two interfaces and store the results. Return 0 if no relevant warnings were outputted; a positive integer otherwise. The return value is affected by the categories of enabled warnings. """ self._output = [] # TODO: do , , affect API? if self._old_namespace.name != self._new_namespace.name or \ self._old_namespace.version != self._new_namespace.version: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Namespace has changed name from ‘%s-%s’ to ' '‘%s-%s’.' % (self._old_namespace.name, self._old_namespace.version, self._new_namespace.name, self._new_namespace.version)) # Sufficiently big difference to give up on comparison right here. return 1 # Parse the transitive closure of included namespaces so that types can # be resolved for subtype analysis. self._old_namespaces = {} self._new_namespaces = {} if not self._parse_includes(self._old_namespace, self._old_namespaces) or \ not self._parse_includes(self._new_namespace, self._new_namespaces): # Sufficiently big failure to give up right now. return 1 # TODO: what about shared-library, identifier-prefixes, # symbol-prefixes? for (name, node) in self._old_namespace.names.iteritems(): # See if the old node exists in the new file. if name not in self._new_namespace.names: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Node ‘%s’ has been removed.' % self._format_name(node)) else: # Compare the two. self._compare_nodes(node, self._new_namespace.names[name]) for (name, node) in self._new_namespace.names.iteritems(): # See if the new node exists in the old file. if name not in self._old_namespace.names: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Node ‘%s’ has been added.' % self._format_name(node)) # Work out the exit status. retval = 0 for (level, _) in self._output: if level != self.OUTPUT_INFO and self._warning_enabled(level): retval = 1 return retval # Private # TODO: Support source code locations, and unique error codes for lookup # online. def _issue_output(self, level, message): self._output.append((level, message)) def _format_level(self, level): return [' INFO', ' WARN', 'ERROR'][level] def _get_fd_for_level(self, level): if level == self.OUTPUT_INFO: return sys.stdout return sys.stderr def _warning_enabled(self, level): return level in self._enabled_warnings def _get_type_for_node(self, node): return node.namespace.type_from_name(node.name) def _get_node_for_type(self, type, namespaces): if type is None: return None assert type.target_giname is not None [namespace, name] = type.target_giname.split('.') if namespace in namespaces and \ name in namespaces[namespace].names: return namespaces[namespace].names[name] # Failure. self._issue_output(self.OUTPUT_INFO, 'Could not resolve type ‘%s’ to node.' % type) return None # Returns (gpointer > type). NOTE: This is strict subtyping. def _type_is_gpointer_subtype(self, type): # TODO return False TYPES_NONEQUAL = 0 # ¬(A :> B) ∧ ¬(B :> A) TYPES_EQUAL = 1 # (A :> B) ∧ (B :> A) TYPES_SUPERTYPE = 2 # (A :> B) ∧ ¬(B :> A) TYPES_SUBTYPE = 3 # ¬(A :> B) ∧ (B :> A) # Returns (A :> B) def _type_is_supertype(self, type_a, node_a, type_b, node_b, namespaces_b): while node_b is not None and not type_a.is_equiv(type_b): type_b = node_b.parent_type node_b = self._get_node_for_type(type_b, namespaces_b) return node_b is not None def _types_equal(self, type_a, type_b): # Allow None as an alias for 'no parent type'. if (type_a is None and type_b is None) or type_a.is_equiv(type_b): return self.TYPES_EQUAL # If the types aren't resolved, bail immediately. if not type_a.resolved or not type_b.resolved: self._issue_output(self.OUTPUT_INFO, 'Could not resolve types ‘%s’ and ‘%s’ for ' 'comparison.' % (type_a, type_b)) return self.TYPES_NONEQUAL # Special case gpointers, as it's common for an annotation to be added # which 'subtypes' from gpointer to a more specific type. if type_a.target_fundamental == 'gpointer' and \ self._type_is_gpointer_subtype(type_b): return self.TYPES_SUPERTYPE elif (type_b.target_fundamental == 'gpointer' and self._type_is_gpointer_subtype(type_a)): return self.TYPES_SUBTYPE # Otherwise, if they are fundamental or foreign types, bail silently. if type_a.target_giname is None or type_b.target_giname is None: return self.TYPES_NONEQUAL # Check for subtype relations. That means looking up the Node instances # for the given types, which may come from other namespaces. node_a = self._get_node_for_type(type_a, self._old_namespaces) node_b = self._get_node_for_type(type_b, self._new_namespaces) if ((isinstance(node_a, ast.Class) and isinstance(node_b, ast.Class)) or (isinstance(node_a, ast.Interface) and isinstance(node_b, ast.Interface))): if self._type_is_supertype(type_a, node_a, type_b, node_b, self._new_namespaces): return self.TYPES_SUPERTYPE elif self._type_is_supertype(type_b, node_b, type_a, node_a, self._old_namespaces): return self.TYPES_SUBTYPE return self.TYPES_NONEQUAL def _compare_nodes(self, old_node, new_node): # TODO: # ast.Include, ast.ErrorQuarkFunction, # ast.Field, ast.Union, ast.Boxed, # foreign # Precondition of calling this function. assert old_node.name == new_node.name assert old_node.namespace.name == new_node.namespace.name assert old_node.namespace.version == new_node.namespace.version if type(old_node) != type(new_node): self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Node ‘%s’ has changed type from %s to %s.' % (self._format_name(old_node), type(old_node), type(new_node))) # Delegate to type-specific comparison functions. comparison_func_map = { ast.Function: self._compare_functions, ast.Class: self._compare_classes, ast.Record: self._compare_records, ast.Constant: self._compare_constants, ast.Enum: self._compare_enums, ast.Bitfield: self._compare_bitfields, ast.Interface: self._compare_interfaces, ast.Alias: self._compare_aliases, ast.Callback: self._compare_callbacks, ast.Union: self._compare_unions, } if type(old_node) in comparison_func_map: comparison_func_map[type(old_node)](old_node, new_node) else: self._issue_output(self.OUTPUT_INFO, 'No comparison function for node ‘%s’ of type ' '%s.' % (self._format_name(old_node), type(old_node))) def _compare_callables(self, old_callable, new_callable): # Precondition of calling this function. assert old_callable.parent.name == new_callable.parent.name # Compare instance parameters. if (old_callable.instance_parameter is None) != \ (new_callable.instance_parameter is None): self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Callable ‘%s’ has changed instance ' 'parameter from %s to %s. TODO' % (self._format_name(old_callable), old_callable.instance_parameter, new_callable.instance_parameter)) elif old_callable.instance_parameter is not None: self._compare_parameters(old_callable.instance_parameter, new_callable.instance_parameter, old_callable, new_callable) # Compare normal parameters. n_old_params = len(old_callable.parameters) n_new_params = len(new_callable.parameters) for i in range(max(n_old_params, n_new_params)): if i >= n_old_params: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Parameter %u (‘%s’) of callable ‘%s’ ' 'has been added.' % (i, new_callable.parameters[i].name, self._format_name(new_callable))) elif i >= n_new_params: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Parameter %u (‘%s’) of callable ‘%s’ ' 'has been removed.' % (i, old_callable.parameters[i].name, self._format_name(old_callable))) else: self._compare_parameters(old_callable.parameters[i], new_callable.parameters[i], old_callable, new_callable) # Throwing errors. if old_callable.throws and not new_callable.throws: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Callable ‘%s’ no longer throws ' 'exceptions.' % self._format_name(new_callable)) elif not old_callable.throws and new_callable.throws: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Callable ‘%s’ now throws exceptions.' % self._format_name(new_callable)) # Return value. type_comparison = self._types_equal(old_callable.retval.type, new_callable.retval.type) if type_comparison == self.TYPES_SUPERTYPE: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Return value of callable ‘%s’ has changed ' 'type from ‘%s’ to its subtype ‘%s’.' % (self._format_name(new_callable), old_callable.retval.type, new_callable.retval.type)) elif type_comparison != self.TYPES_EQUAL: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Return value of callable ‘%s’ has changed ' 'type from ‘%s’ to ‘%s’.' % (self._format_name(new_callable), old_callable.retval.type, new_callable.retval.type)) if old_callable.retval.nullable and not new_callable.retval.nullable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Return value of callable ‘%s’ is no longer ' 'nullable.' % self._format_name(new_callable)) elif not old_callable.retval.nullable and new_callable.retval.nullable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Return value of callable ‘%s’ is now ' 'nullable.' % self._format_name(new_callable)) if old_callable.retval.transfer != new_callable.retval.transfer: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Return value of callable ‘%s’ has changed ' 'transfer from ‘%s’ to ‘%s’.' % (self._format_name(new_callable), old_callable.retval.transfer, new_callable.retval.transfer)) def _compare_functions(self, old_function, new_function): self._compare_callables(old_function, new_function) # TODO: symbol, is_constructor, shadowed_by, shadows, # moved_to, internal_skipped if old_function.is_method and not new_function.is_method: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Function ‘%s’ has changed from a method to a ' 'function.' % self._format_name(old_function)) elif not old_function.is_method and new_function.is_method: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Function ‘%s’ has changed from a function to ' 'a method.' % self._format_name(old_function)) def _compare_virtual_functions(self, old_function, new_function): self._compare_functions(old_function, new_function) def _compare_properties(self, old_property, new_property): # Precondition of calling this function. assert old_property.name == new_property.name assert old_property.parent.name == new_property.parent.name # Compare types. type_comparison = self._types_equal(old_property.type, new_property.type) if type_comparison == self.TYPES_SUPERTYPE: # new_property.type is a subtype of old_property.type self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ has changed type from ‘%s’ ' 'to its subtype ‘%s’.' % (self._format_name(old_property), old_property.type, new_property.type)) elif type_comparison != self.TYPES_EQUAL: # The types are in a supertype relationship in the wrong # direction, or not related at all self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has changed type from ‘%s’ ' 'to ‘%s’.' % (self._format_name(old_property), old_property.type, new_property.type)) # Readability and writeability. if old_property.readable and not new_property.readable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has become non-readable.' % self._format_name(old_property)) elif not old_property.readable and new_property.readable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ has become readable.' % self._format_name(old_property)) if old_property.writable and not new_property.writable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has become non-writable.' % self._format_name(old_property)) elif not old_property.writable and new_property.writable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ has become writable.' % self._format_name(old_property)) # Constructability. .construct is True if the property must be set at # construction time. if old_property.construct and not new_property.construct: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ no longer has to be set on ' 'construction.' % self._format_name(old_property)) elif not old_property.construct and new_property.construct: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ now has to be set on ' 'construction.' % self._format_name(old_property)) if old_property.construct_only and not new_property.construct_only: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ can now be set after ' 'construction.' % self._format_name(old_property)) elif not old_property.construct_only and new_property.construct_only: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ can now only be set on ' 'construction.' % self._format_name(old_property)) # Transfer if old_property.transfer != new_property.transfer: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has changed transfer from ' '‘%s’ to ‘%s’.' % (self._format_name(old_property), old_property.transfer, new_property.transfer)) def _compare_signals(self, old_signal, new_signal): # TODO: when, no_recurse, detailed, action, no_hooks # Precondition for calling this function. assert old_signal.name == new_signal.name self._compare_callables(old_signal, new_signal) def _compare_fields(self, old_field, new_field): # TODO: Annotated, bits, anonymous_node # Precondition of calling this function. assert old_field.name == new_field.name assert old_field.parent.name == new_field.parent.name assert old_field.namespace.name == new_field.namespace.name # Compare types. type_comparison = self._types_equal(old_field.type, new_field.type) if type_comparison == self.TYPES_SUPERTYPE: # new_field.type is a subtype of old_field.type self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has changed type from ‘%s’ ' 'to its subtype ‘%s’.' % (self._format_name(old_field), old_field.type, new_field.type)) elif type_comparison != self.TYPES_EQUAL: # The types are in a supertype relationship in the wrong # direction, or not related at all self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has changed type from ‘%s’ ' 'to ‘%s’.' % (self._format_name(old_field), old_field.type, new_field.type)) # Readability and writeability. if old_field.readable and not new_field.readable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has become non-readable.' % self._format_name(old_field)) elif not old_field.readable and new_field.readable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has become readable.' % self._format_name(old_field)) if old_field.writable and not new_field.writable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has become non-writable.' % self._format_name(old_field)) elif not old_field.writable and new_field.writable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has become writable.' % self._format_name(old_field)) # Privacy. if not old_field.private and new_field.private: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has become private.' % self._format_name(old_field)) elif old_field.private and not new_field.private: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has become public.' % self._format_name(old_field)) def _compare_parameters(self, old_parameter, new_parameter, old_parent=None, new_parent=None): # TODO: TypeContainer, closure_name, destroy_name, scope, # caller_allocates if old_parameter.argname != new_parameter.argname: self._issue_output(self.OUTPUT_INFO, 'Parameter has changed name from ‘%s’ to ' '‘%s’.' % (self._format_name(old_parameter, old_parent), self._format_name(new_parameter, new_parent))) # TODO: Are direction changes from * to inout OK? if old_parameter.direction != new_parameter.direction: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Parameter ‘%s’ has changed direction from ' '‘%s’ to ‘%s’.' % (self._format_name(old_parameter, old_parent), old_parameter.direction, new_parameter.direction)) # Optionality. if old_parameter.optional and not new_parameter.optional: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Parameter ‘%s’ has become mandatory.' % self._format_name(old_parameter, old_parent)) elif not old_parameter.optional and new_parameter.optional: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Parameter ‘%s’ has become optional.' % self._format_name(old_parameter, old_parent)) # Nullability. if old_parameter.nullable and not new_parameter.nullable: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Parameter ‘%s’ has become non-nullable.' % self._format_name(old_parameter, old_parent)) elif not old_parameter.nullable and new_parameter.nullable: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Parameter ‘%s’ has become nullable.' % self._format_name(old_parameter, old_parent)) def _compare_classes(self, old_class, new_class): # TODO: ctype, c_symbol_prefix, parent_type, fundamental, unref_func, # ref_func, set_value_func, get_value_func, parent_chain, # glib_type_struct, # Precondition of calling this function. assert old_class.name == new_class.name if old_class.is_abstract and not new_class.is_abstract: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Class ‘%s’ has become non-abstract.' % self._format_name(old_class)) elif not old_class.is_abstract and new_class.is_abstract: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Class ‘%s’ has become abstract.' % self._format_name(old_class)) type_comparison = self._types_equal(old_class.parent_type, new_class.parent_type) if type_comparison == self.TYPES_SUPERTYPE: # new_class.parent_type is a subtype of old_class.parent_type self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Class ‘%s’ has changed parent type from ‘%s’ ' 'to its subtype ‘%s’.' % (self._format_name(old_class), old_class.parent_type, new_class.parent_type)) elif type_comparison != self.TYPES_EQUAL: # The parent types are in a supertype relationship in the wrong # direction, or not related at all self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Class ‘%s’ has changed parent type from ‘%s’ ' 'to ‘%s’.' % (self._format_name(old_class), old_class.parent_type, new_class.parent_type)) # Methods. (a, both, b) = self._lists_equal(old_class.methods, new_class.methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Virtual methods. (a, both, b) = self._lists_equal(old_class.virtual_methods, new_class.virtual_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Virtual method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Virtual method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_virtual_functions(old_method, new_method) # Static methods. (a, both, b) = self._lists_equal(old_class.static_methods, new_class.static_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Constructors. (a, both, b) = self._lists_equal(old_class.constructors, new_class.constructors, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Interfaces. NOTE: We do not compare interfaces in @both for equality, # since they're just #Types. (a, both, b) = self._lists_equal(old_class.interfaces, new_class.interfaces, lambda x: str(x)) for old_type in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Interface implementation ‘%s’ has been ' 'removed from class ‘%s’.' % (old_type, self._format_name(old_class))) for new_type in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Interface implementation ‘%s’ has been ' 'added to class ‘%s’.' % (new_type, self._format_name(new_class))) # Properties. (a, both, b) = self._lists_equal(old_class.properties, new_class.properties, lambda x: x.name) for old_property in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has been removed.' % self._format_name(old_property)) for new_property in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ has been added.' % self._format_name(new_property)) for (old_property, new_property) in both: self._compare_properties(old_property, new_property) # Signals. (a, both, b) = self._lists_equal(old_class.signals, new_class.signals, lambda x: x.name) for old_signal in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Signal ‘%s’ has been removed.' % self._format_name(old_signal)) for new_signal in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Signal ‘%s’ has been added.' % self._format_name(new_signal)) for (old_signal, new_signal) in both: self._compare_signals(old_signal, new_signal) # Fields. (a, both, b) = self._lists_equal(old_class.fields, new_class.fields, lambda x: x.name) for old_field in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has been removed.' % self._format_name(old_field)) for new_field in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has been added.' % self._format_name(new_field)) for (old_field, new_field) in both: self._compare_fields(old_field, new_field) def _compare_compounds(self, old_compound, new_compound): # TODO: Registered, ctype, disguised, gtype_name, get_type, # c_symbol_prefix, tag_name # Methods. (a, both, b) = self._lists_equal(old_compound.methods, new_compound.methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Static methods. (a, both, b) = self._lists_equal(old_compound.static_methods, new_compound.static_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Constructors. (a, both, b) = self._lists_equal(old_compound.constructors, new_compound.constructors, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Fields. (a, both, b) = self._lists_equal(old_compound.fields, new_compound.fields, lambda x: x.name) for old_field in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has been removed.' % self._format_name(old_field)) for new_field in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has been added.' % self._format_name(new_field)) for (old_field, new_field) in both: self._compare_fields(old_field, new_field) def _compare_records(self, old_record, new_record): # TODO: is_gtype_struct_for self._compare_compounds(old_record, new_record) def _compare_constants(self, old_constant, new_constant): # TODO: ctype # Check the types. type_comparison = self._types_equal(old_constant.value_type, new_constant.value_type) if type_comparison == self.TYPES_SUPERTYPE: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Constant ‘%s’ has changed type from ‘%s’ to ' 'its subtype ‘%s’.' % (self._format_name(new_constant), old_constant.value_type, new_constant.value_type)) elif type_comparison != self.TYPES_EQUAL: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Constant ‘%s’ has changed type from ‘%s’ to ' '‘%s’.' % (self._format_name(new_constant), old_constant.value_type, new_constant.value_type)) # Compare the values for information only. if old_constant.value != new_constant.value: self._issue_output(self.OUTPUT_INFO, 'Constant ‘%s’ has changed value from ‘%s’ to ' '‘%s’.' % (self._format_name(new_constant), old_constant.value, new_constant.value)) def _compare_enums(self, old_enum, new_enum): # TODO: Registered, ctype, c_symbol_prefix # Members. (a, both, b) = self._lists_equal(old_enum.members, new_enum.members, lambda x: x.name) for old_member in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Member ‘%s’ has been removed.' % self._format_name(old_member)) for new_member in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Member ‘%s’ has been added.' % self._format_name(new_member)) for (old_member, new_member) in both: self._compare_members(old_member, new_member) # Static methods. (a, both, b) = self._lists_equal(old_enum.static_methods, new_enum.static_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) def _compare_bitfields(self, old_bitfield, new_bitfield): # TODO: Registered, ctype, c_symbol_prefix # Members. (a, both, b) = self._lists_equal(old_bitfield.members, new_bitfield.members, lambda x: x.name) for old_member in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Member ‘%s’ has been removed.' % self._format_name(old_member)) for new_member in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Member ‘%s’ has been added.' % self._format_name(new_member)) for (old_member, new_member) in both: self._compare_members(old_member, new_member) # Static methods. (a, both, b) = self._lists_equal(old_bitfield.static_methods, new_bitfield.static_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) def _compare_members(self, old_member, new_member): # TODO: Annotated, symbol, nick # Precondition of calling this function. assert old_member.name == new_member.name assert old_member.parent.name == new_member.parent.name # Information output. if old_member.value != new_member.value: self._issue_output(self.OUTPUT_INFO, 'Member ‘%s’ has changed value from ‘%s’ to ' '‘%s’.' % (self._format_name(old_member), old_member.value, new_member.value)) def _compare_interfaces(self, old_interface, new_interface): # TODO: Registered, ctype, c_symbol_prefix, glib_type_struct # Precondition of calling this function. assert old_interface.name == new_interface.name # Parent type. type_comparison = self._types_equal(old_interface.parent_type, new_interface.parent_type) if type_comparison == self.TYPES_SUPERTYPE: # new_interface.parent_type is a subtype of # old_interface.parent_type self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Interface ‘%s’ has changed parent type from ' '‘%s’ to its subtype ‘%s’.' % (self._format_name(old_interface), old_interface.parent_type, new_interface.parent_type)) elif type_comparison != self.TYPES_EQUAL: # The parent types are in a supertype relationship in the wrong # direction, or not related at all self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Interface ‘%s’ has changed parent type from ' '‘%s’ to ‘%s’.' % (self._format_name(old_interface), old_interface.parent_type, new_interface.parent_type)) # Prerequisite interfaces. NOTE: We do not compare interfaces in @both # for equality, since they're just #Types. (a, both, b) = self._lists_equal(old_interface.prerequisites, new_interface.prerequisites, lambda x: str(x)) for old_type in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Prerequisite ‘%s’ has been removed from ' 'interface ‘%s’.' % (old_type, self._format_name(old_interface))) for new_type in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Prerequisite ‘%s’ has been added to ' 'interface ‘%s’.' % (new_type, self._format_name(new_interface))) # Methods. (a, both, b) = self._lists_equal(old_interface.methods, new_interface.methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Virtual methods. (a, both, b) = self._lists_equal(old_interface.virtual_methods, new_interface.virtual_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Virtual method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Virtual method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_virtual_functions(old_method, new_method) # Static methods. (a, both, b) = self._lists_equal(old_interface.static_methods, new_interface.static_methods, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Static method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Constructors. (a, both, b) = self._lists_equal(old_interface.constructors, new_interface.constructors, lambda x: x.name) for old_method in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been removed.' % self._format_name(old_method)) for new_method in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Constructor method ‘%s’ has been added.' % self._format_name(new_method)) for (old_method, new_method) in both: self._compare_functions(old_method, new_method) # Properties. (a, both, b) = self._lists_equal(old_interface.properties, new_interface.properties, lambda x: x.name) for old_property in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Property ‘%s’ has been removed.' % self._format_name(old_property)) for new_property in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Property ‘%s’ has been added.' % self._format_name(new_property)) for (old_property, new_property) in both: self._compare_properties(old_property, new_property) # Signals. (a, both, b) = self._lists_equal(old_interface.signals, new_interface.signals, lambda x: x.name) for old_signal in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Signal ‘%s’ has been removed.' % self._format_name(old_signal)) for new_signal in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Signal ‘%s’ has been added.' % self._format_name(new_signal)) for (old_signal, new_signal) in both: self._compare_signals(old_signal, new_signal) # Fields. (a, both, b) = self._lists_equal(old_interface.fields, new_interface.fields, lambda x: x.name) for old_field in a: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Field ‘%s’ has been removed.' % self._format_name(old_field)) for new_field in b: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Field ‘%s’ has been added.' % self._format_name(new_field)) for (old_field, new_field) in both: self._compare_fields(old_field, new_field) def _compare_aliases(self, old_alias, new_alias): # TODO: ctype # Precondition of calling this function. assert old_alias.name == new_alias.name type_comparison = self._types_equal(old_alias.target, new_alias.target) if type_comparison == self.TYPES_SUPERTYPE: self._issue_output(self.OUTPUT_FORWARDS_INCOMPATIBLE, 'Alias ‘%s’ has changed type from ‘%s’ to ' 'its subtype ‘%s’.' % (self._format_name(new_alias), old_alias.target, new_alias.target)) elif type_comparison != self.TYPES_EQUAL: self._issue_output(self.OUTPUT_BACKWARDS_INCOMPATIBLE, 'Alias ‘%s’ has changed type from ‘%s’ to ' '‘%s’.' % (self._format_name(new_alias), old_alias.target, new_alias.target)) def _compare_callbacks(self, old_callback, new_callback): # TODO: ctype # Precondition of calling this function. assert old_callback.name == new_callback.name self._compare_callables(old_callback, new_callback) def _compare_unions(self, old_union, new_union): self._compare_compounds(old_union, new_union) def _lists_equal(self, list_a, list_b, id_func): """ Compare two lists of objects using the IDs returned by calling @id_func on them. Return a tuple (elements_in_a_only, elements_in_both, elements_in_b_only). elements_in_both contains tuples of the element in list A and in list B. This assumes that the objects in the lists are of the same type, and that the ID returned by @id_func is stable for comparisons. """ # Convert the two to dictionaries for fast comparisons. dict_a = {} dict_b = {} for obj in list_a: dict_a[id_func(obj)] = obj for obj in list_b: dict_b[id_func(obj)] = obj # Output objs_in_a_only = [] objs_in_both = [] objs_in_b_only = [] for obj in list_a: if id_func(obj) in dict_b: objs_in_both.append((obj, dict_b[id_func(obj)])) else: objs_in_a_only.append(obj) for obj in list_b: if id_func(obj) not in dict_a: objs_in_b_only.append(obj) return (objs_in_a_only, objs_in_both, objs_in_b_only) # FIXME: parent should be replaced by the proper parenting infrastructure # in ast.Annotated or similar def _format_name(self, node, parent=None): if isinstance(node, ast.Node) or isinstance(node, ast.Member): output = node.name while True: node = node.parent if isinstance(node, ast.Namespace): break output = node.name + '.' + output output = node.name + '.' + output # namespace elif isinstance(node, ast.Parameter): output = node.argname if parent is not None: output = self._format_name(parent) + '.' + output else: output = node.name return output