############################################################################# ## ## Copyright (C) 2020 The Qt Company Ltd. ## Contact: https://www.qt.io/licensing/ ## ## This file is part of the Qt for Python project. ## ## $QT_BEGIN_LICENSE:LGPL$ ## Commercial License Usage ## Licensees holding valid commercial Qt licenses may use this file in ## accordance with the commercial license agreement provided with the ## Software or, alternatively, in accordance with the terms contained in ## a written agreement between you and The Qt Company. For licensing terms ## and conditions see https://www.qt.io/terms-conditions. For further ## information use the contact form at https://www.qt.io/contact-us. ## ## GNU Lesser General Public License Usage ## Alternatively, this file may be used under the terms of the GNU Lesser ## General Public License version 3 as published by the Free Software ## Foundation and appearing in the file LICENSE.LGPL3 included in the ## packaging of this file. Please review the following information to ## ensure the GNU Lesser General Public License version 3 requirements ## will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ## ## GNU General Public License Usage ## Alternatively, this file may be used under the terms of the GNU ## General Public License version 2.0 or (at your option) the GNU General ## Public license version 3 or any later version approved by the KDE Free ## Qt Foundation. The licenses are as published by the Free Software ## Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ## included in the packaging of this file. Please review the following ## information to ensure the GNU General Public License requirements will ## be met: https://www.gnu.org/licenses/gpl-2.0.html and ## https://www.gnu.org/licenses/gpl-3.0.html. ## ## $QT_END_LICENSE$ ## ############################################################################# """AST visitor printing out C++""" import ast import sys import tokenize import warnings from .formatter import (CppFormatter, format_for_loop, format_literal, format_reference, format_start_function_call, write_import, write_import_from) from .nodedump import debug_format_node _QT_STACK_CLASSES = ["QApplication", "QColorDialog", "QCoreApplication", "QFile", "QFileDialog", "QFileInfo", "QFontDialog", "QGuiApplication", "QIcon", "QLine", "QLineF", "QMessageBox", "QPainter", "QPixmap", "QPoint", "QPointF", "QQmlApplicationEngine", "QQmlComponent", "QQmlEngine", "QQuickView", "QRect", "QRectF", "QSaveFile", "QSettings", "QSize", "QSizeF", "QTextStream"] def _is_qt_constructor(assign_node): """Is this assignment node a plain construction of a Qt class? 'f = QFile(name)'. Returns the class_name.""" call = assign_node.value if (isinstance(call, ast.Call) and isinstance(call.func, ast.Name)): func = call.func.id if func.startswith("Q"): return func return None def _is_if_main(if_node): """Return whether an if statement is: if __name__ == '__main__' """ test = if_node.test return (isinstance(test, ast.Compare) and len(test.ops) == 1 and isinstance(test.ops[0], ast.Eq) and isinstance(test.left, ast.Name) and test.left.id == "__name__" and len(test.comparators) == 1 and isinstance(test.comparators[0], ast.Constant) and test.comparators[0].value == "__main__") class ConvertVisitor(ast.NodeVisitor, CppFormatter): """AST visitor printing out C++ Note on implementation: - Any visit_XXX() overridden function should call self.generic_visit(node) to continue visiting - When controlling the visiting manually (cf visit_Call()), self.visit(child) needs to be called since that dispatches to visit_XXX(). This is usually done to prevent undesired output for example from references of calls, etc. """ debug = False def __init__(self, file_name, output_file): ast.NodeVisitor.__init__(self) CppFormatter.__init__(self, output_file) self._file_name = file_name self._class_scope = [] # List of class names self._stack = [] # nodes self._stack_variables = [] # variables instantiated on stack self._debug_indent = 0 @staticmethod def create_ast(filename): """Create an Abstract Syntax Tree on which a visitor can be run""" node = None with tokenize.open(filename) as file: node = ast.parse(file.read(), mode="exec") return node def generic_visit(self, node): parent = self._stack[-1] if self._stack else None if self.debug: self._debug_enter(node, parent) self._stack.append(node) try: super().generic_visit(node) except Exception as e: line_no = node.lineno if hasattr(node, 'lineno') else -1 error_message = str(e) message = f'{self._file_name}:{line_no}: Error "{error_message}"' warnings.warn(message) self._output_file.write(f'\n// {error_message}\n') del self._stack[-1] if self.debug: self._debug_leave(node) def visit_Add(self, node): self.generic_visit(node) self._output_file.write(' + ') def visit_Assign(self, node): self.INDENT() qt_class = _is_qt_constructor(node) on_stack = qt_class and qt_class in _QT_STACK_CLASSES # Is this a free variable and not a member assignment? Instantiate # on stack or give a type if len(node.targets) == 1 and isinstance(node.targets[0], ast.Name): if qt_class: if on_stack: # "QFile f(args)" var = node.targets[0].id self._stack_variables.append(var) self._output_file.write(f"{qt_class} {var}(") self._write_function_args(node.value.args) self._output_file.write(");\n") return self._output_file.write("auto *") line_no = node.lineno if hasattr(node, 'lineno') else -1 for target in node.targets: if isinstance(target, ast.Tuple): w = f"{self._file_name}:{line_no}: List assignment not handled." warnings.warn(w) elif isinstance(target, ast.Subscript): w = f"{self._file_name}:{line_no}: Subscript assignment not handled." warnings.warn(w) else: self._output_file.write(format_reference(target)) self._output_file.write(' = ') if qt_class and not on_stack: self._output_file.write("new ") self.visit(node.value) self._output_file.write(';\n') def visit_Attribute(self, node): """Format a variable reference (cf visit_Name)""" self._output_file.write(format_reference(node)) def visit_BinOp(self, node): # Parentheses are not exposed, so, every binary operation needs to # be enclosed by (). self._output_file.write('(') self.generic_visit(node) self._output_file.write(')') def visit_BitAnd(self, node): self.generic_visit(node) self._output_file.write(" & ") def visit_BitOr(self, node): self.generic_visit(node) self._output_file.write(" | ") def _format_call(self, node): f = node.func if isinstance(f, ast.Name): self._output_file.write(f.id) else: # Attributes denoting chained calls "a->b()->c()". Walk along in # reverse order, recursing for other calls. names = [] n = f while isinstance(n, ast.Attribute): names.insert(0, n.attr) n = n.value if isinstance(n, ast.Name): # Member or variable reference if n.id != "self": sep = "->" if n.id in self._stack_variables: sep = "." elif n.id[0:1].isupper(): # Heuristics for static sep = "::" self._output_file.write(n.id) self._output_file.write(sep) elif isinstance(n, ast.Call): # A preceding call self._format_call(n) self._output_file.write("->") self._output_file.write("->".join(names)) self._output_file.write('(') self._write_function_args(node.args) self._output_file.write(')') def visit_Call(self, node): self._format_call(node) # Context manager expression? if self._stack and isinstance(self._stack[-1], ast.withitem): self._output_file.write(";\n") def _write_function_args(self, args_node): # Manually do visit(), skip the children of func for i, arg in enumerate(args_node): if i > 0: self._output_file.write(', ') self.visit(arg) def visit_ClassDef(self, node): # Manually do visit() to skip over base classes # and annotations self._class_scope.append(node.name) self.write_class_def(node) self.indent() for b in node.body: self.visit(b) self.dedent() self.indent_line('};') del self._class_scope[-1] def visit_Eq(self, node): self.generic_visit(node) self._output_file.write(" == ") def visit_Expr(self, node): self.INDENT() self.generic_visit(node) self._output_file.write(';\n') def visit_Gt(self, node): self.generic_visit(node) self._output_file.write(" > ") def visit_GtE(self, node): self.generic_visit(node) self._output_file.write(" >= ") def visit_For(self, node): # Manually do visit() to get the indentation right. # TODO: what about orelse? self.indent_line(format_for_loop(node)) self.indent() for b in node.body: self.visit(b) self.dedent() self.indent_line('}') def visit_FunctionDef(self, node): class_context = self._class_scope[-1] if self._class_scope else None self.write_function_def(node, class_context) self.indent() self.generic_visit(node) self.dedent() self.indent_line('}') self._stack_variables.clear() def visit_If(self, node): # Manually do visit() to get the indentation right. Note: # elsif() is modelled as nested if. # Check for the main function if _is_if_main(node): self._output_file.write("\nint main(int argc, char *argv[])\n{\n") self.indent() for b in node.body: self.visit(b) self.indent_string("return 0;\n") self.dedent() self._output_file.write("}\n") return self.indent_string('if (') self.visit(node.test) self._output_file.write(') {\n') self.indent() for b in node.body: self.visit(b) self.dedent() self.indent_string('}') if node.orelse: self._output_file.write(' else {\n') self.indent() for b in node.orelse: self.visit(b) self.dedent() self.indent_string('}') self._output_file.write('\n') def visit_Import(self, node): write_import(self._output_file, node) def visit_ImportFrom(self, node): write_import_from(self._output_file, node) def visit_List(self, node): # Manually do visit() to get separators right self._output_file.write('{') for i, el in enumerate(node.elts): if i > 0: self._output_file.write(', ') self.visit(el) self._output_file.write('}') def visit_LShift(self, node): self.generic_visit(node) self._output_file.write(" << ") def visit_Lt(self, node): self.generic_visit(node) self._output_file.write(" < ") def visit_LtE(self, node): self.generic_visit(node) self._output_file.write(" <= ") def visit_Mult(self, node): self.generic_visit(node) self._output_file.write(' * ') def visit_Name(self, node): """Format a variable reference (cf visit_Attribute)""" # Context manager variable? if self._stack and isinstance(self._stack[-1], ast.withitem): return self._output_file.write(format_reference(node)) def visit_NameConstant(self, node): self.generic_visit(node) if node.value is None: self._output_file.write('nullptr') elif not node.value: self._output_file.write('false') else: self._output_file.write('true') def visit_Not(self, node): self.generic_visit(node) self._output_file.write("!") def visit_NotEq(self, node): self.generic_visit(node) self._output_file.write(" != ") def visit_Num(self, node): self.generic_visit(node) self._output_file.write(format_literal(node)) def visit_RShift(self, node): self.generic_visit(node) self._output_file.write(" >> ") def visit_Return(self, node): self.indent_string("return") if node.value: self._output_file.write(" ") self.generic_visit(node) self._output_file.write(";\n") def visit_Str(self, node): self.generic_visit(node) self._output_file.write(format_literal(node)) def visit_UnOp(self, node): self.generic_visit(node) def visit_With(self, node): self.indent() self.INDENT() self._output_file.write("{ // Converted from context manager\n") for item in node.items: self.INDENT() if item.optional_vars: self._output_file.write(format_reference(item.optional_vars)) self._output_file.write(" = ") self.generic_visit(node) self.INDENT() self._output_file.write("}\n") self.dedent() def _debug_enter(self, node, parent=None): message = '{}>generic_visit({})'.format(' ' * self ._debug_indent, debug_format_node(node)) if parent: message += ', parent={}'.format(debug_format_node(parent)) message += '\n' sys.stderr.write(message) self._debug_indent += 1 def _debug_leave(self, node): self._debug_indent -= 1 message = '{}