From ae1cde4dc1f5b50c7872cafa4642b5d266d1d0b6 Mon Sep 17 00:00:00 2001
From: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
Date: Wed, 5 Apr 2023 13:34:31 -0400
Subject: [PATCH] misc: format QML files using the python format script

+ Requires that the Qt path is supplied when calling build.py --init.
+ format.py now also takes a --qt argument to specify the Qt path, used to locate qmlformat.
+ format.py takes a --type argument to specify the type of files to format (qml, cpp, or both).

qmlformat is called with `--normalize` and `--force`.

Gitlab: #1059
Change-Id: Id9ff4b17018370696792b44f55ed2f4bc8091193
---
 INSTALL.md               |  2 +-
 README.md                |  2 +-
 build.py                 | 32 ++++++++++---
 extras/scripts/format.py | 98 +++++++++++++++++++++++++++++++---------
 4 files changed, 104 insertions(+), 30 deletions(-)

diff --git a/INSTALL.md b/INSTALL.md
index 31cb6098b..4588d9ece 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -63,7 +63,7 @@ for getting the latest development versions; otherwise, you can use
 submodule).
 
 ```bash
-./build.py --init
+./build.py --init [--qt=<path/to/qt> (this is required for qmlformatting to work)]
 ```
 
 Then you will need to install dependencies:
diff --git a/README.md b/README.md
index 6ba07cb3d..ca660c53c 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ For more information about the jami project, see the following:
 
 ## Notes
 
