llvm/clang/docs/tools/generate_formatted_state.py

#!/usr/bin/env python
# A tool to parse creates a document outlining how clang formatted the
# LLVM project is.

import sys
import os
import subprocess
from datetime import datetime


def get_git_revision_short_hash():
    """Get the get SHA in short hash form."""
    return (
        subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
        .decode(sys.stdout.encoding)
        .strip()
    )


def get_style(count, passed):
    """Determine if this directory is good based on  the number of clean
    files vs the number of files in total."""
    if passed == count:
        return ":good:"
    if passed != 0:
        return ":part:"
    return ":none:"


TOP_DIR = os.path.join(os.path.dirname(__file__), "../../..")
CLANG_DIR = os.path.join(os.path.dirname(__file__), "../..")
DOC_FILE = os.path.join(CLANG_DIR, "docs/ClangFormattedStatus.rst")
CLEAN_FILE = os.path.join(CLANG_DIR, "docs/tools/clang-formatted-files.txt")

rootdir = TOP_DIR

skipped_dirs = [".git", "test"]
suffixes = (".cpp", ".h")

RST_PREFIX = """\
.. raw:: html

      <style type="text/css">
        .total {{ font-weight: bold; }}
        .none {{ background-color: #FFFF99; height: 20px; display: inline-block; width: 120px; text-align: center; border-radius: 5px; color: #000000; font-family="Verdana,Geneva,DejaVu Sans,sans-serif" }}
        .part {{ background-color: #FFCC99; height: 20px; display: inline-block; width: 120px; text-align: center; border-radius: 5px; color: #000000; font-family="Verdana,Geneva,DejaVu Sans,sans-serif" }}
        .good {{ background-color: #2CCCFF; height: 20px; display: inline-block; width: 120px; text-align: center; border-radius: 5px; color: #000000; font-family="Verdana,Geneva,DejaVu Sans,sans-serif" }}
      </style>

.. role:: none
.. role:: part
.. role:: good
.. role:: total

======================
Clang Formatted Status
======================

:doc:`ClangFormattedStatus` describes the state of LLVM source
tree in terms of conformance to :doc:`ClangFormat` as of: {today} (`{sha} <https://github.com/llvm/llvm-project/commit/{sha}>`_).


.. list-table:: LLVM Clang-Format Status
   :widths: 50 25 25 25 25
   :header-rows: 1\n
   * - Directory
     - Total Files
     - Formatted Files
     - Unformatted Files
     - % Complete
"""

TABLE_ROW = """\
   * - {path}
     - {style}`{count}`
     - {style}`{passes}`
     - {style}`{fails}`
     - {style2}`{percent}%`
"""


with open(DOC_FILE, "wb") as output:
    cleanfiles = open(CLEAN_FILE, "wb")
    sha = get_git_revision_short_hash()
    today = datetime.now().strftime("%B %d, %Y %H:%M:%S")
    output.write(bytes(RST_PREFIX.format(today=today, sha=sha).encode("utf-8")))

    total_files_count = 0
    total_files_pass = 0
    total_files_fail = 0
    for root, subdirs, files in os.walk(rootdir):
        for subdir in subdirs:
            if any(sd == subdir for sd in skipped_dirs):
                subdirs.remove(subdir)
            else:
                act_sub_dir = os.path.join(root, subdir)
                # Check the git index to see if the directory contains tracked
                # files. Reditect the output to a null descriptor as we aren't
                # interested in it, just the return code.
                git_check = subprocess.Popen(
                    ["git", "ls-files", "--error-unmatch", act_sub_dir],
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
                if git_check.wait() != 0:
                    print("Skipping directory: ", act_sub_dir)
                    subdirs.remove(subdir)

        path = os.path.relpath(root, TOP_DIR)
        path = path.replace("\\", "/")

        file_count = 0
        file_pass = 0
        file_fail = 0
        for filename in files:
            file_path = os.path.join(root, filename)
            ext = os.path.splitext(file_path)[-1].lower()
            if not ext.endswith(suffixes):
                continue

            file_count += 1

            args = ["clang-format", "-n", file_path]
            cmd = subprocess.Popen(args, stderr=subprocess.PIPE)
            stdout, err = cmd.communicate()

            relpath = os.path.relpath(file_path, TOP_DIR)
            relpath = relpath.replace("\\", "/")
            if err.decode(sys.stdout.encoding).find(": warning:") > 0:
                print(relpath, ":", "FAIL")
                file_fail += 1
            else:
                print(relpath, ":", "PASS")
                file_pass += 1
                cleanfiles.write(bytes(relpath + "\n"))
                cleanfiles.flush()

        total_files_count += file_count
        total_files_pass += file_pass
        total_files_fail += file_fail

        if file_count > 0:
            percent = int(100.0 * (float(file_pass) / float(file_count)))
            style = get_style(file_count, file_pass)
            output.write(
                bytes(
                    TABLE_ROW.format(
                        path=path,
                        count=file_count,
                        passes=file_pass,
                        fails=file_fail,
                        percent=str(percent),
                        style="",
                        style2=style,
                    ).encode("utf-8")
                )
            )
            output.flush()

            print("----\n")
            print(path, file_count, file_pass, file_fail, percent)
            print("----\n")

    total_percent = float(total_files_pass) / float(total_files_count)
    percent_str = str(int(100.0 * total_percent))
    output.write(
        bytes(
            TABLE_ROW.format(
                path="Total",
                count=total_files_count,
                passes=total_files_pass,
                fails=total_files_fail,
                percent=percent_str,
                style=":total:",
                style2=":total:",
            ).encode("utf-8")
        )
    )