diff options
| -rw-r--r-- | CMakeLists.txt | 2 | ||||
| -rwxr-xr-x | find-modules/rules_engine.py | 526 | ||||
| -rw-r--r-- | find-modules/sip_generator.py | 700 | 
3 files changed, 1227 insertions, 1 deletions
| diff --git a/CMakeLists.txt b/CMakeLists.txt index 94e88306..fe7ebac5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,7 +117,7 @@ install(FILES ${installModuleFiles} DESTINATION ${MODULES_INSTALL_DIR})  file(GLOB installKdeModuleFiles ${CMAKE_SOURCE_DIR}/kde-modules/*[^~])  install(FILES ${installKdeModuleFiles} DESTINATION ${KDE_MODULES_INSTALL_DIR}) -file(GLOB installFindModuleFiles ${CMAKE_SOURCE_DIR}/find-modules/*[^~]) +file(GLOB installFindModuleFiles ${CMAKE_SOURCE_DIR}/find-modules/*.cmake ${CMAKE_SOURCE_DIR}/find-modules/*.py)  install(FILES ${installFindModuleFiles} DESTINATION ${FIND_MODULES_INSTALL_DIR})  file(GLOB installToolchainModuleFiles ${CMAKE_SOURCE_DIR}/toolchain/*[^~]) diff --git a/find-modules/rules_engine.py b/find-modules/rules_engine.py new file mode 100755 index 00000000..53c81413 --- /dev/null +++ b/find-modules/rules_engine.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python +#============================================================================= +# Copyright 2016 by Shaheed Haque (srhaque@theiet.org) +# Copyright 2016 Stephen Kelly <steveire@gmail.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +#    notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +#    notice, this list of conditions and the following disclaimer in the +#    documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +#    derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +"""SIP file generation rules engine.""" + +from __future__ import print_function + +from abc import * +import argparse +import gettext +import inspect +import logging +import os +import re +import sys +import textwrap +import traceback +from copy import deepcopy +from clang.cindex import CursorKind + +from clang.cindex import AccessSpecifier + +class HelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): +    pass + + +logger = logging.getLogger(__name__) +gettext.install(__name__) +_SEPARATOR = "\x00" + +def _parents(container): +    parents = [] +    parent = container.semantic_parent +    while parent and parent.kind != CursorKind.TRANSLATION_UNIT: +        parents.append(parent.spelling) +        parent = parent.semantic_parent +    if parents: +        parents = "::".join(reversed(parents)) +    else: +        parents = os.path.basename(container.translation_unit.spelling) +    return parents + + +class Rule(object): +    def __init__(self, db, rule_number, fn, pattern_zip): +        self.db = db +        self.rule_number = rule_number +        self.fn = fn +        self.usage = 0 +        try: +            groups = ["(?P<{}>{})".format(name, pattern) for pattern, name in pattern_zip] +            groups = _SEPARATOR.join(groups) +            self.matcher = re.compile(groups) +        except Exception as e: +            groups = ["{} '{}'".format(name, pattern) for pattern, name in pattern_zip] +            groups = ", ".join(groups) +            raise RuntimeError(_("Bad {}: {}: {}").format(self, groups, e)) + +    def match(self, candidate): +        return self.matcher.match(candidate) + +    def trace_result(self, parents, item, original, modified): +        fqn = parents + "::" + original["name"] + "[" + str(item.extent.start.line) + "]" +        self._trace_result(fqn, original, modified) + +    def _trace_result(self, fqn, original, modified): +        if not modified["name"]: +            logger.debug(_("Rule {} suppressed {}, {}").format(self, fqn, original)) +        else: +            delta = False +            for k, v in original.iteritems(): +                if v != modified[k]: +                    delta = True +                    break +            if delta: +                logger.debug(_("Rule {} modified {}, {}->{}").format(self, fqn, original, modified)) +            else: +                logger.warn(_("Rule {} did not modify {}, {}").format(self, fqn, original)) + +    def __str__(self): +        return "[{},{}]".format(self.rule_number, self.fn.__name__) + + +class AbstractCompiledRuleDb(object): +    __metaclass__ = ABCMeta + +    def __init__(self, db, parameter_names): +        self.db = db +        self.compiled_rules = [] +        for i, raw_rule in enumerate(db()): +            if len(raw_rule) != len(parameter_names) + 1: +                raise RuntimeError(_("Bad raw rule {}: {}: {}").format(db.__name__, raw_rule, parameter_names)) +            z = zip(raw_rule[:-1], parameter_names) +            self.compiled_rules.append(Rule(db, i, raw_rule[-1], z)) +        self.candidate_formatter = _SEPARATOR.join(["{}"] * len(parameter_names)) + +    def _match(self, *args): +        candidate = self.candidate_formatter.format(*args) +        for rule in self.compiled_rules: +            matcher = rule.match(candidate) +            if matcher: +                # +                # Only use the first matching rule. +                # +                rule.usage += 1 +                return matcher, rule +        return None, None + +    @abstractmethod +    def apply(self, *args): +        raise NotImplemented(_("Missing subclass")) + +    def dump_usage(self, fn): +        """ Dump the usage counts.""" +        for rule in self.compiled_rules: +            fn(self.__class__.__name__, str(rule), rule.usage) + + +class ContainerRuleDb(AbstractCompiledRuleDb): +    """ +    THE RULES FOR CONTAINERS. + +    These are used to customise the behaviour of the SIP generator by allowing +    the declaration for any container (class, namespace, struct, union) to be +    customised, for example to add SIP compiler annotations. + +    Each entry in the raw rule database must be a list with members as follows: + +        0. A regular expression which matches the fully-qualified name of the +        "container" enclosing the container. + +        1. A regular expression which matches the container name. + +        2. A regular expression which matches any template parameters. + +        3. A regular expression which matches the container declaration. + +        4. A regular expression which matches any base specifiers. + +        5. A function. + +    In use, the database is walked in order from the first entry. If the regular +    expressions are matched, the function is called, and no further entries are +    walked. The function is called with the following contract: + +        def container_xxx(container, sip, matcher): +            ''' +            Return a modified declaration for the given container. + +            :param container:   The clang.cindex.Cursor for the container. +            :param sip:         A dict with the following keys: + +                                    name                The name of the container. +                                    template_parameters Any template parameters. +                                    decl                The declaration. +                                    base_specifiers     Any base specifiers. +                                    body                The body, less the outer +                                                        pair of braces. +                                    annotations         Any SIP annotations. + +            :param matcher:         The re.Match object. This contains named +                                    groups corresponding to the key names above +                                    EXCEPT body and annotations. + +            :return: An updated set of sip.xxx values. Setting sip.name to the +                     empty string will cause the container to be suppressed. +            ''' + +    :return: The compiled form of the rules. +    """ +    def __init__(self, db): +        super(ContainerRuleDb, self).__init__(db, ["parents", "container", "template_parameters", "decl", "base_specifiers"]) + +    def apply(self, container, sip): +        """ +        Walk over the rules database for functions, applying the first matching transformation. + +        :param container:           The clang.cindex.Cursor for the container. +        :param sip:                 The SIP dict. +        """ +        parents = _parents(container) +        matcher, rule = self._match(parents, sip["name"], sip["template_parameters"], sip["decl"], sip["base_specifiers"]) +        if matcher: +            before = deepcopy(sip) +            rule.fn(container, sip, matcher) +            rule.trace_result(parents, container, before, sip) + + +class FunctionRuleDb(AbstractCompiledRuleDb): +    """ +    THE RULES FOR FUNCTIONS. + +    These are used to customise the behaviour of the SIP generator by allowing +    the declaration for any function to be customised, for example to add SIP +    compiler annotations. + +    Each entry in the raw rule database must be a list with members as follows: + +        0. A regular expression which matches the fully-qualified name of the +        "container" enclosing the function. + +        1. A regular expression which matches the function name. + +        2. A regular expression which matches any template parameters. + +        3. A regular expression which matches the function result. + +        4. A regular expression which matches the function parameters (e.g. +        "int a, void *b" for "int foo(int a, void *b)"). + +        5. A function. + +    In use, the database is walked in order from the first entry. If the regular +    expressions are matched, the function is called, and no further entries are +    walked. The function is called with the following contract: + +        def function_xxx(container, function, sip, matcher): +            ''' +            Return a modified declaration for the given function. + +            :param container:   The clang.cindex.Cursor for the container. +            :param function:    The clang.cindex.Cursor for the function. +            :param sip:         A dict with the following keys: + +                                    name                The name of the function. +                                    template_parameters Any template parameters. +                                    fn_result           Result, if not a constructor. +                                    decl                The declaration. +                                    prefix              Leading keyworks ("static"). Separated by space, +                                                        ends with a space. +                                    suffix              Trailing keywords ("const"). Separated by space, starts with +                                                        space. +                                    annotations         Any SIP annotations. + +            :param matcher:         The re.Match object. This contains named +                                    groups corresponding to the key names above +                                    EXCEPT annotations. + +            :return: An updated set of sip.xxx values. Setting sip.name to the +                     empty string will cause the container to be suppressed. +            ''' + +    :return: The compiled form of the rules. +    """ +    def __init__(self, db): +        super(FunctionRuleDb, self).__init__(db, ["container", "function", "template_parameters", "fn_result", "parameters"]) + +    def apply(self, container, function, sip): +        """ +        Walk over the rules database for functions, applying the first matching transformation. + +        :param container:           The clang.cindex.Cursor for the container. +        :param function:            The clang.cindex.Cursor for the function. +        :param sip:                 The SIP dict. +        """ +        parents = _parents(function) +        matcher, rule = self._match(parents, sip["name"], ", ".join(sip["template_parameters"]), sip["fn_result"], ", ".join(sip["parameters"])) +        if matcher: +            before = deepcopy(sip) +            rule.fn(container, function, sip, matcher) +            rule.trace_result(parents, function, before, sip) + + +class ParameterRuleDb(AbstractCompiledRuleDb): +    """ +    THE RULES FOR FUNCTION PARAMETERS. + +    These are used to customise the behaviour of the SIP generator by allowing +    the declaration for any parameter in any function to be customised, for +    example to add SIP compiler annotations. + +    Each entry in the raw rule database must be a list with members as follows: + +        0. A regular expression which matches the fully-qualified name of the +        "container" enclosing the function enclosing the parameter. + +        1. A regular expression which matches the function name enclosing the +        parameter. + +        2. A regular expression which matches the parameter name. + +        3. A regular expression which matches the parameter declaration (e.g. +        "int foo"). + +        4. A regular expression which matches the parameter initialiser (e.g. +        "Xyz:MYCONST + 42"). + +        5. A function. + +    In use, the database is walked in order from the first entry. If the regular +    expressions are matched, the function is called, and no further entries are +    walked. The function is called with the following contract: + +        def parameter_xxx(container, function, parameter, sip, init, matcher): +            ''' +            Return a modified declaration and initialiser for the given parameter. + +            :param container:   The clang.cindex.Cursor for the container. +            :param function:    The clang.cindex.Cursor for the function. +            :param parameter:   The clang.cindex.Cursor for the parameter. +            :param sip:         A dict with the following keys: + +                                    name                The name of the function. +                                    decl                The declaration. +                                    init                Any initialiser. +                                    annotations         Any SIP annotations. + +            :param matcher:         The re.Match object. This contains named +                                    groups corresponding to the key names above +                                    EXCEPT annotations. + +            :return: An updated set of sip.xxx values. +        ''' + +    :return: The compiled form of the rules. +    """ +    def __init__(self, db): +        super(ParameterRuleDb, self).__init__(db, ["container", "function", "parameter", "decl", "init"]) + +    def apply(self, container, function, parameter, sip): +        """ +        Walk over the rules database for parameters, applying the first matching transformation. + +        :param container:           The clang.cindex.Cursor for the container. +        :param function:            The clang.cindex.Cursor for the function. +        :param parameter:           The clang.cindex.Cursor for the parameter. +        :param sip:                 The SIP dict. +        """ +        parents = _parents(function) +        matcher, rule = self._match(parents, function.spelling, sip["name"], sip["decl"], sip["init"]) +        if matcher: +            before = deepcopy(sip) +            rule.fn(container, function, parameter, sip, matcher) +            rule.trace_result(parents, parameter, before, sip) + + +class VariableRuleDb(AbstractCompiledRuleDb): +    """ +    THE RULES FOR VARIABLES. + +    These are used to customise the behaviour of the SIP generator by allowing +    the declaration for any variable to be customised, for example to add SIP +    compiler annotations. + +    Each entry in the raw rule database must be a list with members as follows: + +        0. A regular expression which matches the fully-qualified name of the +        "container" enclosing the variable. + +        1. A regular expression which matches the variable name. + +        2. A regular expression which matches the variable declaration (e.g. +        "int foo"). + +        3. A function. + +    In use, the database is walked in order from the first entry. If the regular +    expressions are matched, the function is called, and no further entries are +    walked. The function is called with the following contract: + +        def variable_xxx(container, variable, sip, matcher): +            ''' +            Return a modified declaration for the given variable. + +            :param container:   The clang.cindex.Cursor for the container. +            :param variable:    The clang.cindex.Cursor for the variable. +            :param sip:         A dict with the following keys: + +                                    name                The name of the variable. +                                    decl                The declaration. +                                    annotations         Any SIP annotations. + +            :param matcher:         The re.Match object. This contains named +                                    groups corresponding to the key names above +                                    EXCEPT annotations. + +            :return: An updated set of sip.xxx values. Setting sip.name to the +                     empty string will cause the container to be suppressed. +            ''' + +    :return: The compiled form of the rules. +    """ +    def __init__(self, db): +        super(VariableRuleDb, self).__init__(db, ["container", "variable", "decl"]) + +    def apply(self, container, variable, sip): +        """ +        Walk over the rules database for variables, applying the first matching transformation. + +        :param container:           The clang.cindex.Cursor for the container. +        :param variable:            The clang.cindex.Cursor for the variable. +        :param sip:                 The SIP dict. +        """ +        parents = _parents(variable) +        matcher, rule = self._match(parents, sip["name"], sip["decl"]) +        if matcher: +            before = deepcopy(sip) +            rule.fn(container, variable, sip, matcher) +            rule.trace_result(parents, variable, before, sip) + + +class RuleSet(object): +    """ +    To implement your own binding, create a subclass of RuleSet, also called +    RuleSet in your own Python module. Your subclass will expose the raw rules +    along with other ancilliary data exposed through the subclass methods. + +    You then simply run the SIP generation and SIP compilation programs passing +    in the name of your rules file +    """ +    __metaclass__ = ABCMeta + +    @abstractmethod +    def container_rules(self): +        """ +        Return a compiled list of rules for containers. + +        :return: A ContainerRuleDb instance +        """ +        raise NotImplemented(_("Missing subclass implementation")) + +    @abstractmethod +    def function_rules(self): +        """ +        Return a compiled list of rules for functions. + +        :return: A FunctionRuleDb instance +        """ +        raise NotImplemented(_("Missing subclass implementation")) + +    @abstractmethod +    def parameter_rules(self): +        """ +        Return a compiled list of rules for function parameters. + +        :return: A ParameterRuleDb instance +        """ +        raise NotImplemented(_("Missing subclass implementation")) + +    @abstractmethod +    def variable_rules(self): +        """ +        Return a compiled list of rules for variables. + +        :return: A VariableRuleDb instance +        """ +        raise NotImplemented(_("Missing subclass implementation")) + +    def dump_unused(self): +        """Usage statistics, to identify unused rules.""" +        def dumper(db_name, rule, usage): +            if usage: +                logger.info(_("Rule {}::{} used {} times".format(db_name, rule, usage))) +            else: +                logger.warn(_("Rule {}::{} unused".format(db_name, rule))) +        for db in [self.container_rules(), self.function_rules(), self.parameter_rules(), +                   self.variable_rules()]: +            db.dump_usage(dumper) + + +def container_discard(container, sip, matcher): +    sip["name"] = "" + +def function_discard(container, function, sip, matcher): +    sip["name"] = "" + +def parameter_transfer_to_parent(container, function, parameter, sip, matcher): +    if function.is_static_method(): +        sip["annotations"].add("Transfer") +    else: +        sip["annotations"].add("TransferThis") + +def param_rewrite_mode_t_as_int(container, function, parameter, sip, matcher): +    sip["decl"] = sip["decl"].replace("mode_t", "unsigned int") + +def return_rewrite_mode_t_as_int(container, function, sip, matcher): +    sip["fn_result"] = "unsigned int" + +def variable_discard(container, variable, sip, matcher): +    sip["name"] = "" + +def parameter_strip_class_enum(container, function, parameter, sip, matcher): +    sip["decl"] = sip["decl"].replace("class ", "").replace("enum ", "") + +def function_discard_impl(container, function, sip, matcher): +    if function.extent.start.column == 1: +        sip["name"] = "" + +def rules(project_rules): +    """ +    Constructor. + +    :param project_rules:       The rules file for the project. +    """ +    import imp +    imp.load_source("project_rules", project_rules) +    # +    # Statically prepare the rule logic. This takes the rules provided by the user and turns them into code. +    # +    return getattr(sys.modules["project_rules"], "RuleSet")() diff --git a/find-modules/sip_generator.py b/find-modules/sip_generator.py new file mode 100644 index 00000000..10be1477 --- /dev/null +++ b/find-modules/sip_generator.py @@ -0,0 +1,700 @@ +#!/usr/bin/env python +#============================================================================= +# Copyright 2016 by Shaheed Haque (srhaque@theiet.org) +# Copyright 2016 by Stephen Kelly (steveire@gmail.com) +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the copyright +#    notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the copyright +#    notice, this list of conditions and the following disclaimer in the +#    documentation and/or other materials provided with the distribution. +# 3. The name of the author may not be used to endorse or promote products +#    derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +#============================================================================= + +"""SIP file generator for PyQt.""" + +from __future__ import print_function +import argparse +import gettext +import inspect +import logging +import os +import re +import subprocess +import sys +import traceback +from clang import cindex +from clang.cindex import AccessSpecifier, CursorKind, SourceRange, StorageClass, TokenKind, TypeKind, TranslationUnit + +import rules_engine + + +class HelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): +    pass + + +logger = logging.getLogger(__name__) +gettext.install(__name__) + +EXPR_KINDS = [ +    CursorKind.UNEXPOSED_EXPR, +    CursorKind.CONDITIONAL_OPERATOR, CursorKind.UNARY_OPERATOR, CursorKind.BINARY_OPERATOR, +    CursorKind.INTEGER_LITERAL, CursorKind.FLOATING_LITERAL, CursorKind.STRING_LITERAL, +    CursorKind.CXX_BOOL_LITERAL_EXPR, CursorKind.CXX_STATIC_CAST_EXPR, CursorKind.DECL_REF_EXPR +] +TEMPLATE_KINDS = [ +                     CursorKind.TYPE_REF, CursorKind.TEMPLATE_REF, CursorKind.NAMESPACE_REF +                 ] + EXPR_KINDS + + +class SipGenerator(object): +    def __init__(self, project_rules, compile_flags, verbose=False, dump_includes=False, dump_privates=False): +        """ +        Constructor. + +        :param project_rules:       The rules for the file. +        :param compile_flags:       The compile flags for the file. +        :param dump_includes:       Turn on diagnostics for include files. +        :param dump_privates:       Turn on diagnostics for omitted private items. +        """ +        self.rules = project_rules +        self.compile_flags = compile_flags +        self.verbose = verbose +        self.dump_includes = dump_includes +        self.dump_privates = dump_privates +        self.diagnostics = set() +        self.tu = None +        self.unpreprocessed_source = None + +    @staticmethod +    def describe(cursor, text=None): +        if not text: +            text = cursor.spelling +        return "{} on line {} '{}'".format(cursor.kind.name, cursor.extent.start.line, text) + +    def create_sip(self, h_file, include_filename): +        """ +        Actually convert the given source header file into its SIP equivalent. + +        :param h_file:              The source (header) file of interest. +        :param include_filename:    The (header) to generate in the sip file. +        """ + +        # +        # Read in the original file. +        # +        source = h_file +        self.unpreprocessed_source = [] +        with open(source, "rU") as f: +            for line in f: +                self.unpreprocessed_source.append(line) + +        index = cindex.Index.create() +        self.tu = index.parse(source, ["-x", "c++"] + self.compile_flags) +        for diag in self.tu.diagnostics: +            # +            # We expect to be run over hundreds of files. Any parsing issues are likely to be very repetitive. +            # So, to avoid bothering the user, we suppress duplicates. +            # +            loc = diag.location +            msg = "{}:{}[{}] {}".format(loc.file, loc.line, loc.column, diag.spelling) +            if msg in self.diagnostics: +                continue +            self.diagnostics.add(msg) +            logger.log(diag.severity, "Parse error {}".format(msg)) +        if self.dump_includes: +            for include in sorted(set(self.tu.get_includes())): +                logger.debug(_("Used includes {}").format(include.include.name)) +        # +        # Run through the top level children in the translation unit. +        # +        body = self._container_get(self.tu.cursor, -1, h_file, include_filename) +        return body, self.tu.get_includes + +    CONTAINER_SKIPPABLE_UNEXPOSED_DECL = re.compile("_DECLARE_PRIVATE|friend|;") +    FN_SKIPPABLE_ATTR = re.compile("_EXPORT|Q_REQUIRED_RESULT|format\(printf") +    VAR_SKIPPABLE_ATTR = re.compile("_EXPORT") +    TYPEDEF_SKIPPABLE_ATTR = re.compile("_EXPORT") + +    def _container_get(self, container, level, h_file, include_filename): +        """ +        Generate the (recursive) translation for a class or namespace. + +        :param container:           A class or namespace. +        :param h_file:              Name of header file being processed. +        :param level:               Recursion level controls indentation. +        :return:                    A string. +        """ + +        def skippable_attribute(member, text): +            """ +            We don't seem to have access to the __attribute__(())s, but at least we can look for stuff we care about. + +            :param member:          The attribute. +            :param text:            The raw source corresponding to the region of member. +            """ +            if text.find("_DEPRECATED") != -1: +                sip["annotations"].add("Deprecated") +                return True +            SipGenerator._report_ignoring(container, member, text) + +        sip = { +            "name": container.displayname, +            "annotations": set() +        } +        name = container.displayname +        if container.access_specifier == AccessSpecifier.PRIVATE: +            if self.dump_privates: +                logger.debug("Ignoring private {}".format(SipGenerator.describe(container))) +            return "" +        body = "" +        base_specifiers = [] +        template_type_parameters = [] +        had_copy_constructor = False +        had_const_member = False +        for member in container.get_children(): +            # +            # Only emit items in the translation unit. +            # +            if member.location.file.name != self.tu.spelling: +                continue +            decl = "" +            if member.kind in [CursorKind.CXX_METHOD, CursorKind.FUNCTION_DECL, CursorKind.FUNCTION_TEMPLATE, +                               CursorKind.CONSTRUCTOR, CursorKind.DESTRUCTOR, CursorKind.CONVERSION_FUNCTION]: +                decl = self._fn_get(container, member, level + 1) +            elif member.kind == CursorKind.ENUM_DECL: +                decl = self._enum_get(container, member, level + 1) + ";\n" +            elif member.kind == CursorKind.CXX_ACCESS_SPEC_DECL: +                decl = self._get_access_specifier(member, level + 1) +            elif member.kind == CursorKind.TYPEDEF_DECL: +                decl = self._typedef_get(member, level + 1) +            elif member.kind == CursorKind.CXX_BASE_SPECIFIER: +                # +                # Strip off the leading "class". Except for TypeKind.UNEXPOSED... +                # +                base_specifiers.append(member.displayname.split(None, 2)[-1]) +            elif member.kind == CursorKind.TEMPLATE_TYPE_PARAMETER: +                template_type_parameters.append(member.displayname) +            elif member.kind == CursorKind.TEMPLATE_NON_TYPE_PARAMETER: +                template_type_parameters.append(member.type.spelling + " " + member.displayname) +            elif member.kind in [CursorKind.VAR_DECL, CursorKind.FIELD_DECL]: +                had_const_member = had_const_member or member.type.is_const_qualified() +                decl = self._var_get(container, member, level + 1) +            elif member.kind in [CursorKind.NAMESPACE, CursorKind.CLASS_DECL, +                                 CursorKind.CLASS_TEMPLATE, CursorKind.CLASS_TEMPLATE_PARTIAL_SPECIALIZATION, +                                 CursorKind.STRUCT_DECL, CursorKind.UNION_DECL]: +                decl = self._container_get(member, level + 1, h_file, include_filename) +            elif member.kind in TEMPLATE_KINDS + [CursorKind.USING_DECLARATION, CursorKind.USING_DIRECTIVE, +                                                  CursorKind.CXX_FINAL_ATTR]: +                # +                # Ignore: +                # +                #   TEMPLATE_KINDS: Template type parameter. +                #   CursorKind.USING_DECLARATION, CursorKind.USING_DIRECTIVE: Using? Pah! +                #   CursorKind.CXX_FINAL_ATTR: Again, not much to be done with this. +                # +                pass +            else: +                SipGenerator._report_ignoring(container, member) + +            def is_copy_constructor(member): +                if member.kind != CursorKind.CONSTRUCTOR: +                    return False +                numParams = 0 +                hasSelfType = False +                for child in member.get_children(): +                    numParams += 1 +                    if child.kind == CursorKind.PARM_DECL: +                        paramType = child.type.spelling +                        paramType = paramType.split("::")[-1] +                        paramType = paramType.replace("const", "").replace("&", "").strip() +                        hasSelfType = paramType == container.displayname +                return numParams == 1 and hasSelfType + +            def has_parameter_default(parameter): +                for member in parameter.get_children(): +                    if member.kind.is_expression(): +                        return True +                return False + +            def is_default_constructor(member): +                if member.kind != CursorKind.CONSTRUCTOR: +                    return False +                numParams = 0 +                for parameter in member.get_children(): +                    if (has_parameter_default(parameter)): +                        break +                    numParams += 1 +                return numParams == 0 + +            had_copy_constructor = had_copy_constructor or is_copy_constructor(member) +            # +            # Discard almost anything which is private. +            # +            if member.access_specifier == AccessSpecifier.PRIVATE: +                if member.kind == CursorKind.CXX_ACCESS_SPEC_DECL: +                    # +                    # We need these because... +                    # +                    pass +                elif is_copy_constructor(member) or is_default_constructor(member): +                    # +                    # ...we need to pass private copy contructors to the SIP compiler. +                    # +                    pass +                else: +                    if self.dump_privates: +                        logger.debug("Ignoring private {}".format(SipGenerator.describe(member))) +                    continue + +            if decl: +                if self.verbose: +                    pad = " " * ((level + 1) * 4) +                    body += pad + "// {}\n".format(SipGenerator.describe(member)) +                body += decl +        # +        # Empty containers are still useful if they provide namespaces or forward declarations. +        # +        if not body and level >= 0: +            body = "\n" +            text = self._read_source(container.extent) +            if not text.endswith("}"): +                # +                # Forward declaration. +                # +                sip["annotations"].add("External") +        if body and level >= 0: +            if container.kind == CursorKind.NAMESPACE: +                container_type = "namespace " + name +            elif container.kind in [CursorKind.CLASS_DECL, CursorKind.CLASS_TEMPLATE, +                                    CursorKind.CLASS_TEMPLATE_PARTIAL_SPECIALIZATION]: +                container_type = "class " + name +            elif container.kind == CursorKind.STRUCT_DECL: +                container_type = "struct " + name +            elif container.kind == CursorKind.UNION_DECL: +                container_type = "union " + name +            else: +                raise AssertionError( +                    _("Unexpected container {}: {}[{}]").format(container.kind, name, container.extent.start.line)) +            # +            # Generate private copy constructor for non-copyable types. +            # +            if had_const_member and not had_copy_constructor: +                body += "    private:\n        {}(const {} &); // Generated\n".format(name, container.type.get_canonical().spelling) +            # +            # Flesh out the SIP context for the rules engine. +            # +            sip["template_parameters"] = ", ".join(template_type_parameters) +            sip["decl"] = container_type +            sip["base_specifiers"] = ", ".join(base_specifiers) +            sip["body"] = body +            self.rules.container_rules().apply(container, sip) +            pad = " " * (level * 4) +            if sip["name"]: +                decl = pad + sip["decl"] +                if "External" in sip["annotations"]: +                    # +                    # SIP /External/ does not seem to work as one might wish. Suppress. +                    # +                    body = decl + " /External/;\n" +                    body = pad + "// Discarded {}\n".format(SipGenerator.describe(container)) +                else: +                    if sip["base_specifiers"]: +                        decl += ": " + sip["base_specifiers"] +                    if sip["annotations"]: +                        decl += " /" + ",".join(sip["annotations"]) + "/" +                    if sip["template_parameters"]: +                        decl = pad + "template <" + sip["template_parameters"] + ">\n" + decl +                    decl += "\n" + pad + "{\n" +                    decl += "%TypeHeaderCode\n#include <{}>\n%End\n".format(include_filename) +                    body = decl + sip["body"] + pad + "};\n" +            else: +                body = pad + "// Discarded {}\n".format(SipGenerator.describe(container)) +        return body + +    def _get_access_specifier(self, member, level): +        """ +        Skip access specifiers embedded in the Q_OBJECT macro. +        """ +        access_specifier = self._read_source(member.extent) +        if access_specifier == "Q_OBJECT": +            return "" +        pad = " " * ((level - 1) * 4) +        decl = pad + access_specifier + "\n" +        return decl + +    def _enum_get(self, container, enum, level): +        pad = " " * (level * 4) +        decl = pad + "enum {} {{\n".format(enum.displayname) +        enumerations = [] +        for enum in enum.get_children(): +            enumerations.append(pad + "    {}".format(enum.displayname)) +            assert enum.kind == CursorKind.ENUM_CONSTANT_DECL +        decl += ",\n".join(enumerations) + "\n" +        decl += pad + "}" +        return decl + +    def _fn_get(self, container, function, level): +        """ +        Generate the translation for a function. + +        :param container:           A class or namespace. +        :param function:            The function object. +        :param level:               Recursion level controls indentation. +        :return:                    A string. +        """ + +        def skippable_attribute(member, text): +            """ +            We don't seem to have access to the __attribute__(())s, but at least we can look for stuff we care about. + +            :param member:          The attribute. +            :param text:            The raw source corresponding to the region of member. +            """ +            if SipGenerator.FN_SKIPPABLE_ATTR.search(text): +                return True +            SipGenerator._report_ignoring(function, member, text) + +        sip = { +            "name": function.spelling, +        } +        parameters = [] +        template_parameters = [] +        for child in function.get_children(): +            if child.kind == CursorKind.PARM_DECL: +                parameter = child.displayname or "__{}".format(len(parameters)) +                theType = child.type.get_canonical() +                typeSpelling = theType.spelling +                if theType.kind == TypeKind.POINTER: +                    typeSpelling = theType.get_pointee().spelling + "* " + +                decl = "{} {}".format(typeSpelling, parameter) +                child_sip = { +                    "name": parameter, +                    "decl": decl, +                    "init": self._fn_get_parameter_default(function, child), +                    "annotations": set() +                } +                self.rules.parameter_rules().apply(container, function, child, child_sip) +                decl = child_sip["decl"] +                if child_sip["annotations"]: +                    decl += " /" + ",".join(child_sip["annotations"]) + "/" +                if child_sip["init"]: +                    decl += " = " + child_sip["init"] +                parameters.append(decl) +            elif child.kind in [CursorKind.COMPOUND_STMT, CursorKind.CXX_OVERRIDE_ATTR, +                                CursorKind.MEMBER_REF, CursorKind.DECL_REF_EXPR, CursorKind.CALL_EXPR] + TEMPLATE_KINDS: +                # +                # Ignore: +                # +                #   CursorKind.COMPOUND_STMT: Function body. +                #   CursorKind.CXX_OVERRIDE_ATTR: The "override" keyword. +                #   CursorKind.MEMBER_REF, CursorKind.DECL_REF_EXPR, CursorKind.CALL_EXPR: Constructor initialisers. +                #   TEMPLATE_KINDS: The result type. +                # +                pass +            elif child.kind == CursorKind.TEMPLATE_TYPE_PARAMETER: +                template_parameters.append(child.displayname) +            elif child.kind == CursorKind.TEMPLATE_NON_TYPE_PARAMETER: +                template_parameters.append(child.type.spelling + " " + child.displayname) +            else: +                text = self._read_source(child.extent) +                if child.kind in [CursorKind.UNEXPOSED_ATTR, CursorKind.VISIBILITY_ATTR] and skippable_attribute(child, +                                                                                                                 text): +                    pass +                else: +                    SipGenerator._report_ignoring(function, child) +        # +        # Flesh out the SIP context for the rules engine. +        # +        sip["template_parameters"] = template_parameters +        if function.kind in [CursorKind.CONSTRUCTOR, CursorKind.DESTRUCTOR]: +            sip["fn_result"] = "" +        else: +            sip["fn_result"] = function.result_type.spelling +        sip["parameters"] = parameters +        sip["prefix"], sip["suffix"] = self._fn_get_decorators(function) +        self.rules.function_rules().apply(container, function, sip) +        pad = " " * (level * 4) +        if sip["name"]: +            sip["template_parameters"] = ", ".join(sip["template_parameters"]) +            decl = sip["name"] + "(" + ", ".join(sip["parameters"]) + ")" +            if sip["fn_result"]: +                decl = sip["fn_result"] + " " + decl +            decl = pad + sip["prefix"] + decl + sip["suffix"] +            if sip["template_parameters"]: +                decl = pad + "template <" + sip["template_parameters"] + ">\n" + decl +            decl += ";\n" +        else: +            decl = pad + "// Discarded {}\n".format(SipGenerator.describe(function)) +        return decl + +    def _fn_get_decorators(self, function): +        """ +        The parser does not provide direct access to the complete keywords (explicit, const, static, etc) of a function +        in the displayname. It would be nice to get these from the AST, but I cannot find where they are hiding. + +        Now, we could resort to using the original source. That does not bode well if you have macros (QOBJECT, +        xxxDEPRECATED?), inlined bodies and the like, using the rule engine could be used to patch corner cases... + +        ...or we can try to guess what SIP cares about, i.e static and maybe const. Luckily (?), we have those to hand! + +        :param function:                    The function object. +        :return: prefix, suffix             String containing any prefix or suffix keywords. +        """ +        suffix = "" +        if function.is_const_method(): +            suffix += " const" +        prefix = "" +        if function.is_static_method(): +            prefix += "static " +        if function.is_virtual_method(): +            prefix += "virtual " +            if function.is_pure_virtual_method(): +                suffix += " = 0" +        return prefix, suffix + +    def _fn_get_parameter_default(self, function, parameter): +        """ +        The parser does not seem to provide access to the complete text of a parameter. +        This makes it hard to find any default values, so we: + +            1. Run the lexer from "here" to the end of the file, bailing out when we see the "," +            or a ")" marking the end. +            2. Watch for the assignment. +        """ +        def _get_param_type(parameter): +            result = parameter.type.get_declaration().type + +            if (parameter.type.get_declaration().type.kind == TypeKind.TYPEDEF): +                isQFlags = False +                for member in parameter.type.get_declaration().get_children(): +                    if member.kind == CursorKind.TEMPLATE_REF and member.spelling == "QFlags": +                        isQFlags = True +                    if isQFlags and member.kind == CursorKind.TYPE_REF: +                        result = member.type +                        break + +            return result + +        def _get_param_value(text, parameterType): +            if text == "0": +                return text +            if not "::" in parameterType.spelling: +                return text +            try: +                typeText, typeInit = text.split("(") +                typeInit = "(" + typeInit +            except: +                typeText = text +                typeInit = "" + +            prefix = parameterType.spelling.rsplit("::", 1)[0] +            if "::" in typeText: +                typeText = typeText.rsplit("::", 1)[1] +            return prefix + "::" + typeText + typeInit + + +        for member in parameter.get_children(): +            if member.kind.is_expression(): + +                possible_extent = SourceRange.from_locations(parameter.extent.start, function.extent.end) +                text = "" +                bracket_level = 0 +                found_start = False +                found_end = False +                for token in self.tu.get_tokens(extent=possible_extent): +                    if (token.spelling == "="): +                        found_start = True +                        continue +                    if token.spelling == "," and bracket_level == 0: +                        found_end = True +                        break +                    elif token.spelling == "(": +                        bracket_level += 1 +                        text += token.spelling +                    elif token.spelling == ")": +                        if bracket_level == 0: +                            found_end = True +                            break +                        bracket_level -= 1 +                        text += token.spelling +                        if bracket_level == 0: +                            found_end = True +                            break +                    elif found_start: +                        text += token.spelling +                if not found_end and text: +                    RuntimeError(_("No end found for {}::{}, '{}'").format(function.spelling, parameter.spelling, text)) + +                parameterType = _get_param_type(parameter) + +                return _get_param_value(text, parameterType) +        return "" + +    def _typedef_get(self, typedef, level): +        """ +        Generate the translation for a typedef. + +        :param container:           A class or namespace. +        :param typedef:             The typedef object. +        :param level:               Recursion level controls indentation. +        :return:                    A string. +        """ + +        pad = " " * (level * 4) + +        decl = pad + "typedef {} {}".format(typedef.underlying_typedef_type.spelling, typedef.displayname) +        decl += ";\n" +        return decl + +    def _var_get(self, container, variable, level): +        """ +        Generate the translation for a variable. + +        :param container:           A class or namespace. +        :param variable:            The variable object. +        :param level:               Recursion level controls indentation. +        :return:                    A string. +        """ + +        def skippable_attribute(member, text): +            """ +            We don't seem to have access to the __attribute__(())s, but at least we can look for stuff we care about. + +            :param member:          The attribute. +            :param text:            The raw source corresponding to the region of member. +            """ +            if SipGenerator.VAR_SKIPPABLE_ATTR.search(text): +                return True +            SipGenerator._report_ignoring(container, member, text) + +        sip = { +            "name": variable.spelling +        } +        for child in variable.get_children(): +            if child.kind in TEMPLATE_KINDS + [CursorKind.STRUCT_DECL, CursorKind.UNION_DECL]: +                # +                # Ignore: +                # +                #   TEMPLATE_KINDS, CursorKind.STRUCT_DECL, CursorKind.UNION_DECL: : The variable type. +                # +                pass +            else: +                text = self._read_source(child.extent) +                if child.kind == CursorKind.VISIBILITY_ATTR and skippable_attribute(child, text): +                    pass +                else: +                    SipGenerator._report_ignoring(variable, child) +        # +        # Flesh out the SIP context for the rules engine. +        # +        decl = "{} {}".format(variable.type.spelling, variable.spelling) +        sip["decl"] = decl +        self.rules.variable_rules().apply(container, variable, sip) + +        pad = " " * (level * 4) +        if sip["name"]: +            decl = sip["decl"] +            # +            # SIP does not support protected variables, so we ignore them. +            # +            if variable.access_specifier == AccessSpecifier.PROTECTED: +                decl = pad + "// Discarded {}\n".format(SipGenerator.describe(variable)) +            else: +                decl = pad + decl + ";\n" +        else: +            decl = pad + "// Discarded {}\n".format(SipGenerator.describe(variable)) +        return decl + +    def _read_source(self, extent): +        """ +        Read the given range from the unpre-processed source. + +        :param extent:              The range of text required. +        """ +        extract = self.unpreprocessed_source[extent.start.line - 1:extent.end.line] +        if extent.start.line == extent.end.line: +            extract[0] = extract[0][extent.start.column - 1:extent.end.column - 1] +        else: +            extract[0] = extract[0][extent.start.column - 1:] +            extract[-1] = extract[-1][:extent.end.column - 1] +        # +        # Return a single line of text. +        # +        return "".join(extract).replace("\n", " ") + +    @staticmethod +    def _report_ignoring(parent, child, text=None): +        if not text: +            text = child.displayname or child.spelling +        logger.debug(_("Ignoring {} {} child {}").format(parent.kind.name, parent.spelling, SipGenerator.describe(child, text))) + + +def main(argv=None): +    """ +    Take a single C++ header file and generate the corresponding SIP file. +    Beyond simple generation of the SIP file from the corresponding C++ +    header file, a set of rules can be used to customise the generated +    SIP file. + +    Examples: + +        sip_generator.py /usr/include/KF5/KItemModels/kselectionproxymodel.h +    """ +    if argv is None: +        argv = sys.argv +    parser = argparse.ArgumentParser(epilog=inspect.getdoc(main), +                                     formatter_class=HelpFormatter) +    parser.add_argument("-v", "--verbose", action="store_true", default=False, help=_("Enable verbose output")) +    parser.add_argument("--flags", +                        help=_("Semicolon-separated C++ compile flags to use")) +    parser.add_argument("--include_filename", help=_("C++ header include to compile")) +    parser.add_argument("libclang", help=_("libclang library to use for parsing")) +    parser.add_argument("project_rules", help=_("Project rules")) +    parser.add_argument("source", help=_("C++ header to process")) +    parser.add_argument("output", help=_("output filename to write")) +    try: +        args = parser.parse_args(argv[1:]) +        if args.verbose: +            logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)s %(levelname)s: %(message)s') +        else: +            logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') +        # +        # Generate! +        # + +        cindex.Config.set_library_file(args.libclang) + +        rules = rules_engine.rules(args.project_rules) +        g = SipGenerator(rules, args.flags.lstrip().split(";"), args.verbose) +        body, includes = g.create_sip(args.source, args.include_filename) +        with open(args.output, "w") as outputFile: +            outputFile.write(body) +    except Exception as e: +        tbk = traceback.format_exc() +        print(tbk) +        return -1 + + +if __name__ == "__main__": +    if sys.argv[1] != "--self-check": +        sys.exit(main()) +    else: +        cindex.Config.set_library_file(sys.argv[2]) | 
