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

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

Issue 2599193002: Split run_tool.py into run_tool.py, extract_edits.py and apply_edits.py (Closed)
Patch Set: --similarity=15 Created 3 years, 12 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
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 """Wrapper script to help run clang tools across Chromium code. 5 """Applies edits generated by a clang tools that was run on Chromium code.
dcheng 2016/12/27 07:30:14 Nit: a clang tool or clang tools
Łukasz Anforowicz 2016/12/27 22:33:26 Done.
6 6
7 How to use this tool: 7 Synopsis:
8 If you want to run the tool across all Chromium code:
9 run_tool.py <tool> <path/to/compiledb>
10 8
11 If you want to include all files mentioned in the compilation database: 9 cat run_tool.out | extract_edits.py | apply_edits.py <build dir> <filters...>
12 run_tool.py <tool> <path/to/compiledb> --all
13 10
14 If you only want to run the tool across just chrome/browser and content/browser: 11 For example - to apply edits only to WTF sources:
15 run_tool.py <tool> <path/to/compiledb> chrome/browser content/browser
16 12
17 Please see https://chromium.googlesource.com/chromium/src/+/master/docs/clang_to ol_refactoring.md for more 13 ... | apply_edits.py out/gn third_party/WebKit/Source/wtf
18 information, which documents the entire automated refactoring flow in Chromium.
19 14
20 Why use this tool: 15 In addition to filters specified on the command line, the tool also skips edits
21 The clang tool implementation doesn't take advantage of multiple cores, and if 16 that apply to files that are not covered by git.
22 it fails mysteriously in the middle, all the generated replacements will be
23 lost.
24
25 Unfortunately, if the work is simply sharded across multiple cores by running
26 multiple RefactoringTools, problems arise when they attempt to rewrite a file at
27 the same time. To work around that, clang tools that are run using this tool
28 should output edits to stdout in the following format:
29
30 ==== BEGIN EDITS ====
31 r:<file path>:<offset>:<length>:<replacement text>
32 r:<file path>:<offset>:<length>:<replacement text>
33 ...etc...
34 ==== END EDITS ====
35
36 Any generated edits are applied once the clang tool has finished running
37 across Chromium, regardless of whether some instances failed or not.
38 """ 17 """
39 18
40 import argparse 19 import argparse
41 import collections 20 import collections
42 import functools 21 import functools
43 import multiprocessing 22 import multiprocessing
44 import os 23 import os
45 import os.path 24 import os.path
46 import subprocess 25 import subprocess
47 import sys 26 import sys
(...skipping 20 matching lines...) Expand all
68 else: 47 else:
69 args.append('git') 48 args.append('git')
70 args.append('ls-files') 49 args.append('ls-files')
71 if paths: 50 if paths:
72 args.extend(paths) 51 args.extend(paths)
73 command = subprocess.Popen(args, stdout=subprocess.PIPE) 52 command = subprocess.Popen(args, stdout=subprocess.PIPE)
74 output, _ = command.communicate() 53 output, _ = command.communicate()
75 return [os.path.realpath(p) for p in output.splitlines()] 54 return [os.path.realpath(p) for p in output.splitlines()]
76 55
77 56
78 def _GetFilesFromCompileDB(build_directory): 57 def _ParseEditsFromStdin(build_directory):
79 """ Gets the list of files mentioned in the compilation database.
80
81 Args:
82 build_directory: Directory that contains the compile database.
83 """
84 return [os.path.join(entry['directory'], entry['file'])
85 for entry in compile_db.Read(build_directory)]
86
87
88 def _ExtractEditsFromStdout(build_directory, stdout):
89 """Extracts generated list of edits from the tool's stdout. 58 """Extracts generated list of edits from the tool's stdout.
90 59
91 The expected format is documented at the top of this file. 60 The expected format is documented at the top of this file.
92 61
93 Args: 62 Args:
94 build_directory: Directory that contains the compile database. Used to 63 build_directory: Directory that contains the compile database. Used to
95 normalize the filenames. 64 normalize the filenames.
96 stdout: The stdout from running the clang tool. 65 stdout: The stdout from running the clang tool.
97 66
98 Returns: 67 Returns:
99 A dictionary mapping filenames to the associated edits. 68 A dictionary mapping filenames to the associated edits.
100 """ 69 """
101 lines = stdout.splitlines()
102 start_index = lines.index('==== BEGIN EDITS ====')
103 end_index = lines.index('==== END EDITS ====')
104 edits = collections.defaultdict(list) 70 edits = collections.defaultdict(list)
105 for line in lines[start_index + 1:end_index]: 71 for line in sys.stdin:
72 line = line.rstrip("\n\r")
106 try: 73 try:
107 edit_type, path, offset, length, replacement = line.split(':::', 4) 74 edit_type, path, offset, length, replacement = line.split(':::', 4)
108 replacement = replacement.replace('\0', '\n') 75 replacement = replacement.replace('\0', '\n')
109 # Normalize the file path emitted by the clang tool. 76 # Normalize the file path emitted by the clang tool.
110 path = os.path.realpath(os.path.join(build_directory, path)) 77 path = os.path.realpath(os.path.join(build_directory, path))
111 edits[path].append(Edit(edit_type, int(offset), int(length), replacement)) 78 edits[path].append(Edit(edit_type, int(offset), int(length), replacement))
112 except ValueError: 79 except ValueError:
113 print 'Unable to parse edit: %s' % line 80 sys.stderr.write('Unable to parse edit: %s\n' % line)
114 return edits 81 return edits
115 82
116 83
117 def _ExecuteTool(toolname, tool_args, build_directory, filename): 84 def _ApplyEditsToSingleFile(filename, edits):
118 """Executes the tool. 85 # Sort the edits and iterate through them in reverse order. Sorting allows
119 86 # duplicate edits to be quickly skipped, while reversing means that
120 This is defined outside the class so it can be pickled for the multiprocessing 87 # subsequent edits don't need to have their offsets updated with each edit
121 module. 88 # applied.
122 89 edit_count = 0
123 Args: 90 edits.sort()
124 toolname: Path to the tool to execute. 91 last_edit = None
125 tool_args: Arguments to be passed to the tool. Can be None. 92 with open(filename, 'rb+') as f:
126 build_directory: Directory that contains the compile database. 93 contents = bytearray(f.read())
127 filename: The file to run the tool over. 94 for edit in reversed(edits):
128 95 if edit == last_edit:
129 Returns: 96 continue
130 A dictionary that must contain the key "status" and a boolean value 97 last_edit = edit
131 associated with it. 98 contents[edit.offset:edit.offset + edit.length] = edit.replacement
132 99 if not edit.replacement:
133 If status is True, then the generated edits are stored with the key "edits" 100 _ExtendDeletionIfElementIsInList(contents, edit.offset)
134 in the dictionary. 101 edit_count += 1
135 102 f.seek(0)
136 Otherwise, the filename and the output from stderr are associated with the 103 f.truncate()
137 keys "filename" and "stderr" respectively. 104 f.write(contents)
138 """ 105 return edit_count
139 args = [toolname, '-p', build_directory, filename]
140 if (tool_args):
141 args.extend(tool_args)
142 command = subprocess.Popen(
143 args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
144 stdout, stderr = command.communicate()
145 if command.returncode != 0:
146 return {'status': False, 'filename': filename, 'stderr': stderr}
147 else:
148 return {'status': True,
149 'edits': _ExtractEditsFromStdout(build_directory, stdout)}
150
151
152 class _CompilerDispatcher(object):
153 """Multiprocessing controller for running clang tools in parallel."""
154
155 def __init__(self, toolname, tool_args, build_directory, filenames):
156 """Initializer method.
157
158 Args:
159 toolname: Path to the tool to execute.
160 tool_args: Arguments to be passed to the tool. Can be None.
161 build_directory: Directory that contains the compile database.
162 filenames: The files to run the tool over.
163 """
164 self.__toolname = toolname
165 self.__tool_args = tool_args
166 self.__build_directory = build_directory
167 self.__filenames = filenames
168 self.__success_count = 0
169 self.__failed_count = 0
170 self.__edit_count = 0
171 self.__edits = collections.defaultdict(list)
172
173 @property
174 def edits(self):
175 return self.__edits
176
177 @property
178 def failed_count(self):
179 return self.__failed_count
180
181 def Run(self):
182 """Does the grunt work."""
183 pool = multiprocessing.Pool()
184 result_iterator = pool.imap_unordered(
185 functools.partial(_ExecuteTool, self.__toolname, self.__tool_args,
186 self.__build_directory),
187 self.__filenames)
188 for result in result_iterator:
189 self.__ProcessResult(result)
190 sys.stdout.write('\n')
191 sys.stdout.flush()
192
193 def __ProcessResult(self, result):
194 """Handles result processing.
195
196 Args:
197 result: The result dictionary returned by _ExecuteTool.
198 """
199 if result['status']:
200 self.__success_count += 1
201 for k, v in result['edits'].iteritems():
202 self.__edits[k].extend(v)
203 self.__edit_count += len(v)
204 else:
205 self.__failed_count += 1
206 sys.stdout.write('\nFailed to process %s\n' % result['filename'])
207 sys.stdout.write(result['stderr'])
208 sys.stdout.write('\n')
209 percentage = (float(self.__success_count + self.__failed_count) /
210 len(self.__filenames)) * 100
211 sys.stdout.write('Succeeded: %d, Failed: %d, Edits: %d [%.2f%%]\r' %
212 (self.__success_count, self.__failed_count,
213 self.__edit_count, percentage))
214 sys.stdout.flush()
215
216 106
217 def _ApplyEdits(edits): 107 def _ApplyEdits(edits):
218 """Apply the generated edits. 108 """Apply the generated edits.
219 109
220 Args: 110 Args:
221 edits: A dict mapping filenames to Edit instances that apply to that file. 111 edits: A dict mapping filenames to Edit instances that apply to that file.
222 """ 112 """
223 edit_count = 0 113 edit_count = 0
114 done_files = 0
224 for k, v in edits.iteritems(): 115 for k, v in edits.iteritems():
225 # Sort the edits and iterate through them in reverse order. Sorting allows 116 edit_count += _ApplyEditsToSingleFile(k, v)
226 # duplicate edits to be quickly skipped, while reversing means that 117 done_files += 1
227 # subsequent edits don't need to have their offsets updated with each edit 118 percentage = (float(done_files) / len(edits)) * 100
228 # applied. 119 sys.stderr.write('Applied %d edits to %d files [%.2f%%]\r' %
229 v.sort() 120 (edit_count, done_files, percentage))
230 last_edit = None 121 sys.stderr.flush()
231 with open(k, 'rb+') as f: 122
232 contents = bytearray(f.read()) 123 sys.stderr.write('\n')
233 for edit in reversed(v): 124 sys.stderr.flush()
danakj 2016/12/23 15:45:06 stderr doesn't need flush right?
Łukasz Anforowicz 2016/12/27 22:33:26 Done. Things (e.g. updating the status line) seem
234 if edit == last_edit:
235 continue
236 last_edit = edit
237 contents[edit.offset:edit.offset + edit.length] = edit.replacement
238 if not edit.replacement:
239 _ExtendDeletionIfElementIsInList(contents, edit.offset)
240 edit_count += 1
241 f.seek(0)
242 f.truncate()
243 f.write(contents)
244 print 'Applied %d edits to %d files' % (edit_count, len(edits))
245 125
246 126
247 _WHITESPACE_BYTES = frozenset((ord('\t'), ord('\n'), ord('\r'), ord(' '))) 127 _WHITESPACE_BYTES = frozenset((ord('\t'), ord('\n'), ord('\r'), ord(' ')))
248 128
249 129
250 def _ExtendDeletionIfElementIsInList(contents, offset): 130 def _ExtendDeletionIfElementIsInList(contents, offset):
251 """Extends the range of a deletion if the deleted element was part of a list. 131 """Extends the range of a deletion if the deleted element was part of a list.
252 132
253 This rewriter helper makes it easy for refactoring tools to remove elements 133 This rewriter helper makes it easy for refactoring tools to remove elements
254 from a list. Even if a matcher callback knows that it is removing an element 134 from a list. Even if a matcher callback knows that it is removing an element
(...skipping 29 matching lines...) Expand all
284 164
285 if char_before: 165 if char_before:
286 if char_after: 166 if char_after:
287 del contents[offset:offset + right_trim_count] 167 del contents[offset:offset + right_trim_count]
288 elif char_before in (',', ':'): 168 elif char_before in (',', ':'):
289 del contents[offset - left_trim_count:offset] 169 del contents[offset - left_trim_count:offset]
290 170
291 171
292 def main(): 172 def main():
293 parser = argparse.ArgumentParser() 173 parser = argparse.ArgumentParser()
294 parser.add_argument('tool', help='clang tool to run')
295 parser.add_argument('--all', action='store_true')
296 parser.add_argument( 174 parser.add_argument(
297 '--generate-compdb', 175 'build_directory',
298 action='store_true', 176 help='path to the build dir (dir that edit paths are relative to)')
299 help='regenerate the compile database before running the tool')
300 parser.add_argument(
301 'compile_database',
302 help='path to the directory that contains the compile database')
303 parser.add_argument( 177 parser.add_argument(
304 'path_filter', 178 'path_filter',
305 nargs='*', 179 nargs='*',
306 help='optional paths to filter what files the tool is run on') 180 help='optional paths to filter what files the tool is run on')
307 parser.add_argument(
308 '--tool-args', nargs='*',
309 help='optional arguments passed to the tool')
310 args = parser.parse_args() 181 args = parser.parse_args()
311 182
312 os.environ['PATH'] = '%s%s%s' % (
313 os.path.abspath(os.path.join(
314 os.path.dirname(__file__),
315 '../../../third_party/llvm-build/Release+Asserts/bin')),
316 os.pathsep,
317 os.environ['PATH'])
318
319 if args.generate_compdb:
320 compile_db.GenerateWithNinja(args.compile_database)
321
322 filenames = set(_GetFilesFromGit(args.path_filter)) 183 filenames = set(_GetFilesFromGit(args.path_filter))
323 if args.all: 184 edits = _ParseEditsFromStdin(args.build_directory)
324 source_filenames = set(_GetFilesFromCompileDB(args.compile_database))
325 else:
326 # Filter out files that aren't C/C++/Obj-C/Obj-C++.
327 extensions = frozenset(('.c', '.cc', '.cpp', '.m', '.mm'))
328 source_filenames = [f
329 for f in filenames
330 if os.path.splitext(f)[1] in extensions]
331 dispatcher = _CompilerDispatcher(args.tool, args.tool_args,
332 args.compile_database,
333 source_filenames)
334 dispatcher.Run()
335 # Filter out edits to files that aren't in the git repository, since it's not
336 # useful to modify files that aren't under source control--typically, these
337 # are generated files or files in a git submodule that's not part of Chromium.
338 _ApplyEdits({k: v 185 _ApplyEdits({k: v
339 for k, v in dispatcher.edits.iteritems() 186 for k, v in edits.iteritems()
340 if os.path.realpath(k) in filenames}) 187 if os.path.realpath(k) in filenames})
341 return -dispatcher.failed_count 188 return 0
dcheng 2016/12/27 07:30:14 In theory, we could detect overlapping conflicting
Łukasz Anforowicz 2016/12/27 22:33:26 Done. Hopefully what I did is "Good Enough (tm)"
342 189
343 190
344 if __name__ == '__main__': 191 if __name__ == '__main__':
345 sys.exit(main()) 192 sys.exit(main())
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698