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 81 82with open(DOC_FILE, "wb") as output: 83 cleanfiles = open(CLEAN_FILE, "wb") 84 sha = get_git_revision_short_hash() 85 today = datetime.now().strftime("%B %d, %Y %H:%M:%S") 86 output.write(bytes(RST_PREFIX.format(today=today, 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=subprocess.DEVNULL, 103 stderr=subprocess.DEVNULL, 104 ) 105 if git_check.wait() != 0: 106 print("Skipping directory: ", act_sub_dir) 107 subdirs.remove(subdir) 108 109 path = os.path.relpath(root, TOP_DIR) 110 path = path.replace("\\", "/") 111 112 file_count = 0 113 file_pass = 0 114 file_fail = 0 115 for filename in files: 116 file_path = os.path.join(root, filename) 117 ext = os.path.splitext(file_path)[-1].lower() 118 if not ext.endswith(suffixes): 119 continue 120 121 file_count += 1 122 123 args = ["clang-format", "-n", file_path] 124 cmd = subprocess.Popen(args, stderr=subprocess.PIPE) 125 stdout, err = cmd.communicate() 126 127 relpath = os.path.relpath(file_path, TOP_DIR) 128 relpath = relpath.replace("\\", "/") 129 if err.decode(sys.stdout.encoding).find(": warning:") > 0: 130 print(relpath, ":", "FAIL") 131 file_fail += 1 132 else: 133 print(relpath, ":", "PASS") 134 file_pass += 1 135 cleanfiles.write(bytes(relpath + "\n")) 136 cleanfiles.flush() 137 138 total_files_count += file_count 139 total_files_pass += file_pass 140 total_files_fail += file_fail 141 142 if file_count > 0: 143 percent = int(100.0 * (float(file_pass) / float(file_count))) 144 style = get_style(file_count, file_pass) 145 output.write( 146 bytes( 147 TABLE_ROW.format( 148 path=path, 149 count=file_count, 150 passes=file_pass, 151 fails=file_fail, 152 percent=str(percent), 153 style="", 154 style2=style, 155 ).encode("utf-8") 156 ) 157 ) 158 output.flush() 159 160 print("----\n") 161 print(path, file_count, file_pass, file_fail, percent) 162 print("----\n") 163 164 total_percent = float(total_files_pass) / float(total_files_count) 165 percent_str = str(int(100.0 * total_percent)) 166 output.write( 167 bytes( 168 TABLE_ROW.format( 169 path="Total", 170 count=total_files_count, 171 passes=total_files_pass, 172 fails=total_files_fail, 173 percent=percent_str, 174 style=":total:", 175 style2=":total:", 176 ).encode("utf-8") 177 ) 178 ) 179