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