sort_sources.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. #!/usr/bin/env python
  2. # Copyright 2015 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. """Given a GYP/GN filename, sort C-ish source files in that file.
  6. Shows a diff and prompts for confirmation before doing the deed.
  7. Works great with tools/git/for-all-touched-files.py.
  8. Limitations:
  9. 1) Comments used as section headers
  10. If a comment (1+ lines starting with #) appears in a source list without a
  11. preceding blank line, the tool assumes that the comment is about the next
  12. line. For example, given the following source list,
  13. sources = [
  14. "b.cc",
  15. # Comment.
  16. "a.cc",
  17. "c.cc",
  18. ]
  19. the tool will produce the following output:
  20. sources = [
  21. # Comment.
  22. "a.cc",
  23. "b.cc",
  24. "c.cc",
  25. ]
  26. This is not correct if the comment is for starting a new section like:
  27. sources = [
  28. "b.cc",
  29. # These are for Linux.
  30. "a.cc",
  31. "c.cc",
  32. ]
  33. The tool cannot disambiguate the two types of comments. The problem can be
  34. worked around by inserting a blank line before the comment because the tool
  35. interprets a blank line as the end of a source list.
  36. 2) Sources commented out
  37. Sometimes sources are commented out with their positions kept in the
  38. alphabetical order, but what if the list is not sorted correctly? For
  39. example, given the following source list,
  40. sources = [
  41. "a.cc",
  42. # "b.cc",
  43. "d.cc",
  44. "c.cc",
  45. ]
  46. the tool will produce the following output:
  47. sources = [
  48. "a.cc",
  49. "c.cc",
  50. # "b.cc",
  51. "d.cc",
  52. ]
  53. This is because the tool assumes that the comment (# "b.cc",) is about the
  54. next line ("d.cc",). This kind of errors should be fixed manually, or the
  55. commented-out code should be deleted.
  56. 3) " and ' are used both used in the same source list (GYP only problem)
  57. If both " and ' are used in the same source list, sources quoted with " will
  58. appear first in the output. The problem is rare enough so the tool does not
  59. attempt to normalize them. Hence this kind of errors should be fixed
  60. manually.
  61. 4) Spaces and tabs used in the same source list
  62. Similarly, if spaces and tabs are both used in the same source list, sources
  63. indented with tabs will appear first in the output. This kind of errors
  64. should be fixed manually.
  65. """
  66. from __future__ import print_function
  67. import difflib
  68. import optparse
  69. import re
  70. import sys
  71. from yes_no import YesNo
  72. SUFFIXES = ['c', 'cc', 'cpp', 'h', 'mm', 'rc', 'rc.version', 'ico', 'def',
  73. 'release']
  74. SOURCE_PATTERN = re.compile(r'^\s+[\'"].*\.(%s)[\'"],$' %
  75. '|'.join([re.escape(x) for x in SUFFIXES]))
  76. COMMENT_PATTERN = re.compile(r'^\s+#')
  77. def SortSources(original_lines):
  78. """Sort source file names in |original_lines|.
  79. Args:
  80. original_lines: Lines of the original content as a list of strings.
  81. Returns:
  82. Lines of the sorted content as a list of strings.
  83. The algorithm is fairly naive. The code tries to find a list of C-ish
  84. source file names by a simple regex, then sort them. The code does not try
  85. to understand the syntax of the build files. See the file comment above for
  86. details.
  87. """
  88. output_lines = []
  89. comments = []
  90. sources = []
  91. for line in original_lines:
  92. if re.search(COMMENT_PATTERN, line):
  93. comments.append(line)
  94. elif re.search(SOURCE_PATTERN, line):
  95. # Associate the line with the preceding comments.
  96. sources.append([line, comments])
  97. comments = []
  98. else:
  99. # |sources| should be flushed first, to handle comments at the end of a
  100. # source list correctly.
  101. if sources:
  102. for source_line, source_comments in sorted(sources):
  103. output_lines.extend(source_comments)
  104. output_lines.append(source_line)
  105. sources = []
  106. if comments:
  107. output_lines.extend(comments)
  108. comments = []
  109. output_lines.append(line)
  110. return output_lines
  111. def ProcessFile(filename, should_confirm):
  112. """Process the input file and rewrite if needed.
  113. Args:
  114. filename: Path to the input file.
  115. should_confirm: If true, diff and confirmation prompt are shown.
  116. """
  117. original_lines = []
  118. with open(filename, 'r') as input_file:
  119. for line in input_file:
  120. original_lines.append(line)
  121. new_lines = SortSources(original_lines)
  122. if original_lines == new_lines:
  123. print('%s: no change' % filename)
  124. return
  125. if should_confirm:
  126. diff = difflib.unified_diff(original_lines, new_lines)
  127. sys.stdout.writelines(diff)
  128. if not YesNo('Use new file (y/N)'):
  129. return
  130. with open(filename, 'w') as output_file:
  131. output_file.writelines(new_lines)
  132. def main():
  133. parser = optparse.OptionParser(usage='%prog filename1 filename2 ...')
  134. parser.add_option('-f', '--force', action='store_false', default=True,
  135. dest='should_confirm',
  136. help='Turn off confirmation prompt.')
  137. opts, filenames = parser.parse_args()
  138. if len(filenames) < 1:
  139. parser.print_help()
  140. return 1
  141. for filename in filenames:
  142. ProcessFile(filename, opts.should_confirm)
  143. if __name__ == '__main__':
  144. sys.exit(main())