• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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