1#!/usr/bin/env python3 2# Copyright 2018 the V8 project authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6import argparse 7from datetime import datetime 8import re 9import subprocess 10import sys 11from pathlib import Path 12import logging 13from multiprocessing import Pool 14 15RE_GITHASH = re.compile(r"^[0-9a-f]{40}") 16RE_AUTHOR_TIME = re.compile(r"^author-time (\d+)$") 17RE_FILENAME = re.compile(r"^filename (.+)$") 18 19VERSION_CACHE = dict() 20RE_VERSION_MAJOR = re.compile(r".*V8_MAJOR_VERSION ([0-9]+)") 21RE_VERSION_MINOR = re.compile(r".*V8_MINOR_VERSION ([0-9]+)") 22 23RE_MACRO_END = re.compile(r"\);") 24RE_DEPRECATE_MACRO = re.compile(r"\(.*?,(.*)\);", re.MULTILINE) 25 26 27class HeaderFile(object): 28 def __init__(self, path): 29 self.path = path 30 self.blame_list = self.get_blame_list() 31 32 @classmethod 33 def get_api_header_files(cls, options): 34 files = subprocess.check_output( 35 ['git', 'ls-tree', '--name-only', '-r', 'HEAD', options.include_dir], 36 encoding='UTF-8') 37 files = map(Path, filter(lambda l: l.endswith('.h'), files.splitlines())) 38 with Pool(processes=24) as pool: 39 return pool.map(cls, files) 40 41 def extract_version(self, hash): 42 if hash in VERSION_CACHE: 43 return VERSION_CACHE[hash] 44 if hash == '0000000000000000000000000000000000000000': 45 return 'HEAD' 46 result = subprocess.check_output( 47 ['git', 'show', f"{hash}:include/v8-version.h"], encoding='UTF-8') 48 major = RE_VERSION_MAJOR.search(result).group(1) 49 minor = RE_VERSION_MINOR.search(result).group(1) 50 version = f"{major}.{minor}" 51 VERSION_CACHE[hash] = version 52 return version 53 54 def get_blame_list(self): 55 logging.info(f"blame list for {self.path}") 56 result = subprocess.check_output( 57 ['git', 'blame', '-t', '--line-porcelain', self.path], 58 encoding='UTF-8') 59 line_iter = iter(result.splitlines()) 60 blame_list = list() 61 current_blame = None 62 while True: 63 line = next(line_iter, None) 64 if line is None: 65 break 66 if RE_GITHASH.match(line): 67 if current_blame is not None: 68 blame_list.append(current_blame) 69 hash = line.split(" ")[0] 70 current_blame = { 71 'datetime': 0, 72 'filename': None, 73 'content': None, 74 'hash': hash 75 } 76 continue 77 match = RE_AUTHOR_TIME.match(line) 78 if match: 79 current_blame['datetime'] = datetime.fromtimestamp( 80 int(match.groups()[0])) 81 continue 82 match = RE_FILENAME.match(line) 83 if match: 84 current_blame['filename'] = match.groups()[0] 85 current_blame['content'] = next(line_iter).strip() 86 continue 87 blame_list.append(current_blame) 88 return blame_list 89 90 def filter_and_print(self, macro, options): 91 before = options.before 92 index = 0 93 re_macro = re.compile(macro) 94 deprecated = list() 95 while index < len(self.blame_list): 96 blame = self.blame_list[index] 97 line = blame['content'] 98 if line.startswith("#") or line.startswith("//"): 99 index += 1 100 continue 101 commit_datetime = blame['datetime'] 102 if commit_datetime >= before: 103 index += 1 104 continue 105 commit_hash = blame['hash'] 106 match = re_macro.search(line) 107 if match: 108 pos = match.end() 109 start = -1 110 parens = 0 111 while True: 112 if pos >= len(line): 113 # Extend to next line 114 index = index + 1 115 blame = self.blame_list[index] 116 line = line + blame['content'] 117 if line[pos] == '(': 118 parens = parens + 1 119 elif line[pos] == ')': 120 parens = parens - 1 121 if parens == 0: 122 # Exclude closing ") 123 pos = pos - 1 124 break 125 elif line[pos] == '"' and start == -1: 126 start = pos + 1 127 pos = pos + 1 128 # Extract content and replace double quotes from merged lines 129 content = line[start:pos].strip().replace('""', '') 130 deprecated.append((index + 1, commit_datetime, commit_hash, content)) 131 index = index + 1 132 for linenumber, commit_datetime, commit_hash, content in deprecated: 133 self.print_details(linenumber, commit_datetime, commit_hash, content) 134 135 def print_details(self, linenumber, commit_datetime, commit_hash, content): 136 commit_date = commit_datetime.date() 137 file_position = (f"{self.path}:{linenumber}").ljust(40) 138 v8_version = f"v{self.extract_version(commit_hash)}".rjust(5) 139 print(f"{file_position} {v8_version} {commit_date} {commit_hash[:8]}" 140 f" {content}") 141 142 def print_v8_version(self, options): 143 commit_hash, commit_datetime = subprocess.check_output( 144 ['git', 'log', '-1', '--format=%H%n%ct', self.path], 145 encoding='UTF-8').splitlines() 146 commit_datetime = datetime.fromtimestamp(int(commit_datetime)) 147 self.print_details(11, commit_datetime, commit_hash, content="") 148 149 150def parse_options(args): 151 parser = argparse.ArgumentParser( 152 description="Collect deprecation statistics") 153 parser.add_argument("include_dir", nargs='?', help="Path to includes dir") 154 parser.add_argument("--before", help="Filter by date") 155 parser.add_argument("--verbose", 156 "-v", 157 help="Verbose logging", 158 action="store_true") 159 options = parser.parse_args(args) 160 if options.verbose: 161 logging.basicConfig(level=logging.DEBUG) 162 if options.before: 163 options.before = datetime.strptime(options.before, '%Y-%m-%d') 164 else: 165 options.before = datetime.now() 166 if options.include_dir is None: 167 base_path = Path(__file__).parent.parent 168 options.include_dir = str((base_path / 'include').relative_to(base_path)) 169 return options 170 171 172def main(args): 173 options = parse_options(args) 174 175 print("# CURRENT V8 VERSION:") 176 version = HeaderFile(Path(options.include_dir) / 'v8-version.h') 177 version.print_v8_version(options) 178 179 header_files = HeaderFile.get_api_header_files(options) 180 print("\n") 181 print("# V8_DEPRECATE_SOON:") 182 for header in header_files: 183 header.filter_and_print("V8_DEPRECATE_SOON", options) 184 185 print("\n") 186 print("# V8_DEPRECATED:") 187 for header in header_files: 188 header.filter_and_print("V8_DEPRECATED", options) 189 190 191if __name__ == "__main__": 192 main(sys.argv[1:]) 193