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