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 28 29def b(x): 30 return bytes(x, encoding="utf-8") 31 32 33parser = argparse.ArgumentParser( 34 prog="relative_lines", 35 description=__doc__, 36 epilog=USAGE, 37 formatter_class=argparse.RawTextHelpFormatter, 38) 39parser.add_argument( 40 "--near", type=int, default=20, help="maximum line distance to make relative" 41) 42parser.add_argument( 43 "--partial", 44 action="store_true", 45 default=False, 46 help="apply replacements to files even if others failed", 47) 48parser.add_argument( 49 "--pattern", 50 default=[], 51 action="append", 52 type=lambda x: re.compile(b(x)), 53 help="regex to match, with line numbers captured in ().", 54) 55parser.add_argument( 56 "--verbose", action="store_true", default=False, help="print matches applied" 57) 58parser.add_argument( 59 "--dry-run", 60 action="store_true", 61 default=False, 62 help="don't apply replacements. Best with --verbose.", 63) 64parser.add_argument("files", nargs="+") 65args = parser.parse_args() 66 67for file in args.files: 68 try: 69 contents = open(file, "rb").read() 70 except UnicodeDecodeError as e: 71 print(f"{file}: not valid UTF-8 - {e}", file=sys.stderr) 72 failures = 0 73 74 def line_number(offset): 75 return 1 + contents[:offset].count(b"\n") 76 77 def replace_one(capture, line, offset): 78 """Text to replace a capture group, e.g. 42 => %(line+1)""" 79 try: 80 target = int(capture) 81 except ValueError: 82 print(f"{file}:{line}: matched non-number '{capture}'", file=sys.stderr) 83 return capture 84 85 if args.near > 0 and abs(target - line) > args.near: 86 print( 87 f"{file}:{line}: target line {target} is farther than {args.near}", 88 file=sys.stderr, 89 ) 90 return capture 91 if target > line: 92 delta = "+" + str(target - line) 93 elif target < line: 94 delta = "-" + str(line - target) 95 else: 96 delta = "" 97 98 prefix = contents[:offset].rsplit(b"\n")[-1] 99 is_lit = b"RUN" in prefix or b"DEFINE" in prefix 100 text = ("%(line{0})" if is_lit else "[[@LINE{0}]]").format(delta) 101 if args.verbose: 102 print(f"{file}:{line}: {0} ==> {text}") 103 return b(text) 104 105 def replace_match(m): 106 """Text to replace a whole match, e.g. --at=42:3 => --at=%(line+2):3""" 107 line = 1 + contents[: m.start()].count(b"\n") 108 result = b"" 109 pos = m.start() 110 for index, capture in enumerate(m.groups()): 111 index += 1 # re groups are conventionally 1-indexed 112 result += contents[pos : m.start(index)] 113 replacement = replace_one(capture, line, m.start(index)) 114 result += replacement 115 if replacement == capture: 116 global failures 117 failures += 1 118 pos = m.end(index) 119 result += contents[pos : m.end()] 120 return result 121 122 for pattern in args.pattern: 123 contents = re.sub(pattern, replace_match, contents) 124 if failures > 0 and not args.partial: 125 print(f"{file}: leaving unchanged (some failed, --partial not given)") 126 continue 127 if not args.dry_run: 128 open(file, "wb").write(contents) 129