1#!/usr/bin/env python
2# Copyright (c) 2012 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"""Given a filename as an argument, sort the #include/#imports in that file.
7
8Shows a diff and prompts for confirmation before doing the deed.
9Works great with tools/git/for-all-touched-files.py.
10"""
11
12import optparse
13import os
14import sys
15
16
17def YesNo(prompt):
18  """Prompts with a yes/no question, returns True if yes."""
19  print prompt,
20  sys.stdout.flush()
21  # http://code.activestate.com/recipes/134892/
22  if sys.platform == 'win32':
23    import msvcrt
24    ch = msvcrt.getch()
25  else:
26    import termios
27    import tty
28    fd = sys.stdin.fileno()
29    old_settings = termios.tcgetattr(fd)
30    ch = 'n'
31    try:
32      tty.setraw(sys.stdin.fileno())
33      ch = sys.stdin.read(1)
34    finally:
35      termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
36  print ch
37  return ch in ('Y', 'y')
38
39
40def IncludeCompareKey(line):
41  """Sorting comparator key used for comparing two #include lines.
42  Returns the filename without the #include/#import/import prefix.
43  """
44  for prefix in ('#include ', '#import ', 'import '):
45    if line.startswith(prefix):
46      line = line[len(prefix):]
47      break
48
49  # The win32 api has all sorts of implicit include order dependencies :-/
50  # Give a few headers special sort keys that make sure they appear before all
51  # other headers.
52  if line.startswith('<windows.h>'):  # Must be before e.g. shellapi.h
53    return '0'
54  if line.startswith('<atlbase.h>'):  # Must be before atlapp.h.
55    return '1' + line
56  if line.startswith('<ole2.h>'):  # Must be before e.g. intshcut.h
57    return '1' + line
58  if line.startswith('<unknwn.h>'):  # Must be before e.g. intshcut.h
59    return '1' + line
60
61  # C++ system headers should come after C system headers.
62  if line.startswith('<'):
63    if line.find('.h>') != -1:
64      return '2' + line.lower()
65    else:
66      return '3' + line.lower()
67
68  return '4' + line
69
70
71def IsInclude(line):
72  """Returns True if the line is an #include/#import/import line."""
73  return any([line.startswith('#include '), line.startswith('#import '),
74              line.startswith('import ')])
75
76
77def SortHeader(infile, outfile):
78  """Sorts the headers in infile, writing the sorted file to outfile."""
79  for line in infile:
80    if IsInclude(line):
81      headerblock = []
82      while IsInclude(line):
83        infile_ended_on_include_line = False
84        headerblock.append(line)
85        # Ensure we don't die due to trying to read beyond the end of the file.
86        try:
87          line = infile.next()
88        except StopIteration:
89          infile_ended_on_include_line = True
90          break
91      for header in sorted(headerblock, key=IncludeCompareKey):
92        outfile.write(header)
93      if infile_ended_on_include_line:
94        # We already wrote the last line above; exit to ensure it isn't written
95        # again.
96        return
97      # Intentionally fall through, to write the line that caused
98      # the above while loop to exit.
99    outfile.write(line)
100
101
102def FixFileWithConfirmFunction(filename, confirm_function,
103                               perform_safety_checks):
104  """Creates a fixed version of the file, invokes |confirm_function|
105  to decide whether to use the new file, and cleans up.
106
107  |confirm_function| takes two parameters, the original filename and
108  the fixed-up filename, and returns True to use the fixed-up file,
109  false to not use it.
110
111  If |perform_safety_checks| is True, then the function checks whether it is
112  unsafe to reorder headers in this file and skips the reorder with a warning
113  message in that case.
114  """
115  if perform_safety_checks and IsUnsafeToReorderHeaders(filename):
116    print ('Not reordering headers in %s as the script thinks that the '
117           'order of headers in this file is semantically significant.'
118           % (filename))
119    return
120  fixfilename = filename + '.new'
121  infile = open(filename, 'rb')
122  outfile = open(fixfilename, 'wb')
123  SortHeader(infile, outfile)
124  infile.close()
125  outfile.close()  # Important so the below diff gets the updated contents.
126
127  try:
128    if confirm_function(filename, fixfilename):
129      if sys.platform == 'win32':
130        os.unlink(filename)
131      os.rename(fixfilename, filename)
132  finally:
133    try:
134      os.remove(fixfilename)
135    except OSError:
136      # If the file isn't there, we don't care.
137      pass
138
139
140def DiffAndConfirm(filename, should_confirm, perform_safety_checks):
141  """Shows a diff of what the tool would change the file named
142  filename to.  Shows a confirmation prompt if should_confirm is true.
143  Saves the resulting file if should_confirm is false or the user
144  answers Y to the confirmation prompt.
145  """
146  def ConfirmFunction(filename, fixfilename):
147    diff = os.system('diff -u %s %s' % (filename, fixfilename))
148    if sys.platform != 'win32':
149      diff >>= 8
150    if diff == 0:  # Check exit code.
151      print '%s: no change' % filename
152      return False
153
154    return (not should_confirm or YesNo('Use new file (y/N)?'))
155
156  FixFileWithConfirmFunction(filename, ConfirmFunction, perform_safety_checks)
157
158def IsUnsafeToReorderHeaders(filename):
159  # *_message_generator.cc is almost certainly a file that generates IPC
160  # definitions. Changes in include order in these files can result in them not
161  # building correctly.
162  if filename.find("message_generator.cc") != -1:
163    return True
164  return False
165
166def main():
167  parser = optparse.OptionParser(usage='%prog filename1 filename2 ...')
168  parser.add_option('-f', '--force', action='store_false', default=True,
169                    dest='should_confirm',
170                    help='Turn off confirmation prompt.')
171  parser.add_option('--no_safety_checks',
172                    action='store_false', default=True,
173                    dest='perform_safety_checks',
174                    help='Do not perform the safety checks via which this '
175                    'script refuses to operate on files for which it thinks '
176                    'the include ordering is semantically significant.')
177  opts, filenames = parser.parse_args()
178
179  if len(filenames) < 1:
180    parser.print_help()
181    return 1
182
183  for filename in filenames:
184    DiffAndConfirm(filename, opts.should_confirm, opts.perform_safety_checks)
185
186
187if __name__ == '__main__':
188  sys.exit(main())
189