3 """Replaces absolute line numbers in lit-tests with relative line numbers.
5 Writing line numbers like 152 in 'RUN: or CHECK:' makes tests hard to maintain:
6 inserting lines in the middle of the test means updating all the line numbers.
8 Encoding 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]]
12 This tool takes a regex which captures a line number, and a list of test files.
13 It searches for line numbers in the files and replaces them with a relative
14 line number reference.
17 USAGE
= """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+}'
30 return bytes(x
, encoding
="utf-8")
33 parser
= argparse
.ArgumentParser(
34 prog
="relative_lines",
37 formatter_class
=argparse
.RawTextHelpFormatter
,
40 "--near", type=int, default
=20, help="maximum line distance to make relative"
46 help="apply replacements to files even if others failed",
52 type=lambda x
: re
.compile(b(x
)),
53 help="regex to match, with line numbers captured in ().",
56 "--verbose", action
="store_true", default
=False, help="print matches applied"
62 help="don't apply replacements. Best with --verbose.",
64 parser
.add_argument("files", nargs
="+")
65 args
= parser
.parse_args()
67 for file in args
.files
:
69 contents
= open(file, "rb").read()
70 except UnicodeDecodeError as e
:
71 print(f
"{file}: not valid UTF-8 - {e}", file=sys
.stderr
)
74 def line_number(offset
):
75 return 1 + contents
[:offset
].count(b
"\n")
77 def replace_one(capture
, line
, offset
):
78 """Text to replace a capture group, e.g. 42 => %(line+1)"""
82 print(f
"{file}:{line}: matched non-number '{capture}'", file=sys
.stderr
)
85 if args
.near
> 0 and abs(target
- line
) > args
.near
:
87 f
"{file}:{line}: target line {target} is farther than {args.near}",
92 delta
= "+" + str(target
- line
)
94 delta
= "-" + str(line
- target
)
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
)
102 print(f
"{file}:{line}: {0} ==> {text}")
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")
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
:
119 result
+= contents
[pos
: m
.end()]
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)")
128 open(file, "wb").write(contents
)