Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(5)

Side by Side Diff: tools/clang/scripts/run_tool.py

Issue 12746010: Implement clang tool that converts std::string("") to std::string(). (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Remove debugging print and add comment. Created 7 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright (c) 2013 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 """Wrapper script to help run clang tools across Chromium code.
7
8 The clang tool implementation doesn't take advantage of multiple cores, and if
9 it fails mysteriously in the middle, all the generated replacements will be
10 lost.
11
12 Unfortunately, if the work is simply sharded across multiple cores by running
13 multiple RefactoringTools, problems arise when they attempt to rewrite a file at
14 the same time. To work around that, clang tools that are run using this tool
15 should output edits to stdout in the following format:
16 ==== BEGIN EDITS ====
17 r:<file path>:<offset>:<length>:<replacement text>
18 r:<file path>:<offset>:<length>:<replacement text>
19 ...etc...
20 ==== END EDITS ====
21
22 Any generated edits are applied once the clang tool has finished running
23 across Chromium, regardless of whether some instances failed or not.
24 """
25
26 import collections
27 import functools
28 import multiprocessing
29 import os.path
30 import subprocess
31 import sys
32
33
34 Edit = collections.namedtuple(
35 'Edit', ('edit_type', 'offset', 'length', 'replacement'))
36
37
38 def _GetFilesFromGit(paths = None):
39 """Gets the list of files in the git repository.
40
41 Args:
42 paths: Prefix filter for the returned paths. May contain multiple entries.
43 """
44 args = ['git', 'ls-files']
45 if paths:
46 args.extend(paths)
47 command = subprocess.Popen(args, stdout=subprocess.PIPE)
48 output, _ = command.communicate()
49 return output.splitlines()
50
51
52 def _ExtractEditsFromStdout(build_directory, stdout):
53 """Extracts generated list of edits from the tool's stdout.
54
55 The expected format is documented at the top of this file.
56
57 Args:
58 build_directory: Directory that contains the compile database. Used to
59 normalize the filenames.
60 stdout: The stdout from running the clang tool.
61
62 Returns:
63 A dictionary mapping filenames to the associated edits.
64 """
65 lines = stdout.splitlines()
66 start_index = lines.index('==== BEGIN EDITS ====')
67 end_index = lines.index('==== END EDITS ====')
68 edits = collections.defaultdict(list)
69 for line in lines[start_index + 1:end_index]:
70 try:
71 edit_type, path, offset, length, replacement = line.split(':', 4)
72 # Normalize the file path emitted by the clang tool to be relative to the
73 # current working directory.
74 path = os.path.relpath(os.path.join(build_directory, path))
75 edits[path].append(Edit(edit_type, int(offset), int(length), replacement))
76 except ValueError:
77 print 'Unable to parse edit: %s' % line
78 return edits
79
80
81 def _ExecuteTool(toolname, build_directory, filename):
82 """Executes the tool.
83
84 This is defined outside the class so it can be pickled for the multiprocessing
85 module.
86
87 Args:
88 toolname: Path to the tool to execute.
89 build_directory: Directory that contains the compile database.
90 filename: The file to run the tool over.
91
92 Returns:
93 A dictionary that must contain the key "status" and a boolean value
94 associated with it.
95
96 If status is True, then the generated edits are stored with the key "edits"
97 in the dictionary.
98
99 Otherwise, the filename and the output from stderr are associated with the
100 keys "filename" and "stderr" respectively.
101 """
102 command = subprocess.Popen((toolname, '-p', build_directory, filename),
103 stdout=subprocess.PIPE,
104 stderr=subprocess.PIPE)
105 stdout, stderr = command.communicate()
106 if command.returncode != 0:
107 return {'status': False, 'filename': filename, 'stderr': stderr}
108 else:
109 return {'status': True,
110 'edits': _ExtractEditsFromStdout(build_directory, stdout)}
111
112
113 class _CompilerDispatcher(object):
114 """Multiprocessing controller for running clang tools in parallel."""
115
116 def __init__(self, toolname, build_directory, filenames):
117 """Initializer method.
118
119 Args:
120 toolname: Path to the tool to execute.
121 build_directory: Directory that contains the compile database.
122 filenames: The files to run the tool over.
123 """
124 self.__toolname = toolname
125 self.__build_directory = build_directory
126 self.__filenames = filenames
127 self.__success_count = 0
128 self.__failed_count = 0
129 self.__edits = collections.defaultdict(list)
130
131 @property
132 def edits(self):
133 return self.__edits
134
135 def Run(self):
136 """Does the grunt work."""
137 pool = multiprocessing.Pool()
138 result_iterator = pool.imap_unordered(
139 functools.partial(_ExecuteTool, self.__toolname,
140 self.__build_directory),
141 self.__filenames)
142 for result in result_iterator:
143 self.__ProcessResult(result)
144 sys.stdout.write('\n')
145 sys.stdout.flush()
146
147 def __ProcessResult(self, result):
148 """Handles result processing.
149
150 Args:
151 result: The result dictionary returned by _ExecuteTool.
152 """
153 if result['status']:
154 self.__success_count += 1
155 for k, v in result['edits'].iteritems():
156 self.__edits[k].extend(v)
157 else:
158 self.__failed_count += 1
159 sys.stdout.write('\nFailed to process %s\n' % result['filename'])
160 sys.stdout.write(result['stderr'])
161 sys.stdout.write('\n')
162 percentage = (
163 float(self.__success_count + self.__failed_count) /
164 len(self.__filenames)) * 100
165 sys.stdout.write('Succeeded: %d, Failed: %d [%.2f%%]\r' % (
166 self.__success_count, self.__failed_count, percentage))
167 sys.stdout.flush()
168
169
170 def _ApplyEdits(edits):
171 """Apply the generated edits.
172
173 Args:
174 edits: A dict mapping filenames to Edit instances that apply to that file.
175 """
176 edit_count = 0
177 for k, v in edits.iteritems():
178 # Sort the edits and iterate through them in reverse order. Sorting allows
179 # duplicate edits to be quickly skipped, while reversing means that
180 # subsequent edits don't need to have their offsets updated with each edit
181 # applied.
182 v.sort()
183 last_edit = None
184 with open(k, 'rb+') as f:
185 contents = bytearray(f.read())
186 for edit in reversed(v):
187 if edit == last_edit:
188 continue
189 last_edit = edit
190 contents[edit.offset:edit.offset + edit.length] = edit.replacement
191 if not edit.replacement:
192 _ExtendDeletionIfElementIsInList(contents, edit.offset)
193 edit_count += 1
194 f.seek(0)
195 f.truncate()
196 f.write(contents)
197 print 'Applied %d edits to %d files' % (edit_count, len(edits))
198
199
200 _WHITESPACE_BYTES = frozenset((ord('\t'), ord('\n'), ord('\r'), ord(' ')))
201
202
203 def _ExtendDeletionIfElementIsInList(contents, offset):
204 """Extends the range of a deletion if the deleted element was part of a list.
205
206 This rewriter helper makes it easy for refactoring tools to remove elements
207 from a list. Even if a matcher callback knows that it is removing an element
208 from a list, it may not have enough information to accurately remove the list
209 element; for example, another matcher callback may end up removing an adjacent
210 list element, or all the list elements may end up being removed.
211
212 With this helper, refactoring tools can simply remove the list element and not
213 worry about having to include the comma in the replacement.
214
215 Args:
216 contents: A bytearray with the deletion already applied.
217 offset: The offset in the bytearray where the deleted range used to be.
218 """
219 char_before = char_after = None
220 left_trim_count = 0
221 for byte in reversed(contents[:offset]):
222 left_trim_count += 1
223 if byte in _WHITESPACE_BYTES:
224 continue
225 if byte in (ord(','), ord(':'), ord('('), ord('{')):
226 char_before = chr(byte)
227 break
228
229 right_trim_count = 0
230 for byte in contents[offset:]:
231 right_trim_count += 1
232 if byte in _WHITESPACE_BYTES:
233 continue
234 if byte == ord(','):
235 char_after = chr(byte)
236 break
237
238 if char_before:
239 if char_after:
240 del contents[offset:offset + right_trim_count]
241 elif char_before in (',', ':'):
242 del contents[offset - left_trim_count:offset]
243
244
245 def main(argv):
246 if len(argv) < 2:
247 print 'Usage: run_tool.py <clang tool> <compile DB> <path 1> <path 2> ...'
248 print ' <clang tool> is the clang tool that should be run.'
249 print ' <compile db> is the directory that contains the compile database'
250 print ' <path 1> <path2> ... can be used to filter what files are edited'
251 sys.exit(1)
252
253 filenames = frozenset(_GetFilesFromGit(argv[2:]))
254 # Filter out files that aren't C/C++/Obj-C/Obj-C++.
255 extensions = frozenset(('.c', '.cc', '.m', '.mm'))
256 dispatcher = _CompilerDispatcher(argv[0], argv[1],
257 [f for f in filenames
258 if os.path.splitext(f)[1] in extensions])
259 dispatcher.Run()
260 # Filter out edits to files that aren't in the git repository, since it's not
261 # useful to modify files that aren't under source control--typically, these
262 # are generated files or files in a git submodule that's not part of Chromium.
263 _ApplyEdits({k : v for k, v in dispatcher.edits.iteritems()
264 if k in filenames})
265 # TODO(dcheng): Consider clang-formatting the result to avoid egregious style
266 # violations.
267
268
269 if __name__ == '__main__':
270 sys.exit(main(sys.argv[1:]))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698