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