1#!/usr/bin/env python
2# Copyright 2017 The Chromium 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
6"""Fix header files missing in GN.
7
8This script takes the missing header files from check_gn_headers.py, and
9try to fix them by adding them to the GN files.
10Manual cleaning up is likely required afterwards.
11"""
12
13import argparse
14import os
15import re
16import subprocess
17import sys
18
19
20def GitGrep(pattern):
21  p = subprocess.Popen(
22      ['git', 'grep', '-En', pattern, '--', '*.gn', '*.gni'],
23      stdout=subprocess.PIPE)
24  out, _ = p.communicate()
25  return out, p.returncode
26
27
28def ValidMatches(basename, cc, grep_lines):
29  """Filter out 'git grep' matches with header files already."""
30  matches = []
31  for line in grep_lines:
32    gnfile, linenr, contents = line.split(':')
33    linenr = int(linenr)
34    new = re.sub(cc, basename, contents)
35    lines = open(gnfile).read().splitlines()
36    assert contents in lines[linenr - 1]
37    # Skip if it's already there. It could be before or after the match.
38    if lines[linenr] == new:
39      continue
40    if lines[linenr - 2] == new:
41      continue
42    print '    ', gnfile, linenr, new
43    matches.append((gnfile, linenr, new))
44  return matches
45
46
47def AddHeadersNextToCC(headers, skip_ambiguous=True):
48  """Add header files next to the corresponding .cc files in GN files.
49
50  When skip_ambiguous is True, skip if multiple .cc files are found.
51  Returns unhandled headers.
52
53  Manual cleaning up is likely required, especially if not skip_ambiguous.
54  """
55  edits = {}
56  unhandled = []
57  for filename in headers:
58    filename = filename.strip()
59    if not (filename.endswith('.h') or filename.endswith('.hh')):
60      continue
61    basename = os.path.basename(filename)
62    print filename
63    cc = r'\b' + os.path.splitext(basename)[0] + r'\.(cc|cpp|mm)\b'
64    out, returncode = GitGrep('(/|")' + cc + '"')
65    if returncode != 0 or not out:
66      unhandled.append(filename)
67      continue
68
69    matches = ValidMatches(basename, cc, out.splitlines())
70
71    if len(matches) == 0:
72      continue
73    if len(matches) > 1:
74      print '\n[WARNING] Ambiguous matching for', filename
75      for i in enumerate(matches, 1):
76        print '%d: %s' % (i[0], i[1])
77      print
78      if skip_ambiguous:
79        continue
80
81      picked = raw_input('Pick the matches ("2,3" for multiple): ')
82      try:
83        matches = [matches[int(i) - 1] for i in picked.split(',')]
84      except (ValueError, IndexError):
85        continue
86
87    for match in matches:
88      gnfile, linenr, new = match
89      print '  ', gnfile, linenr, new
90      edits.setdefault(gnfile, {})[linenr] = new
91
92  for gnfile in edits:
93    lines = open(gnfile).read().splitlines()
94    for l in sorted(edits[gnfile].keys(), reverse=True):
95      lines.insert(l, edits[gnfile][l])
96    open(gnfile, 'w').write('\n'.join(lines) + '\n')
97
98  return unhandled
99
100
101def AddHeadersToSources(headers, skip_ambiguous=True):
102  """Add header files to the sources list in the first GN file.
103
104  The target GN file is the first one up the parent directories.
105  This usually does the wrong thing for _test files if the test and the main
106  target are in the same .gn file.
107  When skip_ambiguous is True, skip if multiple sources arrays are found.
108
109  "git cl format" afterwards is required. Manually cleaning up duplicated items
110  is likely required.
111  """
112  for filename in headers:
113    filename = filename.strip()
114    print filename
115    dirname = os.path.dirname(filename)
116    while not os.path.exists(os.path.join(dirname, 'BUILD.gn')):
117      dirname = os.path.dirname(dirname)
118    rel = filename[len(dirname) + 1:]
119    gnfile = os.path.join(dirname, 'BUILD.gn')
120
121    lines = open(gnfile).read().splitlines()
122    matched = [i for i, l in enumerate(lines) if ' sources = [' in l]
123    if skip_ambiguous and len(matched) > 1:
124      print '[WARNING] Multiple sources in', gnfile
125      continue
126
127    if len(matched) < 1:
128      continue
129    print '  ', gnfile, rel
130    index = matched[0]
131    lines.insert(index + 1, '"%s",' % rel)
132    open(gnfile, 'w').write('\n'.join(lines) + '\n')
133
134
135def RemoveHeader(headers, skip_ambiguous=True):
136  """Remove non-existing headers in GN files.
137
138  When skip_ambiguous is True, skip if multiple matches are found.
139  """
140  edits = {}
141  unhandled = []
142  for filename in headers:
143    filename = filename.strip()
144    if not (filename.endswith('.h') or filename.endswith('.hh')):
145      continue
146    basename = os.path.basename(filename)
147    print filename
148    out, returncode = GitGrep('(/|")' + basename + '"')
149    if returncode != 0 or not out:
150      unhandled.append(filename)
151      print '  Not found'
152      continue
153
154    grep_lines = out.splitlines()
155    matches = []
156    for line in grep_lines:
157      gnfile, linenr, contents = line.split(':')
158      print '    ', gnfile, linenr, contents
159      linenr = int(linenr)
160      lines = open(gnfile).read().splitlines()
161      assert contents in lines[linenr - 1]
162      matches.append((gnfile, linenr, contents))
163
164    if len(matches) == 0:
165      continue
166    if len(matches) > 1:
167      print '\n[WARNING] Ambiguous matching for', filename
168      for i in enumerate(matches, 1):
169        print '%d: %s' % (i[0], i[1])
170      print
171      if skip_ambiguous:
172        continue
173
174      picked = raw_input('Pick the matches ("2,3" for multiple): ')
175      try:
176        matches = [matches[int(i) - 1] for i in picked.split(',')]
177      except (ValueError, IndexError):
178        continue
179
180    for match in matches:
181      gnfile, linenr, contents = match
182      print '  ', gnfile, linenr, contents
183      edits.setdefault(gnfile, set()).add(linenr)
184
185  for gnfile in edits:
186    lines = open(gnfile).read().splitlines()
187    for l in sorted(edits[gnfile], reverse=True):
188      lines.pop(l - 1)
189    open(gnfile, 'w').write('\n'.join(lines) + '\n')
190
191  return unhandled
192
193
194def main():
195  parser = argparse.ArgumentParser()
196  parser.add_argument('input_file', help="missing or non-existing headers, "
197                      "output of check_gn_headers.py")
198  parser.add_argument('--prefix',
199                      help="only handle path name with this prefix")
200  parser.add_argument('--remove', action='store_true',
201                      help="treat input_file as non-existing headers")
202
203  args, _extras = parser.parse_known_args()
204
205  headers = open(args.input_file).readlines()
206
207  if args.prefix:
208    headers = [i for i in headers if i.startswith(args.prefix)]
209
210  if args.remove:
211    RemoveHeader(headers, False)
212  else:
213    unhandled = AddHeadersNextToCC(headers)
214    AddHeadersToSources(unhandled)
215
216
217if __name__ == '__main__':
218  sys.exit(main())
219