[gobject-introspection/wip/api-diff] WIP work to add g-ir-diff



commit 4a7e8b0d72ae9d7b8395f899732a1c40145c38fe
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Tue Mar 31 17:12:42 2015 +0100

    WIP work to add g-ir-diff

 g-ir-diff                        |  121 ++++
 giscanner/gircomparator.py       | 1196 ++++++++++++++++++++++++++++++++++++++
 tests/scanner/test_comparator.py |  114 ++++
 3 files changed, 1431 insertions(+), 0 deletions(-)
---
diff --git a/g-ir-diff b/g-ir-diff
new file mode 100755
index 0000000..c27993c
--- /dev/null
+++ b/g-ir-diff
@@ -0,0 +1,121 @@
+#!/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
+import __builtin__
+
+if os.name == 'nt':
+    datadir = os.path.join(os.path.dirname(__file__), '..', 'share')
+else:
+    datadir = "/opt/gnome3/build/share"
+
+__builtin__.__dict__['DATADIR'] = datadir
+
+if 'GI_SCANNER_DEBUG' in os.environ:
+    def on_exception(exctype, value, tb):
+        print("Caught exception: %r %r" % (exctype, value))
+        import pdb
+        pdb.pm()
+    sys.excepthook = on_exception
+
+srcdir = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None)
+if srcdir is not None:
+    path = srcdir
+else:
+    # This is a private directory, we don't want to pollute the global
+    # namespace.
+    if os.name == 'nt':
+        # Makes g-ir-scanner 'relocatable' at runtime on Windows.
+        path = os.path.join(os.path.dirname(__file__),
+                            '..', 'lib', 'gobject-introspection')
+    else:
+        # TODO
+        path = os.path.join('/opt/gnome3/build/lib', 'gobject-introspection')
+sys.path.insert(0, path)
+
+import argparse
+from giscanner.girparser import GIRParser
+from giscanner.gircomparator import GIRComparator
+
+# Warning categories.
+WARNING_CATEGORIES = [
+    'info',
+    'backwards-compatibility',
+    'forwards-compatibility',
+]
+
+
+if __name__ == '__main__':
+    # Parse command line arguments.
+    parser = argparse.ArgumentParser(
+        description='Comparing GIR APIs for stability')
+    parser.add_argument('old_file', type=str, help='Old GIR file')
+    parser.add_argument('new_file', type=str, help='New GIR file')
+    parser.add_argument('--warnings', dest='warnings', metavar='CATEGORY,…',
+                        type=str,
+                        help='Warning categories (%s)' %
+                             ', '.join(WARNING_CATEGORIES))
+
+    args = parser.parse_args()
+
+    if not args.old_file or not args.new_file:
+        parser.print_help()
+        sys.exit(1)
+
+    if args.warnings is None:
+        # Enable all warnings by default
+        _enabled_warnings = WARNING_CATEGORIES
+    else:
+        _enabled_warnings = args.warnings.split(',')
+
+    enabled_warnings = []
+    i = 0
+    for category in _enabled_warnings:
+        if category not in WARNING_CATEGORIES:
+            parser.print_help()
+            sys.exit(1)
+
+        # TODO: this is really unneat
+        enabled_warnings.append(i)
+        i += 1
+
+    # Parse the two files.
+    old_parser = GIRParser()
+    new_parser = GIRParser()
+
+    try:
+        filename = args.old_file
+        old_parser.parse(args.old_file)
+        filename = args.new_file
+        new_parser.parse(args.new_file)
+    except Exception as e:
+        sys.stderr.write('Error parsing ‘%s’:\n' % filename)
+        sys.stderr.write(e)
+        sys.exit(1)
+
+    old_namespace = old_parser.get_namespace()
+    new_namespace = new_parser.get_namespace()
+
+    # Compare the interfaces.
+    comparator = GIRComparator(old_namespace, new_namespace, enabled_warnings)
+    out = comparator.compare()
+    comparator.print_output()
+    sys.exit(out)
diff --git a/giscanner/gircomparator.py b/giscanner/gircomparator.py
new file mode 100644
index 0000000..1e78453
--- /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 <include>, <package>, <c:include> 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
diff --git a/tests/scanner/test_comparator.py b/tests/scanner/test_comparator.py
new file mode 100755
index 0000000..b6b2649
--- /dev/null
+++ b/tests/scanner/test_comparator.py
@@ -0,0 +1,114 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+# TODO: copyright
+
+import unittest
+import tempfile
+import os
+import sys
+import __builtin__
+
+
+os.environ['GI_SCANNER_DISABLE_CACHE'] = '1'
+path = os.getenv('UNINSTALLED_INTROSPECTION_SRCDIR', None)
+assert path is not None
+sys.path.insert(0, path)
+
+# Not correct, but enough to get the tests going uninstalled
+__builtin__.__dict__['DATADIR'] = path
+
+from giscanner.girparser import GIRParser
+from giscanner.gircomparator import GIRComparator
+from giscanner.message import MessageLogger, WARNING, ERROR, FATAL
+
+
+class TestComparatorErrors(unittest.TestCase):
+    def _create_temp_gir_file(self, xml):
+        tmp_fd, tmp_name = tempfile.mkstemp(suffix='.gir', text=True)
+        file = os.fdopen(tmp_fd, 'wt')
+        file.write(xml)
+        file.close()
+
+        return tmp_name
+
+    def _test_comparator(self, old_xml, new_xml, wrap=True):
+        # Wrap the files in a repository and namespace for convenience.
+        if wrap:
+            old_xml = self._wrap_gir(old_xml)
+            new_xml = self._wrap_gir(new_xml)
+
+        old_tmpfile = self._create_temp_gir_file(old_xml)
+        new_tmpfile = self._create_temp_gir_file(new_xml)
+
+        old_parser = GIRParser()
+        new_parser = GIRParser()
+
+        old_parser.parse(old_tmpfile)
+        new_parser.parse(new_tmpfile)
+
+        old_namespace = old_parser.get_namespace()
+        new_namespace = new_parser.get_namespace()
+
+        os.unlink(new_tmpfile)
+        os.unlink(old_tmpfile)
+
+        self.assertNotEqual(old_namespace, None)
+        self.assertNotEqual(new_namespace, None)
+
+        return GIRComparator(old_namespace, new_namespace)
+
+    def _wrap_gir(self, xml):
+        return ('<?xml version="1.0"?>'
+                '<repository version="1.2" '
+                        'xmlns="http://www.gtk.org/introspection/core/1.0"; '
+                        'xmlns:c="http://www.gtk.org/introspection/c/1.0";>'
+                    '<include name="GObject" version="2.0"/>'
+                    '<include name="Gio" version="2.0"/>'
+                    '<namespace name="N" '
+                               'version="1.0" '
+                               'shared-library="libtest-1.0.so.0" '
+                               'c:identifier-prefixes="Test" '
+                               'c:symbol-prefixes="test">'
+                        '%s'
+                    '</namespace>'
+                '</repository>') % xml
+
+    def assertSuccess(self, old_xml, new_xml, wrap=True):
+        comparator = self._test_comparator(old_xml, new_xml, wrap)
+        self.assertEqual(comparator.compare(), 0)
+
+    def assertErrors(self, old_xml, new_xml, errors, wrap=True):
+        comparator = self._test_comparator(old_xml, new_xml, wrap)
+        self.assertNotEqual(comparator.compare(), 0)
+
+        output = comparator.get_output()
+        self.assertEqual(output, errors)
+
+    def assertInfos(self, old_xml, new_xml, infos, wrap=True):
+        comparator = self._test_comparator(old_xml, new_xml, wrap)
+        self.assertEqual(comparator.compare(), 0)
+
+        output = comparator.get_output()
+        self.assertEqual(output, infos)
+
+    def test_alias_type_changed(self):
+        self.assertErrors(
+            '<alias name="A"><type name="GObject.Object"/></alias>',
+            '<alias name="A"><type name="Gio.InputStream"/></alias>',
+            [
+                (GIRComparator.OUTPUT_FORWARDS_INCOMPATIBLE,
+                 'Alias ‘N.A’ has changed type from ‘GObject.Object’ to its '
+                 'subtype ‘Gio.InputStream’.'),
+            ])
+        self.assertErrors(
+            '<alias name="A"><type name="Gio.InputStream"/></alias>',
+            '<alias name="A"><type name="GObject.Object"/></alias>',
+            [
+                (GIRComparator.OUTPUT_BACKWARDS_INCOMPATIBLE,
+                 'Alias ‘N.A’ has changed type from ‘Gio.InputStream’ to '
+                 '‘GObject.Object’.'),
+            ])
+
+
+if __name__ == '__main__':
+    unittest.main()


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]