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