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