xref: /netbsd-src/external/apache2/llvm/dist/clang/tools/clang-format/clang-format-diff.py (revision e038c9c4676b0f19b1b7dd08a940c6ed64a6d5ae)
1*e038c9c4Sjoerg#!/usr/bin/env python3
27330f729Sjoerg#
37330f729Sjoerg#===- clang-format-diff.py - ClangFormat Diff Reformatter ----*- python -*--===#
47330f729Sjoerg#
57330f729Sjoerg# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
67330f729Sjoerg# See https://llvm.org/LICENSE.txt for license information.
77330f729Sjoerg# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
87330f729Sjoerg#
97330f729Sjoerg#===------------------------------------------------------------------------===#
107330f729Sjoerg
117330f729Sjoerg"""
127330f729SjoergThis script reads input from a unified diff and reformats all the changed
137330f729Sjoerglines. This is useful to reformat all the lines touched by a specific patch.
147330f729SjoergExample usage for git/svn users:
157330f729Sjoerg
16*e038c9c4Sjoerg  git diff -U0 --no-color --relative HEAD^ | clang-format-diff.py -p1 -i
177330f729Sjoerg  svn diff --diff-cmd=diff -x-U0 | clang-format-diff.py -i
187330f729Sjoerg
19*e038c9c4SjoergIt should be noted that the filename contained in the diff is used unmodified
20*e038c9c4Sjoergto determine the source file to update. Users calling this script directly
21*e038c9c4Sjoergshould be careful to ensure that the path in the diff is correct relative to the
22*e038c9c4Sjoergcurrent working directory.
237330f729Sjoerg"""
247330f729Sjoergfrom __future__ import absolute_import, division, print_function
257330f729Sjoerg
267330f729Sjoergimport argparse
277330f729Sjoergimport difflib
287330f729Sjoergimport re
297330f729Sjoergimport subprocess
307330f729Sjoergimport sys
317330f729Sjoerg
327330f729Sjoergif sys.version_info.major >= 3:
337330f729Sjoerg    from io import StringIO
347330f729Sjoergelse:
357330f729Sjoerg    from io import BytesIO as StringIO
367330f729Sjoerg
377330f729Sjoerg
387330f729Sjoergdef main():
397330f729Sjoerg  parser = argparse.ArgumentParser(description=__doc__,
407330f729Sjoerg                                   formatter_class=
417330f729Sjoerg                                           argparse.RawDescriptionHelpFormatter)
427330f729Sjoerg  parser.add_argument('-i', action='store_true', default=False,
437330f729Sjoerg                      help='apply edits to files instead of displaying a diff')
447330f729Sjoerg  parser.add_argument('-p', metavar='NUM', default=0,
457330f729Sjoerg                      help='strip the smallest prefix containing P slashes')
467330f729Sjoerg  parser.add_argument('-regex', metavar='PATTERN', default=None,
477330f729Sjoerg                      help='custom pattern selecting file paths to reformat '
487330f729Sjoerg                      '(case sensitive, overrides -iregex)')
497330f729Sjoerg  parser.add_argument('-iregex', metavar='PATTERN', default=
50*e038c9c4Sjoerg                      r'.*\.(cpp|cc|c\+\+|cxx|c|cl|h|hh|hpp|hxx|m|mm|inc|js|ts'
51*e038c9c4Sjoerg                      r'|proto|protodevel|java|cs)',
527330f729Sjoerg                      help='custom pattern selecting file paths to reformat '
537330f729Sjoerg                      '(case insensitive, overridden by -regex)')
547330f729Sjoerg  parser.add_argument('-sort-includes', action='store_true', default=False,
557330f729Sjoerg                      help='let clang-format sort include blocks')
567330f729Sjoerg  parser.add_argument('-v', '--verbose', action='store_true',
577330f729Sjoerg                      help='be more verbose, ineffective without -i')
587330f729Sjoerg  parser.add_argument('-style',
59*e038c9c4Sjoerg                      help='formatting style to apply (LLVM, GNU, Google, Chromium, '
60*e038c9c4Sjoerg                      'Microsoft, Mozilla, WebKit)')
617330f729Sjoerg  parser.add_argument('-binary', default='clang-format',
627330f729Sjoerg                      help='location of binary to use for clang-format')
637330f729Sjoerg  args = parser.parse_args()
647330f729Sjoerg
657330f729Sjoerg  # Extract changed lines for each file.
667330f729Sjoerg  filename = None
677330f729Sjoerg  lines_by_file = {}
687330f729Sjoerg  for line in sys.stdin:
697330f729Sjoerg    match = re.search(r'^\+\+\+\ (.*?/){%s}(\S*)' % args.p, line)
707330f729Sjoerg    if match:
717330f729Sjoerg      filename = match.group(2)
72*e038c9c4Sjoerg    if filename is None:
737330f729Sjoerg      continue
747330f729Sjoerg
757330f729Sjoerg    if args.regex is not None:
767330f729Sjoerg      if not re.match('^%s$' % args.regex, filename):
777330f729Sjoerg        continue
787330f729Sjoerg    else:
797330f729Sjoerg      if not re.match('^%s$' % args.iregex, filename, re.IGNORECASE):
807330f729Sjoerg        continue
817330f729Sjoerg
827330f729Sjoerg    match = re.search(r'^@@.*\+(\d+)(,(\d+))?', line)
837330f729Sjoerg    if match:
847330f729Sjoerg      start_line = int(match.group(1))
857330f729Sjoerg      line_count = 1
867330f729Sjoerg      if match.group(3):
877330f729Sjoerg        line_count = int(match.group(3))
887330f729Sjoerg      if line_count == 0:
897330f729Sjoerg        continue
907330f729Sjoerg      end_line = start_line + line_count - 1
917330f729Sjoerg      lines_by_file.setdefault(filename, []).extend(
927330f729Sjoerg          ['-lines', str(start_line) + ':' + str(end_line)])
937330f729Sjoerg
947330f729Sjoerg  # Reformat files containing changes in place.
957330f729Sjoerg  for filename, lines in lines_by_file.items():
967330f729Sjoerg    if args.i and args.verbose:
977330f729Sjoerg      print('Formatting {}'.format(filename))
987330f729Sjoerg    command = [args.binary, filename]
997330f729Sjoerg    if args.i:
1007330f729Sjoerg      command.append('-i')
1017330f729Sjoerg    if args.sort_includes:
1027330f729Sjoerg      command.append('-sort-includes')
1037330f729Sjoerg    command.extend(lines)
1047330f729Sjoerg    if args.style:
1057330f729Sjoerg      command.extend(['-style', args.style])
106*e038c9c4Sjoerg
107*e038c9c4Sjoerg    try:
1087330f729Sjoerg      p = subprocess.Popen(command,
1097330f729Sjoerg                           stdout=subprocess.PIPE,
1107330f729Sjoerg                           stderr=None,
1117330f729Sjoerg                           stdin=subprocess.PIPE,
1127330f729Sjoerg                           universal_newlines=True)
113*e038c9c4Sjoerg    except OSError as e:
114*e038c9c4Sjoerg      # Give the user more context when clang-format isn't
115*e038c9c4Sjoerg      # found/isn't executable, etc.
116*e038c9c4Sjoerg      raise RuntimeError(
117*e038c9c4Sjoerg        'Failed to run "%s" - %s"' % (" ".join(command), e.strerror))
118*e038c9c4Sjoerg
1197330f729Sjoerg    stdout, stderr = p.communicate()
1207330f729Sjoerg    if p.returncode != 0:
1217330f729Sjoerg      sys.exit(p.returncode)
1227330f729Sjoerg
1237330f729Sjoerg    if not args.i:
1247330f729Sjoerg      with open(filename) as f:
1257330f729Sjoerg        code = f.readlines()
1267330f729Sjoerg      formatted_code = StringIO(stdout).readlines()
1277330f729Sjoerg      diff = difflib.unified_diff(code, formatted_code,
1287330f729Sjoerg                                  filename, filename,
1297330f729Sjoerg                                  '(before formatting)', '(after formatting)')
1307330f729Sjoerg      diff_string = ''.join(diff)
1317330f729Sjoerg      if len(diff_string) > 0:
1327330f729Sjoerg        sys.stdout.write(diff_string)
1337330f729Sjoerg
1347330f729Sjoergif __name__ == '__main__':
1357330f729Sjoerg  main()
136