#!/usr/bin/env vpython3
# Copyright 2015 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Applies fixits generated by clang. clang's -Xclang -fixit-recompile flag
# automatically applies fixits and recompiles the result, but this does not work
# well with parallel clang invocations.
#
# Usage:
# 1. Enable parseable fixits and disable warnings as errors. Instructions for
#    doing this vary based on the build environment, but for GN, warnings as
#    errors can be disabled by setting treat_warnings_as_errors = false
#    Enabling parseable fixits requires editing build/config/compiler/BUILD.gn
#    and adding `-fdiagnostics-parseable-fixits` to cflags.
# 2. Build everything and capture the output:
#      ninja -C <build_directory> &> generated-fixits
# 3. Apply the fixits with this script:
#      python apply_fixits.py -p <build_directory> < generated-fixits

from __future__ import print_function

import argparse
import codecs
import collections
import os
import re
import sys

# fix-it:"../../base/threading/sequenced_worker_pool.h":{341:3-341:11}:""
# Note that the file path is relative to the build directory.
_FIXIT_RE = re.compile(
  rb'^fix-it:"(?P<file>.+?)":'
  rb'{(?P<start_line>\d+?):(?P<start_col>\d+?)-'
  rb'(?P<end_line>\d+?):(?P<end_col>\d+?)}:'
  rb'"(?P<text>.*?)"$'
)

FixIt = collections.namedtuple(
  'FixIt', ('start_line', 'start_col', 'end_line', 'end_col', 'text')
)


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument(
    '-p',
    required=True,
    help='path to the build directory to complete relative paths in fixits',
  )
  args = parser.parse_args()

  fixits = collections.defaultdict(list)
  for line in sys.stdin.buffer:
    if not line.startswith(b'fix-it:'):
      continue
    m = _FIXIT_RE.match(line)
    if not m:
      continue
    # Machine-parseable fixits escape certain characters:
    # https://github.com/llvm/llvm-project/blob/4d676e56f0ee819a20a021434a53060a43c33120/clang/lib/Frontend/TextDiagnostic.cpp#L1629
    text, _ = codecs.escape_decode(m.group('text'))
    # The negative line numbers are a hack to sort fixits in line order but
    # reverse column order. Applying the fixits in reverse order makes things
    # simpler, since column offsets won't have to be adjusted as the text is
    # changed.
    filename = m.group('file').decode('utf-8')
    fixits[filename].append(
      FixIt(
        int(m.group('start_line')),
        -int(m.group('start_col')),
        int(m.group('end_line')),
        -int(m.group('end_col')),
        text,
      )
    )
  for k, v in fixits.items():
    v.sort()
    # Offsets in fixit hints are in bytes, so use binary mode.
    with open(os.path.join(args.p, k), mode='rb+') as f:
      lines = f.readlines()
      last_fixit = None
      line_offset = 0
      for fixit in v:
        if fixit == last_fixit:
          continue
        last_fixit = fixit

        # The line/column numbers emitted in fixit hints start at 1, so offset
        # is appropriately. Also apply unary `-` to all column numbers to
        # reverse the hack above.
        prefix = lines[fixit.start_line + line_offset - 1][
          : -fixit.start_col - 1
        ]
        suffix = lines[fixit.end_line + line_offset - 1][-fixit.end_col - 1 :]

        lines[fixit.start_line + line_offset - 1] = prefix + fixit.text + suffix

        del lines[
          fixit.start_line + line_offset + 1 - 1 : fixit.end_line
          + line_offset
          + 1
          - 1
        ]
        line_offset -= fixit.end_line - fixit.start_line

      f.seek(0)
      f.truncate()
      f.writelines(lines)


if __name__ == '__main__':
  sys.exit(main())
