xref: /llvm-project/clang/docs/tools/generate_formatted_state.py (revision dd3c26a045c081620375a878159f536758baba6e)
1#!/usr/bin/env python
2# A tool to parse creates a document outlining how clang formatted the
3# LLVM project is.
4
5import sys
6import os
7import subprocess
8from datetime import datetime
9
10
11def get_git_revision_short_hash():
12    """Get the get SHA in short hash form."""
13    return (
14        subprocess.check_output(["git", "rev-parse", "--short", "HEAD"])
15        .decode(sys.stdout.encoding)
16        .strip()
17    )
18
19
20def get_style(count, passed):
21    """Determine if this directory is good based on  the number of clean
22    files vs the number of files in total."""
23    if passed == count:
24        return ":good:"
25    if passed != 0:
26        return ":part:"
27    return ":none:"
28
29
30TOP_DIR = os.path.join(os.path.dirname(__file__), "../../..")
31CLANG_DIR = os.path.join(os.path.dirname(__file__), "../..")
32DOC_FILE = os.path.join(CLANG_DIR, "docs/ClangFormattedStatus.rst")
33CLEAN_FILE = os.path.join(CLANG_DIR, "docs/tools/clang-formatted-files.txt")
34
35rootdir = TOP_DIR
36
37skipped_dirs = [".git", "test"]
38suffixes = (".cpp", ".h")
39
40RST_PREFIX = """\
41.. raw:: html
42
43      <style type="text/css">
44        .total {{ font-weight: bold; }}
45        .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" }}
46        .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" }}
47        .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" }}
48      </style>
49
50.. role:: none
51.. role:: part
52.. role:: good
53.. role:: total
54
55======================
56Clang Formatted Status
57======================
58
59:doc:`ClangFormattedStatus` describes the state of LLVM source
60tree in terms of conformance to :doc:`ClangFormat` as of: {today} (`{sha} <https://github.com/llvm/llvm-project/commit/{sha}>`_).
61
62
63.. list-table:: LLVM Clang-Format Status
64   :widths: 50 25 25 25 25
65   :header-rows: 1\n
66   * - Directory
67     - Total Files
68     - Formatted Files
69     - Unformatted Files
70     - % Complete
71"""
72
73TABLE_ROW = """\
74   * - {path}
75     - {style}`{count}`
76     - {style}`{passes}`
77     - {style}`{fails}`
78     - {style2}`{percent}%`
79"""
80
81FNULL = open(os.devnull, "w")
82
83
84with open(DOC_FILE, "wb") as output:
85    cleanfiles = open(CLEAN_FILE, "wb")
86    sha = get_git_revision_short_hash()
87    today = datetime.now().strftime("%B %d, %Y %H:%M:%S")
88    output.write(bytes(RST_PREFIX.format(today=today, sha=sha).encode("utf-8")))
89
90    total_files_count = 0
91    total_files_pass = 0
92    total_files_fail = 0
93    for root, subdirs, files in os.walk(rootdir):
94        for subdir in subdirs:
95            if any(sd == subdir for sd in skipped_dirs):
96                subdirs.remove(subdir)
97            else:
98                act_sub_dir = os.path.join(root, subdir)
99                # Check the git index to see if the directory contains tracked
100                # files. Reditect the output to a null descriptor as we aren't
101                # interested in it, just the return code.
102                git_check = subprocess.Popen(
103                    ["git", "ls-files", "--error-unmatch", act_sub_dir],
104                    stdout=FNULL,
105                    stderr=FNULL,
106                )
107                if git_check.wait() != 0:
108                    print("Skipping directory: ", act_sub_dir)
109                    subdirs.remove(subdir)
110
111        path = os.path.relpath(root, TOP_DIR)
112        path = path.replace("\\", "/")
113
114        file_count = 0
115        file_pass = 0
116        file_fail = 0
117        for filename in files:
118            file_path = os.path.join(root, filename)
119            ext = os.path.splitext(file_path)[-1].lower()
120            if not ext.endswith(suffixes):
121                continue
122
123            file_count += 1
124
125            args = ["clang-format", "-n", file_path]
126            cmd = subprocess.Popen(args, stderr=subprocess.PIPE)
127            stdout, err = cmd.communicate()
128
129            relpath = os.path.relpath(file_path, TOP_DIR)
130            relpath = relpath.replace("\\", "/")
131            if err.decode(sys.stdout.encoding).find(": warning:") > 0:
132                print(relpath, ":", "FAIL")
133                file_fail += 1
134            else:
135                print(relpath, ":", "PASS")
136                file_pass += 1
137                cleanfiles.write(bytes(relpath + "\n"))
138                cleanfiles.flush()
139
140        total_files_count += file_count
141        total_files_pass += file_pass
142        total_files_fail += file_fail
143
144        if file_count > 0:
145            percent = int(100.0 * (float(file_pass) / float(file_count)))
146            style = get_style(file_count, file_pass)
147            output.write(
148                bytes(
149                    TABLE_ROW.format(
150                        path=path,
151                        count=file_count,
152                        passes=file_pass,
153                        fails=file_fail,
154                        percent=str(percent),
155                        style="",
156                        style2=style,
157                    ).encode("utf-8")
158                )
159            )
160            output.flush()
161
162            print("----\n")
163            print(path, file_count, file_pass, file_fail, percent)
164            print("----\n")
165
166    total_percent = float(total_files_pass) / float(total_files_count)
167    percent_str = str(int(100.0 * total_percent))
168    output.write(
169        bytes(
170            TABLE_ROW.format(
171                path="Total",
172                count=total_files_count,
173                passes=total_files_pass,
174                fails=total_files_fail,
175                percent=percent_str,
176                style=":total:",
177                style2=":total:",
178            ).encode("utf-8")
179        )
180    )
181