From 4a7e8b0d72ae9d7b8395f899732a1c40145c38fe Mon Sep 17 00:00:00 2001 From: Philip Withnall Date: Tue, 31 Mar 2015 17:12:42 +0100 Subject: WIP work to add g-ir-diff --- giscanner/gircomparator.py | 1196 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1196 insertions(+) create mode 100644 giscanner/gircomparator.py (limited to 'giscanner/gircomparator.py') diff --git a/giscanner/gircomparator.py b/giscanner/gircomparator.py new file mode 100644 index 00000000..1e784534 --- /dev/null +++ b/giscanner/gircomparator.py @@ -0,0 +1,1196 @@ +#!/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 -- cgit v1.2.1