1#!/usr/bin/env python3 2 3"""Replaces absolute line numbers in lit-tests with relative line numbers. 4 5Writing line numbers like 152 in 'RUN: or CHECK:' makes tests hard to maintain: 6inserting lines in the middle of the test means updating all the line numbers. 7 8Encoding them relative to the current line helps, and tools support it: 9 Lit will substitute %(line+2) with the actual line number 10 FileCheck supports [[@LINE+2]] 11 12This tool takes a regex which captures a line number, and a list of test files. 13It searches for line numbers in the files and replaces them with a relative 14line number reference. 15""" 16 17USAGE = """Example usage: 18 find -type f clang/test/CodeCompletion | grep -v /Inputs/ | \\ 19 xargs relative_lines.py --dry-run --verbose --near=100 \\ 20 --pattern='-code-completion-at[ =]%s:(\d+)' \\ 21 --pattern='requires fix-it: {(\d+):\d+-(\d+):\d+}' 22""" 23 24import argparse 25import re 26import sys 27 28def b(x): 29 return bytes(x, encoding='utf-8') 30 31parser = argparse.ArgumentParser(prog = 'relative_lines', 32 description = __doc__, 33 epilog = USAGE, 34 formatter_class=argparse.RawTextHelpFormatter) 35parser.add_argument('--near', type=int, default=20, 36 help = "maximum line distance to make relative") 37parser.add_argument('--partial', action='store_true', default=False, 38 help = "apply replacements to files even if others failed") 39parser.add_argument('--pattern', default=[], action='append', 40 type=lambda x: re.compile(b(x)), 41 help = "regex to match, with line numbers captured in ().") 42parser.add_argument('--verbose', action='store_true', default=False, 43 help = "print matches applied") 44parser.add_argument('--dry-run', action='store_true', default=False, 45 help = "don't apply replacements. Best with --verbose.") 46parser.add_argument('files', nargs = '+') 47args = parser.parse_args() 48 49for file in args.files: 50 try: 51 contents = open(file, 'rb').read() 52 except UnicodeDecodeError as e: 53 print(f"{file}: not valid UTF-8 - {e}", file=sys.stderr) 54 failures = 0 55 56 def line_number(offset): 57 return 1 + contents[:offset].count(b'\n') 58 59 def replace_one(capture, line, offset): 60 """Text to replace a capture group, e.g. 42 => %(line+1)""" 61 try: 62 target = int(capture) 63 except ValueError: 64 print(f"{file}:{line}: matched non-number '{capture}'", file=sys.stderr) 65 return capture 66 67 if args.near > 0 and abs(target - line) > args.near: 68 print(f"{file}:{line}: target line {target} is farther than {args.near}", file=sys.stderr) 69 return capture 70 if target > line: 71 delta = '+' + str(target - line) 72 elif target < line: 73 delta = '-' + str(line - target) 74 else: 75 delta = '' 76 77 prefix = contents[:offset].rsplit(b'\n')[-1] 78 is_lit = b'RUN' in prefix or b'DEFINE' in prefix 79 text = ('%(line{0})' if is_lit else '[[@LINE{0}]]').format(delta) 80 if args.verbose: 81 print(f"{file}:{line}: {0} ==> {text}") 82 return b(text) 83 84 def replace_match(m): 85 """Text to replace a whole match, e.g. --at=42:3 => --at=%(line+2):3""" 86 line = 1 + contents[:m.start()].count(b'\n') 87 result = b'' 88 pos = m.start() 89 for index, capture in enumerate(m.groups()): 90 index += 1 # re groups are conventionally 1-indexed 91 result += contents[pos:m.start(index)] 92 replacement = replace_one(capture, line, m.start(index)) 93 result += replacement 94 if replacement == capture: 95 global failures 96 failures += 1 97 pos = m.end(index) 98 result += contents[pos:m.end()] 99 return result 100 101 for pattern in args.pattern: 102 contents = re.sub(pattern, replace_match, contents) 103 if failures > 0 and not args.partial: 104 print(f"{file}: leaving unchanged (some failed, --partial not given)") 105 continue 106 if not args.dry_run: 107 open(file, "wb").write(contents) 108