mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
This script is used to automatically update test cases using -verify in clang's test suite. Swift also has a similar -verify test functionality, and while it can automatically fix some of them using -verify-apply-all, this functionality only updates existing checks - it doesn't add or remove any, and it doesn't handle newer and more complex things like expected-expansion. Handling that type of complexity feels out of scope to embed in the compiler, so let's copy clang's approach. This commit adds this script as is from clang. It doesn't work at all for Swift in its current form, as the output from Swift's -verify is formatted differently than in clang. This will be fixed in subsequent commits. This could have been done by adapting the script as-is in the llvm-project repository (since it only exists in the Swift fork, not upstream), but tests using swift-frontend would have to reside in the swift repo, and modifying a script in one repo with tests in a different repo sounds like a recipe for endless CI issues.
454 lines
14 KiB
Python
454 lines
14 KiB
Python
import sys
|
|
import re
|
|
|
|
DEBUG = False
|
|
|
|
|
|
def dprint(*args):
|
|
if DEBUG:
|
|
print(*args, file=sys.stderr)
|
|
|
|
|
|
class KnownException(Exception):
|
|
pass
|
|
|
|
|
|
def parse_error_category(s, prefix):
|
|
if "no expected directives found" in s:
|
|
return None
|
|
parts = s.split("diagnostics")
|
|
diag_category = parts[0]
|
|
category_parts = parts[0].strip().strip("'").split("-")
|
|
expected = category_parts[0]
|
|
if expected != prefix:
|
|
raise Exception(
|
|
f"expected prefix '{prefix}', but found '{expected}'. Multiple verify prefixes are not supported."
|
|
)
|
|
diag_category = category_parts[1]
|
|
if "seen but not expected" in parts[1]:
|
|
seen = True
|
|
elif "expected but not seen" in parts[1]:
|
|
seen = False
|
|
else:
|
|
raise KnownException(f"unexpected category '{parts[1]}'")
|
|
return (diag_category, seen)
|
|
|
|
|
|
diag_error_re = re.compile(r"File (\S+) Line (\d+): (.+)")
|
|
diag_error_re2 = re.compile(r"File \S+ Line \d+ \(directive at (\S+):(\d+)\): (.+)")
|
|
|
|
|
|
def parse_diag_error(s):
|
|
m = diag_error_re2.match(s)
|
|
if not m:
|
|
m = diag_error_re.match(s)
|
|
if not m:
|
|
return None
|
|
return (m.group(1), int(m.group(2)), m.group(3))
|
|
|
|
|
|
class Line:
|
|
def __init__(self, content, line_n):
|
|
self.content = content
|
|
self.diag = None
|
|
self.line_n = line_n
|
|
self.targeting_diags = []
|
|
|
|
def update_line_n(self, n):
|
|
self.line_n = n
|
|
|
|
def render(self):
|
|
if not self.diag:
|
|
return self.content
|
|
assert "{{DIAG}}" in self.content
|
|
res = self.content.replace("{{DIAG}}", self.diag.render())
|
|
if not res.strip():
|
|
return ""
|
|
return res
|
|
|
|
|
|
class Diag:
|
|
def __init__(
|
|
self,
|
|
prefix,
|
|
diag_content,
|
|
category,
|
|
parsed_target_line_n,
|
|
line_is_absolute,
|
|
count,
|
|
line,
|
|
is_re,
|
|
whitespace_strings,
|
|
is_from_source_file,
|
|
):
|
|
self.prefix = prefix
|
|
self.diag_content = diag_content
|
|
self.category = category
|
|
self.parsed_target_line_n = parsed_target_line_n
|
|
self.line_is_absolute = line_is_absolute
|
|
self.count = count
|
|
self.line = line
|
|
self.target = None
|
|
self.is_re = is_re
|
|
self.absolute_target()
|
|
self.whitespace_strings = whitespace_strings
|
|
self.is_from_source_file = is_from_source_file
|
|
|
|
def decrement_count(self):
|
|
self.count -= 1
|
|
assert self.count >= 0
|
|
|
|
def increment_count(self):
|
|
assert self.count >= 0
|
|
self.count += 1
|
|
|
|
def unset_target(self):
|
|
assert self.target is not None
|
|
self.target.targeting_diags.remove(self)
|
|
self.target = None
|
|
|
|
def set_target(self, target):
|
|
if self.target:
|
|
self.unset_target()
|
|
self.target = target
|
|
self.target.targeting_diags.append(self)
|
|
|
|
def absolute_target(self):
|
|
if self.target:
|
|
return self.target.line_n
|
|
if self.line_is_absolute:
|
|
return self.parsed_target_line_n
|
|
return self.line.line_n + self.parsed_target_line_n
|
|
|
|
def relative_target(self):
|
|
return self.absolute_target() - self.line.line_n
|
|
|
|
def take(self, other_diag):
|
|
assert self.count == 0
|
|
assert other_diag.count > 0
|
|
assert other_diag.target == self.target
|
|
assert not other_diag.line_is_absolute
|
|
assert not other_diag.is_re and not self.is_re
|
|
self.line_is_absolute = False
|
|
self.diag_content = other_diag.diag_content
|
|
self.count = other_diag.count
|
|
self.category = other_diag.category
|
|
self.count = other_diag.count
|
|
other_diag.count = 0
|
|
|
|
def render(self):
|
|
assert self.count >= 0
|
|
if self.count == 0:
|
|
return ""
|
|
line_location_s = ""
|
|
if self.relative_target() != 0:
|
|
if self.line_is_absolute:
|
|
line_location_s = f"@{self.absolute_target()}"
|
|
elif self.relative_target() > 0:
|
|
line_location_s = f"@+{self.relative_target()}"
|
|
else:
|
|
line_location_s = (
|
|
f"@{self.relative_target()}" # the minus sign is implicit
|
|
)
|
|
count_s = "" if self.count == 1 else f"{self.count}"
|
|
re_s = "-re" if self.is_re else ""
|
|
if self.whitespace_strings:
|
|
whitespace1_s = self.whitespace_strings[0]
|
|
whitespace2_s = self.whitespace_strings[1]
|
|
whitespace3_s = self.whitespace_strings[2]
|
|
else:
|
|
whitespace1_s = " "
|
|
whitespace2_s = ""
|
|
whitespace3_s = ""
|
|
if count_s and not whitespace2_s:
|
|
whitespace2_s = " " # required to parse correctly
|
|
elif not count_s and whitespace2_s == " ":
|
|
"""Don't emit a weird extra space.
|
|
However if the whitespace is something other than the
|
|
standard single space, let it be to avoid disrupting manual formatting.
|
|
The existence of a non-empty whitespace2_s implies this was parsed with
|
|
a count > 1 and then decremented, otherwise this whitespace would have
|
|
been parsed as whitespace3_s.
|
|
"""
|
|
whitespace2_s = ""
|
|
return f"//{whitespace1_s}{self.prefix}-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}"
|
|
|
|
|
|
expected_diag_re = re.compile(
|
|
r"//(\s*)([a-zA-Z]+)-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}"
|
|
)
|
|
|
|
|
|
def parse_diag(line, filename, lines, prefix):
|
|
s = line.content
|
|
ms = expected_diag_re.findall(s)
|
|
if not ms:
|
|
return None
|
|
if len(ms) > 1:
|
|
raise KnownException(
|
|
f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation."
|
|
)
|
|
[
|
|
whitespace1_s,
|
|
check_prefix,
|
|
category_s,
|
|
re_s,
|
|
target_line_s,
|
|
whitespace2_s,
|
|
count_s,
|
|
whitespace3_s,
|
|
diag_s,
|
|
] = ms[0]
|
|
if check_prefix != prefix:
|
|
return None
|
|
if not target_line_s:
|
|
target_line_n = 0
|
|
is_absolute = False
|
|
elif target_line_s.startswith("@+"):
|
|
target_line_n = int(target_line_s[2:])
|
|
is_absolute = False
|
|
elif target_line_s.startswith("@-"):
|
|
target_line_n = int(target_line_s[1:])
|
|
is_absolute = False
|
|
else:
|
|
target_line_n = int(target_line_s[1:])
|
|
is_absolute = True
|
|
count = int(count_s) if count_s else 1
|
|
line.content = expected_diag_re.sub("{{DIAG}}", s)
|
|
|
|
return Diag(
|
|
prefix,
|
|
diag_s,
|
|
category_s,
|
|
target_line_n,
|
|
is_absolute,
|
|
count,
|
|
line,
|
|
bool(re_s),
|
|
[whitespace1_s, whitespace2_s, whitespace3_s],
|
|
True,
|
|
)
|
|
|
|
|
|
def add_line(new_line, lines):
|
|
lines.insert(new_line.line_n - 1, new_line)
|
|
for i in range(new_line.line_n, len(lines)):
|
|
line = lines[i]
|
|
assert line.line_n == i
|
|
line.update_line_n(i + 1)
|
|
assert all(line.line_n == i + 1 for i, line in enumerate(lines))
|
|
|
|
|
|
def remove_line(old_line, lines):
|
|
lines.remove(old_line)
|
|
for i in range(old_line.line_n - 1, len(lines)):
|
|
line = lines[i]
|
|
assert line.line_n == i + 2
|
|
line.update_line_n(i + 1)
|
|
assert all(line.line_n == i + 1 for i, line in enumerate(lines))
|
|
|
|
|
|
indent_re = re.compile(r"\s*")
|
|
|
|
|
|
def get_indent(s):
|
|
return indent_re.match(s).group(0)
|
|
|
|
|
|
def orig_line_n_to_new_line_n(line_n, orig_lines):
|
|
return orig_lines[line_n - 1].line_n
|
|
|
|
|
|
def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines, prefix):
|
|
line_n = orig_line_n_to_new_line_n(orig_line_n, orig_lines)
|
|
target = lines[line_n - 1]
|
|
for other in target.targeting_diags:
|
|
if other.is_re:
|
|
raise KnownException(
|
|
"mismatching diag on line with regex matcher. Skipping due to missing implementation"
|
|
)
|
|
reverse = (
|
|
True
|
|
if [other for other in target.targeting_diags if other.relative_target() < 0]
|
|
else False
|
|
)
|
|
|
|
targeting = [
|
|
other for other in target.targeting_diags if not other.line_is_absolute
|
|
]
|
|
targeting.sort(reverse=reverse, key=lambda d: d.relative_target())
|
|
prev_offset = 0
|
|
prev_line = target
|
|
direction = -1 if reverse else 1
|
|
for d in targeting:
|
|
if d.relative_target() != prev_offset + direction:
|
|
break
|
|
prev_offset = d.relative_target()
|
|
prev_line = d.line
|
|
total_offset = prev_offset - 1 if reverse else prev_offset + 1
|
|
if reverse:
|
|
new_line_n = prev_line.line_n + 1
|
|
else:
|
|
new_line_n = prev_line.line_n
|
|
assert new_line_n == line_n + (not reverse) - total_offset
|
|
|
|
new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n)
|
|
add_line(new_line, lines)
|
|
|
|
whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None
|
|
new_diag = Diag(
|
|
prefix,
|
|
diag_s,
|
|
diag_category,
|
|
total_offset,
|
|
False,
|
|
1,
|
|
new_line,
|
|
False,
|
|
whitespace_strings,
|
|
False,
|
|
)
|
|
new_line.diag = new_diag
|
|
new_diag.set_target(target)
|
|
|
|
|
|
def remove_dead_diags(lines):
|
|
for line in lines:
|
|
if not line.diag or line.diag.count != 0:
|
|
continue
|
|
if line.render() == "":
|
|
remove_line(line, lines)
|
|
else:
|
|
assert line.diag.is_from_source_file
|
|
for other_diag in line.targeting_diags:
|
|
if (
|
|
other_diag.is_from_source_file
|
|
or other_diag.count == 0
|
|
or other_diag.category != line.diag.category
|
|
):
|
|
continue
|
|
if other_diag.is_re or line.diag.is_re:
|
|
continue
|
|
line.diag.take(other_diag)
|
|
remove_line(other_diag.line, lines)
|
|
|
|
|
|
def has_live_diags(lines):
|
|
for line in lines:
|
|
if line.diag and line.diag.count > 0:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_expected_no_diags_line_n(lines, prefix):
|
|
for line in lines:
|
|
if f"{prefix}-no-diagnostics" in line.content:
|
|
return line.line_n
|
|
return None
|
|
|
|
|
|
def update_test_file(filename, diag_errors, prefix, updated_test_files):
|
|
dprint(f"updating test file {filename}")
|
|
if filename in updated_test_files:
|
|
raise KnownException(f"{filename} already updated, but got new output")
|
|
else:
|
|
updated_test_files.add(filename)
|
|
with open(filename, "r") as f:
|
|
lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())]
|
|
orig_lines = list(lines)
|
|
expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines, prefix)
|
|
|
|
for line in lines:
|
|
diag = parse_diag(line, filename, lines, prefix)
|
|
if diag:
|
|
line.diag = diag
|
|
diag.set_target(lines[diag.absolute_target() - 1])
|
|
|
|
for line_n, diag_s, diag_category, seen in diag_errors:
|
|
if seen:
|
|
continue
|
|
# this is a diagnostic expected but not seen
|
|
assert lines[line_n - 1].diag
|
|
if diag_s != lines[line_n - 1].diag.diag_content:
|
|
raise KnownException(
|
|
f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_s}"
|
|
)
|
|
if diag_category != lines[line_n - 1].diag.category:
|
|
raise KnownException(
|
|
f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}"
|
|
)
|
|
lines[line_n - 1].diag.decrement_count()
|
|
diag_errors_left = []
|
|
diag_errors.sort(reverse=True, key=lambda t: t[0])
|
|
for line_n, diag_s, diag_category, seen in diag_errors:
|
|
if not seen:
|
|
continue
|
|
target = orig_lines[line_n - 1]
|
|
other_diags = [
|
|
d
|
|
for d in target.targeting_diags
|
|
if d.diag_content == diag_s and d.category == diag_category
|
|
]
|
|
other_diag = other_diags[0] if other_diags else None
|
|
if other_diag:
|
|
other_diag.increment_count()
|
|
else:
|
|
add_diag(line_n, diag_s, diag_category, lines, orig_lines, prefix)
|
|
remove_dead_diags(lines)
|
|
has_diags = has_live_diags(lines)
|
|
with open(filename, "w") as f:
|
|
if not has_diags and expected_no_diags_line_n is None:
|
|
f.write("// expected-no-diagnostics\n")
|
|
for line in lines:
|
|
if has_diags and line.line_n == expected_no_diags_line_n:
|
|
continue
|
|
f.write(line.render())
|
|
|
|
|
|
def update_test_files(errors, prefix):
|
|
errors_by_file = {}
|
|
for (filename, line, diag_s), (diag_category, seen) in errors:
|
|
if filename not in errors_by_file:
|
|
errors_by_file[filename] = []
|
|
errors_by_file[filename].append((line, diag_s, diag_category, seen))
|
|
updated_test_files = set()
|
|
for filename, diag_errors in errors_by_file.items():
|
|
try:
|
|
update_test_file(filename, diag_errors, prefix, updated_test_files)
|
|
except KnownException as e:
|
|
return f"Error in update-verify-tests while updating {filename}: {e}"
|
|
updated_files = list(updated_test_files)
|
|
assert updated_files
|
|
if len(updated_files) == 1:
|
|
return f"updated file {updated_files[0]}"
|
|
updated_files_s = "\n\t".join(updated_files)
|
|
return "updated files:\n\t{updated_files_s}"
|
|
|
|
|
|
def check_expectations(tool_output, prefix):
|
|
"""
|
|
The entry point function.
|
|
Called by the stand-alone update-verify-tests.py as well as litplugin.py.
|
|
"""
|
|
curr = []
|
|
curr_category = None
|
|
try:
|
|
for line in tool_output:
|
|
if line.startswith("error: "):
|
|
curr_category = parse_error_category(line[len("error: ") :], prefix)
|
|
continue
|
|
|
|
diag_error = parse_diag_error(line.strip())
|
|
if diag_error:
|
|
curr.append((diag_error, curr_category))
|
|
else:
|
|
dprint("no match")
|
|
dprint(line.strip())
|
|
except KnownException as e:
|
|
return f"Error in update-verify-tests while parsing tool output: {e}"
|
|
if curr:
|
|
return update_test_files(curr, prefix)
|
|
else:
|
|
return "no mismatching diagnostics found"
|
|
|