https://github.com/hnrklssn updated https://github.com/llvm/llvm-project/pull/97369
>From d93d77e193f235d12d4de4a4b184c458508fa8df Mon Sep 17 00:00:00 2001 From: "Henrik G. Olsson" <h_ols...@apple.com> Date: Mon, 1 Jul 2024 18:19:09 -0700 Subject: [PATCH 1/4] [Utils] add update-verify-tests.py Adds a python script to automatically take output from a failed clang -verify test and update the test case(s) to expect the new behaviour. --- clang/utils/update-verify-tests.py | 404 +++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 clang/utils/update-verify-tests.py diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py new file mode 100644 index 00000000000000..cfcfefc85e576a --- /dev/null +++ b/clang/utils/update-verify-tests.py @@ -0,0 +1,404 @@ +import sys +import re + +""" + Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output. + When inserting new expected-* checks it will place them on the line before the location of the diagnostic, with an @+1, + or @+N for some N if there are multiple diagnostics emitted on the same line. If the current checks are using @-N for + this line, the new check will follow that convention also. + Existing checks will be left untouched as much as possible, including their location and whitespace content, to minimize + diffs. If inaccurate their count will be updated, or the check removed entirely. + + Missing features: + - custom prefix support (-verify=my-prefix) + - multiple prefixes on the same line (-verify=my-prefix,my-other-prefix) + - multiple prefixes on separate RUN lines (RUN: -verify=my-prefix\nRUN: -verify my-other-prefix) + - regexes with expected-*-re: existing ones will be left untouched if accurate, but the script will abort if there are any + diagnostic mismatches on the same line. + - multiple checks targeting the same line are supported, but a line may only contain one check + - if multiple checks targeting the same line are failing the script is not guaranteed to produce a minimal diff + +Example usage: + build/bin/llvm-lit clang/test/Sema/ --no-progress-bar -v | python3 update-verify-tests.py +""" + + +class KnownException(Exception): + pass + + +def parse_error_category(s): + parts = s.split("diagnostics") + diag_category = parts[0] + category_parts = parts[0].strip().strip("'").split("-") + expected = category_parts[0] + if expected != "expected": + raise Exception( + f"expected 'expected', but found '{expected}'. Custom 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.related_diags = [] + self.targeting_diags = [] + + def update_line_n(self, n): + if self.diag and not self.diag.line_is_absolute: + self.diag.orig_target_line_n += n - self.line_n + self.line_n = n + for diag in self.targeting_diags: + if diag.line_is_absolute: + diag.orig_target_line_n = n + else: + diag.orig_target_line_n = n - diag.line.line_n + for diag in self.related_diags: + if not diag.line_is_absolute: + pass + + 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, + diag_content, + category, + targeted_line_n, + line_is_absolute, + count, + line, + is_re, + whitespace_strings, + ): + self.diag_content = diag_content + self.category = category + self.orig_target_line_n = targeted_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 + + def add(self): + if targeted_line > 0: + targeted_line += 1 + elif targeted_line < 0: + targeted_line -= 1 + + def absolute_target(self): + if self.line_is_absolute: + res = self.orig_target_line_n + else: + res = self.line.line_n + self.orig_target_line_n + if self.target: + assert self.line.line_n == res + return res + + def relative_target(self): + return self.absolute_target() - self.line.line_n + + 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] + whitespace4_s = self.whitespace_strings[3] + else: + whitespace1_s = " " + whitespace2_s = "" + whitespace3_s = "" + whitespace4_s = "" + if count_s and not whitespace3_s: + whitespace3_s = " " + return f"//{whitespace1_s}expected-{self.category}{re_s}{whitespace2_s}{line_location_s}{whitespace3_s}{count_s}{whitespace4_s}{{{{{self.diag_content}}}}}" + + +expected_diag_re = re.compile( + r"//(\s*)expected-(note|warning|error)(-re)?(\s*)(@[+-]?\d+)?(\s*)(\d+)?(\s*)\{\{(.*)\}\}" +) + + +def parse_diag(line, filename, lines): + s = line.content + ms = expected_diag_re.findall(s) + if not ms: + return None + if len(ms) > 1: + print( + f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation." + ) + sys.exit(1) + [ + whitespace1_s, + category_s, + re_s, + whitespace2_s, + target_line_s, + whitespace3_s, + count_s, + whitespace4_s, + diag_s, + ] = ms[0] + 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( + diag_s, + category_s, + target_line_n, + is_absolute, + count, + line, + bool(re_s), + [whitespace1_s, whitespace2_s, whitespace3_s, whitespace4_s], + ) + + +def link_line_diags(lines, diag): + line_n = diag.line.line_n + target_line_n = diag.absolute_target() + step = 1 if target_line_n < line_n else -1 + for i in range(target_line_n, line_n, step): + lines[i - 1].related_diags.append(diag) + + +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)) + + +indent_re = re.compile(r"\s*") + + +def get_indent(s): + return indent_re.match(s).group(0) + + +def add_diag(line_n, diag_s, diag_category, 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) + new_line.related_diags = list(prev_line.related_diags) + add_line(new_line, lines) + + new_diag = Diag( + diag_s, diag_category, total_offset, False, 1, new_line, False, None + ) + new_line.diag = new_diag + new_diag.target_line = target + assert type(new_diag) != str + target.targeting_diags.append(new_diag) + link_line_diags(lines, new_diag) + + +updated_test_files = set() + + +def update_test_file(filename, diag_errors): + print(f"updating test file {filename}") + if filename in updated_test_files: + print( + f"{filename} already updated, but got new output - expect incorrect results" + ) + else: + updated_test_files.add(filename) + with open(filename, "r") as f: + lines = [Line(line, i + 1) for i, line in enumerate(f.readlines())] + for line in lines: + diag = parse_diag(line, filename, lines) + if diag: + line.diag = diag + diag.target_line = lines[diag.absolute_target() - 1] + link_line_diags(lines, diag) + lines[diag.absolute_target() - 1].targeting_diags.append(diag) + + 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.count -= 1 + 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 = 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.count += 1 + else: + diag_errors_left.append((line_n, diag_s, diag_category)) + for line_n, diag_s, diag_category in diag_errors_left: + add_diag(line_n, diag_s, diag_category, lines) + with open(filename, "w") as f: + for line in lines: + f.write(line.render()) + + +def update_test_files(errors): + 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)) + for filename, diag_errors in errors_by_file.items(): + try: + update_test_file(filename, diag_errors) + except KnownException as e: + print(f"{filename} - ERROR: {e}") + print("continuing...") + + +curr = [] +curr_category = None +curr_run_line = None +lines_since_run = [] +for line in sys.stdin.readlines(): + lines_since_run.append(line) + try: + if line.startswith("RUN:"): + if curr: + update_test_files(curr) + curr = [] + lines_since_run = [line] + curr_run_line = line + else: + for line in lines_since_run: + print(line, end="") + print("====================") + print("no mismatching diagnostics found since last RUN line") + continue + if line.startswith("error: "): + if "no expected directives found" in line: + print( + f"no expected directives found for RUN line '{curr_run_line.strip()}'. Add 'expected-no-diagnostics' manually if this is intended." + ) + continue + curr_category = parse_error_category(line[len("error: ") :]) + continue + + diag_error = parse_diag_error(line.strip()) + if diag_error: + curr.append((diag_error, curr_category)) + except Exception as e: + for line in lines_since_run: + print(line, end="") + print("====================") + print(e) + sys.exit(1) +if curr: + update_test_files(curr) + print("done!") +else: + for line in lines_since_run: + print(line, end="") + print("====================") + print("no mismatching diagnostics found") >From e9919beb705213398ad4faaf4d2b70d6579bdf5a Mon Sep 17 00:00:00 2001 From: "Henrik G. Olsson" <h_ols...@apple.com> Date: Tue, 27 Aug 2024 16:51:35 -0700 Subject: [PATCH 2/4] [Utils] Add testing for update-verify-tests, fix bugs Fixes various line offset bugs. Fixes a bug where whitespace could be emitted between 'expected-[category]' and the line location specifier, which clang does not parse. Now handles the cases where the file does not emit any diagnostics, or previously contained 'expected-no-diagnostics'. Also does further work to minimise diffs by replacing diagnostics checks with a new one, even if the previous check was in a location that the script would not normally emit. This is useful for the case where a check is on the same line as the emitted diagnostic, and the diagnostic message changes. Instead of removing the previous diagnostic and inserting a new one with @+1, the old diagnostic text is replaced with the new one. --- .../Inputs/duplicate-diag.c | 8 + .../Inputs/duplicate-diag.c.expected | 8 + .../Inputs/infer-indentation.c | 8 + .../Inputs/infer-indentation.c.expected | 11 + .../Inputs/leave-existing-diags.c | 11 + .../Inputs/leave-existing-diags.c.expected | 12 ++ .../Inputs/multiple-errors.c | 6 + .../Inputs/multiple-errors.c.expected | 9 + .../multiple-missing-errors-same-line.c | 8 + ...ltiple-missing-errors-same-line.c.expected | 13 ++ .../update-verify-tests/Inputs/no-checks.c | 3 + .../Inputs/no-checks.c.expected | 4 + .../update-verify-tests/Inputs/no-diags.c | 5 + .../Inputs/no-diags.c.expected | 5 + .../Inputs/no-expected-diags.c | 4 + .../Inputs/no-expected-diags.c.expected | 4 + .../Inputs/update-same-line.c | 4 + .../Inputs/update-same-line.c.expected | 4 + .../Inputs/update-single-check.c | 4 + .../Inputs/update-single-check.c.expected | 4 + .../update-verify-tests/duplicate-diag.test | 4 + .../infer-indentation.test | 3 + .../leave-existing-diags.test | 4 + .../utils/update-verify-tests/lit.local.cfg | 25 +++ .../update-verify-tests/multiple-errors.test | 3 + .../multiple-missing-errors-same-line.test | 3 + .../utils/update-verify-tests/no-checks.test | 3 + .../utils/update-verify-tests/no-diags.test | 4 + .../no-expected-diags.test | 4 + .../update-verify-tests/update-same-line.test | 4 + .../update-single-check.test | 3 + clang/utils/update-verify-tests.py | 197 ++++++++++++------ 32 files changed, 325 insertions(+), 67 deletions(-) create mode 100644 clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/infer-indentation.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-errors.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-checks.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-diags.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-same-line.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-single-check.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected create mode 100644 clang/test/utils/update-verify-tests/duplicate-diag.test create mode 100644 clang/test/utils/update-verify-tests/infer-indentation.test create mode 100644 clang/test/utils/update-verify-tests/leave-existing-diags.test create mode 100644 clang/test/utils/update-verify-tests/lit.local.cfg create mode 100644 clang/test/utils/update-verify-tests/multiple-errors.test create mode 100644 clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test create mode 100644 clang/test/utils/update-verify-tests/no-checks.test create mode 100644 clang/test/utils/update-verify-tests/no-diags.test create mode 100644 clang/test/utils/update-verify-tests/no-expected-diags.test create mode 100644 clang/test/utils/update-verify-tests/update-same-line.test create mode 100644 clang/test/utils/update-verify-tests/update-single-check.test diff --git a/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c new file mode 100644 index 00000000000000..8c7e46c6eca9c1 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c @@ -0,0 +1,8 @@ +void foo() { + // expected-error@+1{{use of undeclared identifier 'a'}} + a = 2; a = 2; + b = 2; b = 2; + // expected-error@+1 3{{use of undeclared identifier 'c'}} + c = 2; c = 2; + // expected-error 2{{asdf}} +} diff --git a/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected new file mode 100644 index 00000000000000..6214ff382f4495 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/duplicate-diag.c.expected @@ -0,0 +1,8 @@ +void foo() { + // expected-error@+1 2{{use of undeclared identifier 'a'}} + a = 2; a = 2; + // expected-error@+1 2{{use of undeclared identifier 'b'}} + b = 2; b = 2; + // expected-error@+1 2{{use of undeclared identifier 'c'}} + c = 2; c = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c new file mode 100644 index 00000000000000..0210ac35fd5cd1 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c @@ -0,0 +1,8 @@ +void foo() { + // expected-error@+1 2 {{use of undeclared identifier 'a'}} + a = 2; a = 2; b = 2; b = 2; c = 2; + // expected-error@+1 2 {{asdf}} + d = 2; + e = 2; f = 2; // expected-error 2 {{use of undeclared identifier 'e'}} +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected new file mode 100644 index 00000000000000..5c5aaeeef97acf --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/infer-indentation.c.expected @@ -0,0 +1,11 @@ +void foo() { + // expected-error@+3 {{use of undeclared identifier 'c'}} + // expected-error@+2 2 {{use of undeclared identifier 'b'}} + // expected-error@+1 2 {{use of undeclared identifier 'a'}} + a = 2; a = 2; b = 2; b = 2; c = 2; + // expected-error@+1 {{use of undeclared identifier 'd'}} + d = 2; + // expected-error@+1 {{use of undeclared identifier 'f'}} + e = 2; f = 2; // expected-error {{use of undeclared identifier 'e'}} +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c new file mode 100644 index 00000000000000..1aa8d088e97273 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c @@ -0,0 +1,11 @@ +void foo() { + a = 2; + // expected-error@-1{{use of undeclared identifier 'a'}} + b = 2;// expected-error{{use of undeclared identifier 'b'}} + c = 2; + // expected-error@5{{use of undeclared identifier 'c'}} + d = 2; // expected-error-re{{use of {{.*}} identifier 'd'}} + + e = 2; // error to trigger mismatch +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected new file mode 100644 index 00000000000000..6b621061bbfbbd --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/leave-existing-diags.c.expected @@ -0,0 +1,12 @@ +void foo() { + a = 2; + // expected-error@-1{{use of undeclared identifier 'a'}} + b = 2;// expected-error{{use of undeclared identifier 'b'}} + c = 2; + // expected-error@5{{use of undeclared identifier 'c'}} + d = 2; // expected-error-re{{use of {{.*}} identifier 'd'}} + + // expected-error@+1{{use of undeclared identifier 'e'}} + e = 2; // error to trigger mismatch +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c new file mode 100644 index 00000000000000..e230e0a337bf49 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c @@ -0,0 +1,6 @@ +void foo() { + a = 2; + b = 2; + + c = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected new file mode 100644 index 00000000000000..27dc1f30a26faf --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/multiple-errors.c.expected @@ -0,0 +1,9 @@ +void foo() { + // expected-error@+1{{use of undeclared identifier 'a'}} + a = 2; + // expected-error@+1{{use of undeclared identifier 'b'}} + b = 2; + + // expected-error@+1{{use of undeclared identifier 'c'}} + c = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c new file mode 100644 index 00000000000000..03f723d44bbe82 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c @@ -0,0 +1,8 @@ +void foo() { + a = 2; b = 2; c = 2; +} + +void bar() { + x = 2; y = 2; z = 2; + // expected-error@-1{{use of undeclared identifier 'x'}} +} diff --git a/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected new file mode 100644 index 00000000000000..24b57f4353d95d --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/multiple-missing-errors-same-line.c.expected @@ -0,0 +1,13 @@ +void foo() { + // expected-error@+3{{use of undeclared identifier 'c'}} + // expected-error@+2{{use of undeclared identifier 'b'}} + // expected-error@+1{{use of undeclared identifier 'a'}} + a = 2; b = 2; c = 2; +} + +void bar() { + x = 2; y = 2; z = 2; + // expected-error@-1{{use of undeclared identifier 'x'}} + // expected-error@-2{{use of undeclared identifier 'y'}} + // expected-error@-3{{use of undeclared identifier 'z'}} +} diff --git a/clang/test/utils/update-verify-tests/Inputs/no-checks.c b/clang/test/utils/update-verify-tests/Inputs/no-checks.c new file mode 100644 index 00000000000000..8fd1f7cd333705 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-checks.c @@ -0,0 +1,3 @@ +void foo() { + bar = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected new file mode 100644 index 00000000000000..e80548fbe50f2c --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-checks.c.expected @@ -0,0 +1,4 @@ +void foo() { + // expected-error@+1{{use of undeclared identifier 'bar'}} + bar = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/no-diags.c b/clang/test/utils/update-verify-tests/Inputs/no-diags.c new file mode 100644 index 00000000000000..66d169be439402 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-diags.c @@ -0,0 +1,5 @@ +void foo() { + // expected-error@+1{{asdf}} + int a = 2; +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected new file mode 100644 index 00000000000000..05230284945702 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-diags.c.expected @@ -0,0 +1,5 @@ +// expected-no-diagnostics +void foo() { + int a = 2; +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c new file mode 100644 index 00000000000000..78b72e1357da76 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c @@ -0,0 +1,4 @@ +// expected-no-diagnostics +void foo() { + a = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected new file mode 100644 index 00000000000000..d948ffce56189a --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/no-expected-diags.c.expected @@ -0,0 +1,4 @@ +void foo() { + // expected-error@+1{{use of undeclared identifier 'a'}} + a = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/update-same-line.c b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c new file mode 100644 index 00000000000000..5278ce0c57c319 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c @@ -0,0 +1,4 @@ +void foo() { + bar = 2; // expected-error {{asdf}} +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected new file mode 100644 index 00000000000000..8ba47f788319b1 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/update-same-line.c.expected @@ -0,0 +1,4 @@ +void foo() { + bar = 2; // expected-error {{use of undeclared identifier 'bar'}} +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/update-single-check.c b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c new file mode 100644 index 00000000000000..20b011bfc3d77e --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c @@ -0,0 +1,4 @@ +void foo() { + // expected-error@+1{{asdf}} + bar = 2; +} diff --git a/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected new file mode 100644 index 00000000000000..e80548fbe50f2c --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/update-single-check.c.expected @@ -0,0 +1,4 @@ +void foo() { + // expected-error@+1{{use of undeclared identifier 'bar'}} + bar = 2; +} diff --git a/clang/test/utils/update-verify-tests/duplicate-diag.test b/clang/test/utils/update-verify-tests/duplicate-diag.test new file mode 100644 index 00000000000000..3163ce46199c3f --- /dev/null +++ b/clang/test/utils/update-verify-tests/duplicate-diag.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/duplicate-diag.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/duplicate-diag.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c + diff --git a/clang/test/utils/update-verify-tests/infer-indentation.test b/clang/test/utils/update-verify-tests/infer-indentation.test new file mode 100644 index 00000000000000..6ba2f5d9d505bf --- /dev/null +++ b/clang/test/utils/update-verify-tests/infer-indentation.test @@ -0,0 +1,3 @@ +# RUN: cp %S/Inputs/infer-indentation.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/infer-indentation.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c diff --git a/clang/test/utils/update-verify-tests/leave-existing-diags.test b/clang/test/utils/update-verify-tests/leave-existing-diags.test new file mode 100644 index 00000000000000..cde690ef715a67 --- /dev/null +++ b/clang/test/utils/update-verify-tests/leave-existing-diags.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/leave-existing-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/leave-existing-diags.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c + diff --git a/clang/test/utils/update-verify-tests/lit.local.cfg b/clang/test/utils/update-verify-tests/lit.local.cfg new file mode 100644 index 00000000000000..a0b6afccc25010 --- /dev/null +++ b/clang/test/utils/update-verify-tests/lit.local.cfg @@ -0,0 +1,25 @@ +import lit.util + +# python 2.7 backwards compatibility +try: + from shlex import quote as shell_quote +except ImportError: + from pipes import quote as shell_quote + +if config.standalone_build: + # These tests require the update-verify-tests.py script from the clang + # source tree, so skip these tests if we are doing standalone builds. + config.unsupported = True +else: + config.suffixes = [".test"] + + script_path = os.path.join( + config.clang_src_dir, "utils", "update-verify-tests.py" + ) + python = shell_quote(config.python_executable) + config.substitutions.append( + ( + "%update-verify-tests", + "%s %s" % (python, shell_quote(script_path)), + ) + ) diff --git a/clang/test/utils/update-verify-tests/multiple-errors.test b/clang/test/utils/update-verify-tests/multiple-errors.test new file mode 100644 index 00000000000000..1332ef365dc863 --- /dev/null +++ b/clang/test/utils/update-verify-tests/multiple-errors.test @@ -0,0 +1,3 @@ +# RUN: cp %S/Inputs/multiple-errors.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/multiple-errors.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c diff --git a/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test b/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test new file mode 100644 index 00000000000000..a9c21cd77e192b --- /dev/null +++ b/clang/test/utils/update-verify-tests/multiple-missing-errors-same-line.test @@ -0,0 +1,3 @@ +# RUN: cp %S/Inputs/multiple-missing-errors-same-line.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/multiple-missing-errors-same-line.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c diff --git a/clang/test/utils/update-verify-tests/no-checks.test b/clang/test/utils/update-verify-tests/no-checks.test new file mode 100644 index 00000000000000..f6ea91fa552be4 --- /dev/null +++ b/clang/test/utils/update-verify-tests/no-checks.test @@ -0,0 +1,3 @@ +# RUN: cp %S/Inputs/no-checks.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/no-checks.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c diff --git a/clang/test/utils/update-verify-tests/no-diags.test b/clang/test/utils/update-verify-tests/no-diags.test new file mode 100644 index 00000000000000..464fe8894253b6 --- /dev/null +++ b/clang/test/utils/update-verify-tests/no-diags.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/no-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/no-diags.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c + diff --git a/clang/test/utils/update-verify-tests/no-expected-diags.test b/clang/test/utils/update-verify-tests/no-expected-diags.test new file mode 100644 index 00000000000000..75235f17a64a29 --- /dev/null +++ b/clang/test/utils/update-verify-tests/no-expected-diags.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/no-expected-diags.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/no-expected-diags.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c + diff --git a/clang/test/utils/update-verify-tests/update-same-line.test b/clang/test/utils/update-verify-tests/update-same-line.test new file mode 100644 index 00000000000000..324768eae5faac --- /dev/null +++ b/clang/test/utils/update-verify-tests/update-same-line.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/update-same-line.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/update-same-line.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c + diff --git a/clang/test/utils/update-verify-tests/update-single-check.test b/clang/test/utils/update-verify-tests/update-single-check.test new file mode 100644 index 00000000000000..2cb1ae3bcbd3b8 --- /dev/null +++ b/clang/test/utils/update-verify-tests/update-single-check.test @@ -0,0 +1,3 @@ +# RUN: cp %S/Inputs/update-single-check.c %t.c && not %clang_cc1 -verify %t.c 2>&1 | %update-verify-tests +# RUN: diff -u %S/Inputs/update-single-check.c.expected %t.c +# RUN: %clang_cc1 -verify %t.c diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py index cfcfefc85e576a..f40ce15e449f22 100644 --- a/clang/utils/update-verify-tests.py +++ b/clang/utils/update-verify-tests.py @@ -28,6 +28,8 @@ class KnownException(Exception): def parse_error_category(s): + if "no expected directives found" in line: + return None parts = s.split("diagnostics") diag_category = parts[0] category_parts = parts[0].strip().strip("'").split("-") @@ -64,21 +66,10 @@ def __init__(self, content, line_n): self.content = content self.diag = None self.line_n = line_n - self.related_diags = [] self.targeting_diags = [] def update_line_n(self, n): - if self.diag and not self.diag.line_is_absolute: - self.diag.orig_target_line_n += n - self.line_n self.line_n = n - for diag in self.targeting_diags: - if diag.line_is_absolute: - diag.orig_target_line_n = n - else: - diag.orig_target_line_n = n - diag.line.line_n - for diag in self.related_diags: - if not diag.line_is_absolute: - pass def render(self): if not self.diag: @@ -95,16 +86,17 @@ def __init__( self, diag_content, category, - targeted_line_n, + parsed_target_line_n, line_is_absolute, count, line, is_re, whitespace_strings, + is_from_source_file, ): self.diag_content = diag_content self.category = category - self.orig_target_line_n = targeted_line_n + self.parsed_target_line_n = parsed_target_line_n self.line_is_absolute = line_is_absolute self.count = count self.line = line @@ -112,25 +104,50 @@ def __init__( 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 add(self): - if targeted_line > 0: - targeted_line += 1 - elif targeted_line < 0: - targeted_line -= 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.line_is_absolute: - res = self.orig_target_line_n - else: - res = self.line.line_n + self.orig_target_line_n if self.target: - assert self.line.line_n == res - return res + 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: @@ -151,19 +168,25 @@ def render(self): whitespace1_s = self.whitespace_strings[0] whitespace2_s = self.whitespace_strings[1] whitespace3_s = self.whitespace_strings[2] - whitespace4_s = self.whitespace_strings[3] else: whitespace1_s = " " whitespace2_s = "" whitespace3_s = "" - whitespace4_s = "" - if count_s and not whitespace3_s: - whitespace3_s = " " - return f"//{whitespace1_s}expected-{self.category}{re_s}{whitespace2_s}{line_location_s}{whitespace3_s}{count_s}{whitespace4_s}{{{{{self.diag_content}}}}}" - + 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}expected-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}" expected_diag_re = re.compile( - r"//(\s*)expected-(note|warning|error)(-re)?(\s*)(@[+-]?\d+)?(\s*)(\d+)?(\s*)\{\{(.*)\}\}" + r"//(\s*)expected-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}" ) @@ -173,19 +196,17 @@ def parse_diag(line, filename, lines): if not ms: return None if len(ms) > 1: - print( + raise KnownException( f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation." ) - sys.exit(1) [ whitespace1_s, category_s, re_s, - whitespace2_s, target_line_s, - whitespace3_s, + whitespace2_s, count_s, - whitespace4_s, + whitespace3_s, diag_s, ] = ms[0] if not target_line_s: @@ -211,18 +232,11 @@ def parse_diag(line, filename, lines): count, line, bool(re_s), - [whitespace1_s, whitespace2_s, whitespace3_s, whitespace4_s], + [whitespace1_s, whitespace2_s, whitespace3_s], + True, ) -def link_line_diags(lines, diag): - line_n = diag.line.line_n - target_line_n = diag.absolute_target() - step = 1 if target_line_n < line_n else -1 - for i in range(target_line_n, line_n, step): - lines[i - 1].related_diags.append(diag) - - 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)): @@ -231,6 +245,14 @@ def add_line(new_line, lines): 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*") @@ -238,8 +260,11 @@ def add_line(new_line, lines): 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(line_n, diag_s, diag_category, lines): +def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines): + 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: @@ -272,18 +297,42 @@ def add_diag(line_n, diag_s, diag_category, lines): assert new_line_n == line_n + (not reverse) - total_offset new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n) - new_line.related_diags = list(prev_line.related_diags) add_line(new_line, lines) + whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None new_diag = Diag( - diag_s, diag_category, total_offset, False, 1, new_line, False, None + diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False, ) new_line.diag = new_diag - new_diag.target_line = target - assert type(new_diag) != str - target.targeting_diags.append(new_diag) - link_line_diags(lines, 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): + for line in lines: + if "expected-no-diagnostics" in line.content: + return line.line_n + return None updated_test_files = set() @@ -298,13 +347,14 @@ def update_test_file(filename, diag_errors): 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) + for line in lines: diag = parse_diag(line, filename, lines) if diag: line.diag = diag - diag.target_line = lines[diag.absolute_target() - 1] - link_line_diags(lines, diag) - lines[diag.absolute_target() - 1].targeting_diags.append(diag) + diag.set_target(lines[diag.absolute_target() - 1]) for line_n, diag_s, diag_category, seen in diag_errors: if seen: @@ -319,13 +369,13 @@ def update_test_file(filename, diag_errors): raise KnownException( f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_category}" ) - lines[line_n - 1].diag.count -= 1 + 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 = lines[line_n - 1] + target = orig_lines[line_n - 1] other_diags = [ d for d in target.targeting_diags @@ -333,13 +383,17 @@ def update_test_file(filename, diag_errors): ] other_diag = other_diags[0] if other_diags else None if other_diag: - other_diag.count += 1 + other_diag.increment_count() else: - diag_errors_left.append((line_n, diag_s, diag_category)) - for line_n, diag_s, diag_category in diag_errors_left: - add_diag(line_n, diag_s, diag_category, lines) + add_diag(line_n, diag_s, diag_category, lines, orig_lines) + 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()) @@ -361,6 +415,7 @@ def update_test_files(errors): curr_category = None curr_run_line = None lines_since_run = [] +skip_to_next_file = False for line in sys.stdin.readlines(): lines_since_run.append(line) try: @@ -374,20 +429,28 @@ def update_test_files(errors): for line in lines_since_run: print(line, end="") print("====================") - print("no mismatching diagnostics found since last RUN line") + if lines_since_run: + print("no mismatching diagnostics found since last RUN line") + skip_to_next_file = False + continue + if skip_to_next_file: continue if line.startswith("error: "): - if "no expected directives found" in line: - print( - f"no expected directives found for RUN line '{curr_run_line.strip()}'. Add 'expected-no-diagnostics' manually if this is intended." - ) - continue curr_category = parse_error_category(line[len("error: ") :]) continue diag_error = parse_diag_error(line.strip()) if diag_error: curr.append((diag_error, curr_category)) + except KnownException as e: + print(f"Error while parsing: {e}") + if curr: + print("skipping to next file") + curr = [] + curr_category = None + curr_run_line = None + lines_since_run = [] + skip_to_next_file = True except Exception as e: for line in lines_since_run: print(line, end="") >From dd702ce2be48dcf225755d7228b7993b698e888a Mon Sep 17 00:00:00 2001 From: "Henrik G. Olsson" <h_ols...@apple.com> Date: Tue, 27 Aug 2024 17:42:50 -0700 Subject: [PATCH 3/4] [Utils] Add custom prefix support to update-verify-tests Custom prefixes can now be provided with --prefix. The default remains 'expected'. --- .../Inputs/non-default-prefix.c | 5 + .../Inputs/non-default-prefix.c.expected | 5 + .../non-default-prefix.test | 4 + clang/utils/update-verify-tests.py | 153 ++++++++++-------- 4 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c create mode 100644 clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected create mode 100644 clang/test/utils/update-verify-tests/non-default-prefix.test diff --git a/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c new file mode 100644 index 00000000000000..3d63eaf0f1b878 --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c @@ -0,0 +1,5 @@ +void foo() { + a = 2; // check-error{{asdf}} + // expected-error@-1{ignored}} +} + diff --git a/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected new file mode 100644 index 00000000000000..a877f86922123d --- /dev/null +++ b/clang/test/utils/update-verify-tests/Inputs/non-default-prefix.c.expected @@ -0,0 +1,5 @@ +void foo() { + a = 2; // check-error{{use of undeclared identifier 'a'}} + // expected-error@-1{ignored}} +} + diff --git a/clang/test/utils/update-verify-tests/non-default-prefix.test b/clang/test/utils/update-verify-tests/non-default-prefix.test new file mode 100644 index 00000000000000..e581755a6e6038 --- /dev/null +++ b/clang/test/utils/update-verify-tests/non-default-prefix.test @@ -0,0 +1,4 @@ +# RUN: cp %S/Inputs/non-default-prefix.c %t.c && not %clang_cc1 -verify=check %t.c 2>&1 | %update-verify-tests --prefix check +# RUN: diff -u %S/Inputs/non-default-prefix.c.expected %t.c +# RUN: %clang_cc1 -verify=check %t.c + diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py index f40ce15e449f22..dd83a9418ddf5f 100644 --- a/clang/utils/update-verify-tests.py +++ b/clang/utils/update-verify-tests.py @@ -1,5 +1,6 @@ import sys import re +import argparse """ Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output. @@ -10,7 +11,6 @@ diffs. If inaccurate their count will be updated, or the check removed entirely. Missing features: - - custom prefix support (-verify=my-prefix) - multiple prefixes on the same line (-verify=my-prefix,my-other-prefix) - multiple prefixes on separate RUN lines (RUN: -verify=my-prefix\nRUN: -verify my-other-prefix) - regexes with expected-*-re: existing ones will be left untouched if accurate, but the script will abort if there are any @@ -27,16 +27,16 @@ class KnownException(Exception): pass -def parse_error_category(s): - if "no expected directives found" in line: +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 != "expected": + if expected != prefix: raise Exception( - f"expected 'expected', but found '{expected}'. Custom verify prefixes are not supported." + 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]: @@ -84,6 +84,7 @@ def render(self): class Diag: def __init__( self, + prefix, diag_content, category, parsed_target_line_n, @@ -94,6 +95,7 @@ def __init__( 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 @@ -183,14 +185,14 @@ def render(self): been parsed as whitespace3_s. """ whitespace2_s = "" - return f"//{whitespace1_s}expected-{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}" + 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*)expected-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}" + r"//(\s*)([a-zA-Z]+)-(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}" ) -def parse_diag(line, filename, lines): +def parse_diag(line, filename, lines, prefix): s = line.content ms = expected_diag_re.findall(s) if not ms: @@ -201,6 +203,7 @@ def parse_diag(line, filename, lines): ) [ whitespace1_s, + check_prefix, category_s, re_s, target_line_s, @@ -209,6 +212,8 @@ def parse_diag(line, filename, lines): whitespace3_s, diag_s, ] = ms[0] + if check_prefix != prefix: + return None if not target_line_s: target_line_n = 0 is_absolute = False @@ -225,6 +230,7 @@ def parse_diag(line, filename, lines): line.content = expected_diag_re.sub("{{DIAG}}", s) return Diag( + prefix, diag_s, category_s, target_line_n, @@ -263,7 +269,7 @@ def get_indent(s): 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): +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: @@ -301,7 +307,7 @@ def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines): whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None new_diag = Diag( - diag_s, diag_category, total_offset, False, 1, new_line, False, whitespace_strings, False, + 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) @@ -328,16 +334,16 @@ def has_live_diags(lines): return True return False -def get_expected_no_diags_line_n(lines): +def get_expected_no_diags_line_n(lines, prefix): for line in lines: - if "expected-no-diagnostics" in line.content: + if f"{prefix}-no-diagnostics" in line.content: return line.line_n return None updated_test_files = set() -def update_test_file(filename, diag_errors): +def update_test_file(filename, diag_errors, prefix): print(f"updating test file {filename}") if filename in updated_test_files: print( @@ -348,10 +354,10 @@ def update_test_file(filename, diag_errors): 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) + expected_no_diags_line_n = get_expected_no_diags_line_n(orig_lines, prefix) for line in lines: - diag = parse_diag(line, filename, lines) + diag = parse_diag(line, filename, lines, prefix) if diag: line.diag = diag diag.set_target(lines[diag.absolute_target() - 1]) @@ -385,7 +391,7 @@ def update_test_file(filename, diag_errors): if other_diag: other_diag.increment_count() else: - add_diag(line_n, diag_s, diag_category, lines, orig_lines) + 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: @@ -397,7 +403,7 @@ def update_test_file(filename, diag_errors): f.write(line.render()) -def update_test_files(errors): +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: @@ -405,63 +411,72 @@ def update_test_files(errors): errors_by_file[filename].append((line, diag_s, diag_category, seen)) for filename, diag_errors in errors_by_file.items(): try: - update_test_file(filename, diag_errors) + update_test_file(filename, diag_errors, prefix) except KnownException as e: print(f"{filename} - ERROR: {e}") print("continuing...") - -curr = [] -curr_category = None -curr_run_line = None -lines_since_run = [] -skip_to_next_file = False -for line in sys.stdin.readlines(): - lines_since_run.append(line) - try: - if line.startswith("RUN:"): - if curr: - update_test_files(curr) +def check_expectations(tool_output, prefix): + curr = [] + curr_category = None + curr_run_line = None + lines_since_run = [] + skip_to_next_file = False + for line in tool_output: + lines_since_run.append(line) + try: + if line.startswith("RUN:"): + if curr: + update_test_files(curr, prefix) + curr = [] + lines_since_run = [line] + curr_run_line = line + else: + for line in lines_since_run: + print(line, end="") + print("====================") + if lines_since_run: + print("no mismatching diagnostics found since last RUN line") + skip_to_next_file = False + continue + if skip_to_next_file: + continue + 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)) + except KnownException as e: + print(f"Error while parsing: {e}") + if curr: + print("skipping to next file") curr = [] - lines_since_run = [line] - curr_run_line = line - else: - for line in lines_since_run: - print(line, end="") - print("====================") - if lines_since_run: - print("no mismatching diagnostics found since last RUN line") - skip_to_next_file = False - continue - if skip_to_next_file: - continue - if line.startswith("error: "): - curr_category = parse_error_category(line[len("error: ") :]) - continue - - diag_error = parse_diag_error(line.strip()) - if diag_error: - curr.append((diag_error, curr_category)) - except KnownException as e: - print(f"Error while parsing: {e}") - if curr: - print("skipping to next file") - curr = [] - curr_category = None - curr_run_line = None - lines_since_run = [] - skip_to_next_file = True - except Exception as e: + curr_category = None + curr_run_line = None + lines_since_run = [] + skip_to_next_file = True + except Exception as e: + for line in lines_since_run: + print(line, end="") + print("====================") + print(e) + sys.exit(1) + if curr: + update_test_files(curr, prefix) + print("done!") + else: for line in lines_since_run: print(line, end="") print("====================") - print(e) - sys.exit(1) -if curr: - update_test_files(curr) - print("done!") -else: - for line in lines_since_run: - print(line, end="") - print("====================") - print("no mismatching diagnostics found") + print("no mismatching diagnostics found") + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--prefix", default="expected", help="The prefix passed to -verify") + args = parser.parse_args() + check_expectations(sys.stdin.readlines(), args.prefix) + +if __name__ == "__main__": + main() >From 7395ef2db8c43f41cdbed70a4fa8c06579a078fe Mon Sep 17 00:00:00 2001 From: "Henrik G. Olsson" <h_ols...@apple.com> Date: Wed, 11 Sep 2024 17:05:43 -0700 Subject: [PATCH 4/4] [Utils] Separate update-verify-tests script from core The script no longer attempts to be RUN line aware. Feeding raw llvm-lit output into it _may_ still work, but given a large enough number of test cases to update, the risk that some test case uses unsupported features grows pretty large. This will instead be handled by adding a lit integration to automatically invoke the core for each failing test output. The lit integration will be landed as a separate change. --- clang/utils/UpdateVerifyTests/core.py | 452 +++++++++++++++++++++++++ clang/utils/update-verify-tests.py | 462 +------------------------- 2 files changed, 461 insertions(+), 453 deletions(-) create mode 100644 clang/utils/UpdateVerifyTests/core.py diff --git a/clang/utils/UpdateVerifyTests/core.py b/clang/utils/UpdateVerifyTests/core.py new file mode 100644 index 00000000000000..d1350cdbb698b6 --- /dev/null +++ b/clang/utils/UpdateVerifyTests/core.py @@ -0,0 +1,452 @@ +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" diff --git a/clang/utils/update-verify-tests.py b/clang/utils/update-verify-tests.py index dd83a9418ddf5f..e2874a8c049ef3 100644 --- a/clang/utils/update-verify-tests.py +++ b/clang/utils/update-verify-tests.py @@ -1,6 +1,6 @@ import sys -import re import argparse +from UpdateVerifyTests.core import check_expectations """ Pipe output from clang's -verify into this script to have the test case updated to expect the actual diagnostic output. @@ -19,464 +19,20 @@ - if multiple checks targeting the same line are failing the script is not guaranteed to produce a minimal diff Example usage: - build/bin/llvm-lit clang/test/Sema/ --no-progress-bar -v | python3 update-verify-tests.py + clang -verify [file] | python3 update-verify-tests.py + clang -verify=check [file] | python3 update-verify-tests.py --prefix check """ -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 - -updated_test_files = set() - - -def update_test_file(filename, diag_errors, prefix): - print(f"updating test file {filename}") - if filename in updated_test_files: - print( - f"{filename} already updated, but got new output - expect incorrect results" - ) - 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)) - for filename, diag_errors in errors_by_file.items(): - try: - update_test_file(filename, diag_errors, prefix) - except KnownException as e: - print(f"{filename} - ERROR: {e}") - print("continuing...") - -def check_expectations(tool_output, prefix): - curr = [] - curr_category = None - curr_run_line = None - lines_since_run = [] - skip_to_next_file = False - for line in tool_output: - lines_since_run.append(line) - try: - if line.startswith("RUN:"): - if curr: - update_test_files(curr, prefix) - curr = [] - lines_since_run = [line] - curr_run_line = line - else: - for line in lines_since_run: - print(line, end="") - print("====================") - if lines_since_run: - print("no mismatching diagnostics found since last RUN line") - skip_to_next_file = False - continue - if skip_to_next_file: - continue - 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)) - except KnownException as e: - print(f"Error while parsing: {e}") - if curr: - print("skipping to next file") - curr = [] - curr_category = None - curr_run_line = None - lines_since_run = [] - skip_to_next_file = True - except Exception as e: - for line in lines_since_run: - print(line, end="") - print("====================") - print(e) - sys.exit(1) - if curr: - update_test_files(curr, prefix) - print("done!") - else: - for line in lines_since_run: - print(line, end="") - print("====================") - print("no mismatching diagnostics found") - def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--prefix", default="expected", help="The prefix passed to -verify") + parser.add_argument( + "--prefix", default="expected", help="The prefix passed to -verify" + ) args = parser.parse_args() - check_expectations(sys.stdin.readlines(), args.prefix) + output = check_expectations(sys.stdin.readlines(), args.prefix) + print(output) + if __name__ == "__main__": main() _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits