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