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# Script to apply fixits generated by clang. This is to work around the fact
7# that clang's -Xclang -fixit-recompile flag, which automatically applies fixits
8# and recompiles, doesn't work well with parallel invocations of clang.
9#
10# Usage:
11# 1. Enable parseable fixits and disable warnings as errors. Instructions for
12#    doing this vary based on the build environment, but for GN, warnings as
13#    errors can be disabled by setting treat_warnings_as_errors = false
14#    Enabling parseable fixits requires editing build/config/compiler/BUILD.gn
15#    and adding `-fdiagnostics-parseable-fixits` to cflags.
16# 2. Build everything and capture the output:
17#      ninja -C <build_directory> &> generated-fixits
18# 3. Apply the fixits with this script:
19#      python apply_fixits.py[ <build_directory>] < generated-fixits
20#    <build_directory> is optional and only required if your build directory is
21#    a non-standard location.
22
23import argparse
24import collections
25import fileinput
26import os
27import re
28import sys
29
30# fix-it:"../../base/threading/sequenced_worker_pool.h":{341:3-341:11}:""
31# Note that the file path is relative to the build directory.
32_FIXIT_RE = re.compile(r'^fix-it:"(?P<file>.+?)":'
33                       r'{(?P<start_line>\d+?):(?P<start_col>\d+?)-'
34                       r'(?P<end_line>\d+?):(?P<end_col>\d+?)}:'
35                       r'"(?P<text>.*?)"$')
36
37FixIt = collections.namedtuple(
38    'FixIt', ('start_line', 'start_col', 'end_line', 'end_col', 'text'))
39
40
41def main():
42  parser = argparse.ArgumentParser()
43  parser.add_argument(
44      'build_directory',
45      nargs='?',
46      default='out/Debug',
47      help='path to the build directory to complete relative paths in fixits')
48  args = parser.parse_args()
49
50  fixits = collections.defaultdict(list)
51  for line in fileinput.input(['-']):
52    if not line.startswith('fix-it:'):
53      continue
54    m = _FIXIT_RE.match(line)
55    if not m:
56      continue
57    # The negative line numbers are a cheap hack so we can sort things in line
58    # order but reverse column order. Applying the fixits in reverse order makes
59    # things simpler, since offsets won't have to be adjusted as the text is
60    # changed.
61    fixits[m.group('file')].append(FixIt(
62        int(m.group('start_line')), -int(m.group('start_col')), int(m.group(
63            'end_line')), -int(m.group('end_col')), m.group('text')))
64  for k, v in fixits.iteritems():
65    v.sort()
66    with open(os.path.join(args.build_directory, k), 'rb+') as f:
67      lines = f.readlines()
68      last_fixit = None
69      for fixit in v:
70        if fixit.start_line != fixit.end_line:
71          print 'error: multiline fixits not supported! file: %s, fixit: %s' % (
72              k, fixit)
73          sys.exit(1)
74        if fixit == last_fixit:
75          continue
76        last_fixit = fixit
77        # The line/column numbers emitted in fixit hints start at 1, so offset
78        # is appropriately.
79        line = lines[fixit.start_line - 1]
80        lines[fixit.start_line - 1] = (line[:-fixit.start_col - 1] + fixit.text
81                                       + line[-fixit.end_col - 1:])
82      f.seek(0)
83      f.truncate()
84      f.writelines(lines)
85
86
87if __name__ == '__main__':
88  sys.exit(main())
89