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