Files
RedBear-OS/local/recipes/dev/libclc/source/llvm/utils/relative_lines.py
T
vasilito cb424d7448 build: static patch-sanity linter (shift-left the malformed-patch class)
verify-patch-sanity.py validates every active recipe .patch has internally-
consistent hunk line counts — catching the 'malformed patch at line N' failure
at commit/CI/preflight time instead of hours into a cook. This cycle hit that
class three times (qtwaylandscanner, sddm, xwayland), each only discovered when
cookbook tried to apply the patch.

Running it across the repo found 29 latent malformed patches (validated against
GNU patch: e.g. relibc/P3-sysv-ipc reproduces 'malformed patch at line 22').
They were harmless only because they sit in vendored recipes (baked, not re-
applied) — but would fail on any version-bump re-derivation. --fix recounts the
hunk headers (body untouched) and repaired all 29.

Wired into build-preflight.sh (Phase 1.0D) and redbear-ci.yml, with a unit test
(test-patch-sanity.sh). Skips archived/legacy trees and unvalidatable formats
(empty placeholders, bare-@@ git hunks).
2026-08-01 05:13:02 +03:00

129 lines
4.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Replaces absolute line numbers in lit-tests with relative line numbers.
Writing line numbers like 152 in 'RUN: or CHECK:' makes tests hard to maintain:
inserting lines in the middle of the test means updating all the line numbers.
Encoding them relative to the current line helps, and tools support it:
Lit will substitute %(line+2) with the actual line number
FileCheck supports [[@LINE+2]]
This tool takes a regex which captures a line number, and a list of test files.
It searches for line numbers in the files and replaces them with a relative
line number reference.
"""
USAGE = """Example usage:
find -type f clang/test/CodeCompletion | grep -v /Inputs/ | \\
xargs relative_lines.py --dry-run --verbose --near=100 \\
--pattern='-code-completion-at[ =]%s:(\d+)' \\
--pattern='requires fix-it: {(\d+):\d+-(\d+):\d+}'
"""
import argparse
import re
import sys
def b(x):
return bytes(x, encoding="utf-8")
parser = argparse.ArgumentParser(
prog="relative_lines",
description=__doc__,
epilog=USAGE,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--near", type=int, default=20, help="maximum line distance to make relative"
)
parser.add_argument(
"--partial",
action="store_true",
default=False,
help="apply replacements to files even if others failed",
)
parser.add_argument(
"--pattern",
default=[],
action="append",
type=lambda x: re.compile(b(x)),
help="regex to match, with line numbers captured in ().",
)
parser.add_argument(
"--verbose", action="store_true", default=False, help="print matches applied"
)
parser.add_argument(
"--dry-run",
action="store_true",
default=False,
help="don't apply replacements. Best with --verbose.",
)
parser.add_argument("files", nargs="+")
args = parser.parse_args()
for file in args.files:
try:
contents = open(file, "rb").read()
except UnicodeDecodeError as e:
print(f"{file}: not valid UTF-8 - {e}", file=sys.stderr)
failures = 0
def line_number(offset):
return 1 + contents[:offset].count(b"\n")
def replace_one(capture, line, offset):
"""Text to replace a capture group, e.g. 42 => %(line+1)"""
try:
target = int(capture)
except ValueError:
print(f"{file}:{line}: matched non-number '{capture}'", file=sys.stderr)
return capture
if args.near > 0 and abs(target - line) > args.near:
print(
f"{file}:{line}: target line {target} is farther than {args.near}",
file=sys.stderr,
)
return capture
if target > line:
delta = "+" + str(target - line)
elif target < line:
delta = "-" + str(line - target)
else:
delta = ""
prefix = contents[:offset].rsplit(b"\n")[-1]
is_lit = b"RUN" in prefix or b"DEFINE" in prefix
text = ("%(line{0})" if is_lit else "[[@LINE{0}]]").format(delta)
if args.verbose:
print(f"{file}:{line}: {0} ==> {text}")
return b(text)
def replace_match(m):
"""Text to replace a whole match, e.g. --at=42:3 => --at=%(line+2):3"""
line = 1 + contents[: m.start()].count(b"\n")
result = b""
pos = m.start()
for index, capture in enumerate(m.groups()):
index += 1 # re groups are conventionally 1-indexed
result += contents[pos : m.start(index)]
replacement = replace_one(capture, line, m.start(index))
result += replacement
if replacement == capture:
global failures
failures += 1
pos = m.end(index)
result += contents[pos : m.end()]
return result
for pattern in args.pattern:
contents = re.sub(pattern, replace_match, contents)
if failures > 0 and not args.partial:
print(f"{file}: leaving unchanged (some failed, --partial not given)")
continue
if not args.dry_run:
open(file, "wb").write(contents)