aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rwxr-xr-xfind-modules/rules_engine.py526
-rw-r--r--find-modules/sip_generator.py700
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])