-- Coding style is managed by the clang-format, if you want to contribute, please use the pre-commit hook automatically installed with `./build.py --init`
+- Coding style is managed by the clang-format and qmlformat, if you want to contribute, please use the pre-commit hook automatically installed with `./build.py --init --qt=<path/to/qt>`
 - We use gerrit for our review. Please read about [working with Gerrit](https://docs.jami.net/developer/working-with-gerrit.html) if you want to submit patches.
 
 ## Build
diff --git a/build.py b/build.py
index dfda20921..43dfcb0f4 100755
--- a/build.py
+++ b/build.py
@@ -304,18 +304,37 @@ def run_dependencies(args):
         sys.exit(1)
 
 
-def run_init():
+def run_init(args):
+    """Initialize the git submodules and install the commit-msg hook."""
     subprocess.run(["git", "submodule", "update", "--init"],
                    check=True)
 
-    hooks_directories = ['.git/hooks/', f'.git/modules/daemon/hooks']
-    for hooks_dir in hooks_directories:
+    client_hooks_dir = '.git/hooks'
+    daemon_hooks_dir = '.git/modules/daemon/hooks'
+
+    print("Installing commit-msg hooks...")
+    # Copy the commit-msg hook to all modules in the same way.
+    for hooks_dir in [client_hooks_dir, daemon_hooks_dir]:
         if not os.path.exists(hooks_dir):
             os.makedirs(hooks_dir)
         copy_file("./extras/scripts/commit-msg",
                   os.path.join(hooks_dir, "commit-msg"))
-        execute_script(['./extras/scripts/format.py --install %(path)s'],
-                       {"path": hooks_dir})
+
+    print("Installing pre-commit hooks...")
+    format_script = "./extras/scripts/format.py"
+    # Prepend with the python executable if on Windows (not WSL).
+    if sys.platform == 'win32':
+        format_script = f'python {format_script}'
+    # The client submodule has QML files, so we need to run qmlformat on it,
+    # and thus need to supply the Qt path.
+    execute_script([f'{format_script} --install {client_hooks_dir}'
+                    f' --qt {args.qt}' if args.qt else ''],
+                   {"path": client_hooks_dir})
+
+    # The daemon submodule has no QML files, so we don't need to run
+    # qmlformat on it, and thus don't need to supply the Qt path.
+    execute_script([f'{format_script} --install {daemon_hooks_dir}'],
+                   {"path": daemon_hooks_dir})
 
 
 def copy_file(src, dest):
@@ -651,7 +670,8 @@ def main():
         run_dependencies(parsed_args)
 
     elif parsed_args.init:
-        run_init()
+        run_init(parsed_args)
+
     elif parsed_args.clean:
         run_clean()
 
diff --git a/extras/scripts/format.py b/extras/scripts/format.py
index 4676c592a..59cf6bffd 100755
--- a/extras/scripts/format.py
+++ b/extras/scripts/format.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 """
-Clang format C/C++ source files with clang-format.
+Clang format C/C++ source files with clang-format (C/C++), and
+qmlformat (QML) if installed.
 Also optionally installs a pre-commit hook to run this script.
 
 Usage:
@@ -18,16 +19,34 @@ import sys
 import subprocess
 import argparse
 import shutil
+from platform import uname
 
 CFVERSION = "9"
 CLANGFORMAT = ""
 
+QMLFORMAT = None
+
 
 def command_exists(cmd):
     """ Check if a command exists """
     return shutil.which(cmd) is not None
 
 
+def find_qmlformat(qt_path):
+    """Find the path to the qmlformat binary."""
+
+    # Correct the path if it's a Windows WSL path.
+    is_windows = os.name == "nt"
+    if 'Microsoft' in uname().release:
+        qt_path = qt_path.replace('C:', '/mnt/c')
+        is_windows = True
+
+    # Check if qmlformat is in a subdirectory called bin.
+    qmlformat_path = os.path.join(qt_path, "bin", "qmlformat")
+    qmlformat_path += ".exe" if is_windows else ""
+    return qmlformat_path if os.path.exists(qmlformat_path) else None
+
+
 def clang_format_file(filename):
     """ Format a file using clang-format """
     if os.path.isfile(filename):
@@ -36,18 +55,35 @@ def clang_format_file(filename):
 
 def clang_format_files(files):
     """ Format a list of files """
+    if not files:
+        return
     for filename in files:
         print(f"Formatting: {filename}", end='\r')
         clang_format_file(filename)
 
 
+def qml_format_files(files):
+    """ Format a file using qmlformat """
+    if QMLFORMAT is None or not files:
+        return
+    for filename in files:
+        if os.path.isfile(filename):
+            print(f"Formatting: {filename}", end='\r')
+            extra_args = ['--normalize', '--force']
+            subprocess.call([QMLFORMAT, '--inplace', filename] + extra_args)
+            # This may generate a backup file (ending with ~), so delete it.
+            backup_file = filename + "~"
+            if os.path.isfile(backup_file):
+                os.remove(backup_file)
+
+
 def exit_if_no_files():
     """ Exit if no files to format """
     print("No files to format")
     sys.exit(0)
 
 
-def install_hook(hooks_path):
+def install_hook(hooks_path, qt_path=None):
     """ Install a pre-commit hook to run this script """
     if not os.path.isdir(hooks_path):
         print(f"{hooks_path} path does not exist")
@@ -55,7 +91,8 @@ def install_hook(hooks_path):
     print(f"Installing pre-commit hook in {hooks_path}")
     with open(os.path.join(hooks_path, "pre-commit"),
               "w", encoding="utf-8") as file:
-        file.write(os.path.realpath(sys.argv[0]))
+        file.write(os.path.realpath(sys.argv[0])
+                   + f' --qt={qt_path}' if qt_path else '')
     os.chmod(os.path.join(hooks_path, "pre-commit"), 0o755)
 
 
@@ -86,7 +123,7 @@ def get_files(file_types, recursive=True, committed_only=False):
 
 
 def main():
-    """Check if clang-format is installed, and format files."""
+    """Check for formatter installations, install hooks, and format files."""
     global CLANGFORMAT  # pylint: disable=global-statement
     parser = argparse.ArgumentParser(
         description="Format source filess with a clang-format")
@@ -94,33 +131,50 @@ def main():
                         help="format all files instead of only committed ones")
     parser.add_argument("-i", "--install", metavar="PATH",
                         help="install a pre-commit hook to run this script")
+    parser.add_argument("-q", "--qt", default=None,
+                        help="The Qt root path")
+    # Add an option to only format a specific type (qml, cpp, or both)
+    parser.add_argument("-t", "--type", default="both",
+                        help="The type of files to format (qml, cpp, or both)")
     args = parser.parse_args()
 
-    if not command_exists("clang-format-" + CFVERSION):
-        if not command_exists("clang-format"):
-            print("Required version of clang-format not found")
-            sys.exit(1)
+    if args.type in ["cpp", "both"]:
+        if not command_exists("clang-format-" + CFVERSION):
+            if not command_exists("clang-format"):
+                print("Required version of clang-format not found")
+                sys.exit(1)
+            else:
+                CLANGFORMAT = "clang-format"
         else:
-            CLANGFORMAT = "clang-format"
+            CLANGFORMAT = "clang-format-" + CFVERSION
+        print("Using source formatter: " + CLANGFORMAT)
+
+    if args.qt is not None and args.type in ["qml", "both"]:
+        global QMLFORMAT  # pylint: disable=global-statement
+        QMLFORMAT = find_qmlformat(args.qt)
+
+    if QMLFORMAT is not None:
+        print("Using qmlformatter: " + QMLFORMAT)
     else:
-        CLANGFORMAT = "clang-format-" + CFVERSION
+        print("No qmlformat found, can't format QML files")
 
     if args.install:
-        install_hook(args.install)
+        install_hook(args.install, args.qt)
         sys.exit(0)
 
-    if args.all:
-        print("Formatting all files...")
-        # Find all files in the recursively in the current directory.
-        clang_format_files(get_files((".cpp", ".cxx", ".cc", ".h", ".hpp"),
-                                     committed_only=False))
+    src_files = get_files([".cpp", ".cxx", ".cc", ".h", ".hpp"],
+                          committed_only=not args.all)
+    qml_files = get_files([".qml"], committed_only=not args.all)
+
+    if not src_files and not qml_files:
+        exit_if_no_files()
     else:
-        files = get_files((".cpp", ".cxx", ".cc", ".h", ".hpp"),
-                          committed_only=True)
-        if not files:
-            exit_if_no_files()
-        print("Formatting committed source files...")
-        clang_format_files(files)
+        if src_files and args.type in ["cpp", "both"] and CLANGFORMAT:
+            print("Formatting source files...")
+            clang_format_files(src_files)
+        if qml_files and args.type in ["qml", "both"] and QMLFORMAT:
+            print("Formatting QML files...")
+            qml_format_files(qml_files)
 
 
 if __name__ == "__main__":
-- 
GitLab