diff --git a/test/Utils/update-verify-tests/Inputs/unstringify-macro.swift b/test/Utils/update-verify-tests/Inputs/unstringify-macro.swift new file mode 100644 index 000000000000..e5c33f86c43d --- /dev/null +++ b/test/Utils/update-verify-tests/Inputs/unstringify-macro.swift @@ -0,0 +1,19 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct UnstringifyPeerMacro: PeerMacro { + public static func expansion( + of node: AttributeSyntax, + providingPeersOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + do { + let argumentList = node.arguments!.as(LabeledExprListSyntax.self)! + let arguments = [LabeledExprSyntax](argumentList) + let arg = arguments.first!.expression.as(StringLiteralExprSyntax.self)! + let content = arg.representedLiteralValue! + return [DeclSyntax("\(raw: content)")] + } + } +} diff --git a/test/Utils/update-verify-tests/expansion.swift b/test/Utils/update-verify-tests/expansion.swift new file mode 100644 index 000000000000..db1f80a5678a --- /dev/null +++ b/test/Utils/update-verify-tests/expansion.swift @@ -0,0 +1,240 @@ +// REQUIRES: swift_swift_parser, executable_test + +// RUN: %empty-directory(%t) +// RUN: split-file %s %t + +// Building this macro takes some time, so amortise the cost by using it for multiple sub tests +// RUN: %host-build-swift -swift-version 5 -emit-library -o %t/%target-library-name(UnstringifyMacroDefinition) -module-name=UnstringifyMacroDefinition \ +// RUN: %S/Inputs/unstringify-macro.swift -g -no-toolchain-stdlib-rpath + + + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/single.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/single.swift +// RUN: %diff %t/single.swift %t/single.swift.expected + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/multiple.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/multiple.swift +// RUN: %diff %t/multiple.swift %t/multiple.swift.expected + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/existing.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/existing.swift +// RUN: %diff %t/existing.swift %t/existing.swift.expected + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/gone.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/gone.swift +// RUN: %diff %t/gone.swift %t/gone.swift.expected + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/wrong-location.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/wrong-location.swift +// RUN: %diff %t/wrong-location.swift %t/wrong-location.swift.expected + +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/nested.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/nested.swift +// RUN: %diff %t/nested.swift %t/nested.swift.expected + +//--- single.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +@unstringifyPeer(""" +func foo(_ x: Int) { + let a = x +} +""") +func foo() {} + +//--- single.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1{{in expansion of macro 'unstringifyPeer' on global function 'foo()' here}} +@unstringifyPeer(""" +func foo(_ x: Int) { + let a = x +} +""") +// expected-expansion@+3:14{{ +// expected-warning@2{{initialization of immutable value 'a' was never used; consider replacing with assignment to '_' or removing it}} +// }} +func foo() {} + +//--- multiple.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") +func foo() {} + +//--- multiple.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1 4{{in expansion of macro 'unstringifyPeer' on global function 'foo()' here}} +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") +// expected-expansion@+5:14{{ +// expected-note@1 2{{'x' declared here}} +// expected-error@2{{cannot find 'a' in scope; did you mean 'x'?}} +// expected-error@3{{cannot find 'b' in scope; did you mean 'x'?}} +// }} +func foo() {} + +//--- existing.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") + //expected-expansion@+4:14{{ + // expected-note@1 {{'x' declared here}} + // expected-error@3 {{cannot find 'b' in scope; did you mean 'x'?}} + //}} +func foo() {} + +//--- existing.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1 4{{in expansion of macro 'unstringifyPeer' on global function 'foo()' here}} +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") + //expected-expansion@+5:14{{ + // expected-error@3 {{cannot find 'b' in scope; did you mean 'x'?}} + // expected-note@1 2{{'x' declared here}} + // expected-error@2 {{cannot find 'a' in scope; did you mean 'x'?}} + //}} +func foo() {} + +//--- gone.swift + //expected-expansion@+4:14{{ + // expected-note@1 {{'x' declared here}} + // expected-error@3 {{cannot find 'b' in scope; did you mean 'x'?}} + //}} +func foo() {} + +//--- gone.swift.expected +func foo() {} + +//--- wrong-location.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + + // expected-expansion@2:14{{ + // expected-note@2 {{'x' declared here}} + // expected-error@3 {{cannot find 'b' in scope; did you mean 'x'?}} + // }} +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") +func foo() {} + +//--- wrong-location.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1 4{{in expansion of macro 'unstringifyPeer' on global function 'foo()' here}} +@unstringifyPeer(""" +func foo(_ x: Int) { + a = 2 + b = x +} +""") +// expected-expansion@+5:14{{ +// expected-note@1 2{{'x' declared here}} +// expected-error@2{{cannot find 'a' in scope; did you mean 'x'?}} +// expected-error@3{{cannot find 'b' in scope; did you mean 'x'?}} +// }} +func foo() {} + +//--- nested.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") +// hack to make this seem non-recursive +@attached(peer, names: overloaded) +macro unstringifyPeer2(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +@unstringifyPeer(""" +func bar(_ y: Int) { + @unstringifyPeer2(\""" + func foo(_ x: Int) { + a = 2 + b = x + } + \""") + func foo() {} + foo(y) +} +""") +func bar() {} + +//--- nested.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") +// hack to make this seem non-recursive +@attached(peer, names: overloaded) +macro unstringifyPeer2(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1 7{{in expansion of macro 'unstringifyPeer' on global function 'bar()' here}} +@unstringifyPeer(""" +func bar(_ y: Int) { + @unstringifyPeer2(\""" + func foo(_ x: Int) { + a = 2 + b = x + } + \""") + func foo() {} + foo(y) +} +""") +// expected-expansion@+10:14{{ +// expected-note@1 2{{did you mean 'y'?}} +// expected-note@2 4{{in expansion of macro 'unstringifyPeer2' on local function 'foo()' here}} +// expected-expansion@9:6{{ +// expected-note@1 2{{did you mean 'x'?}} +// expected-error@2{{cannot find 'a' in scope}} +// expected-error@3{{cannot find 'b' in scope}} +// }} +// expected-error@10{{argument passed to call that takes no arguments}} +// }} +func bar() {} + diff --git a/test/Utils/update-verify-tests/remarks.swift b/test/Utils/update-verify-tests/remarks.swift new file mode 100644 index 000000000000..b9981e7dcdc5 --- /dev/null +++ b/test/Utils/update-verify-tests/remarks.swift @@ -0,0 +1,21 @@ +// RUN: %empty-directory(%t) +// RUN: split-file %s %t + +// RUN: not %target-swift-frontend-verify -typecheck %t/test.swift -Rmodule-api-import 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -typecheck %t/test.swift -Rmodule-api-import +// RUN: %diff %t/test.swift %t/test.swift.expected + +//--- test.swift +public typealias Foo = String + +public typealias Bar = Optional // expected-remark@+1{{asdf}} + +//--- test.swift.expected +// expected-remark@+1{{struct 'String' is imported via 'Swift'}} +public typealias Foo = String + +// expected-remark@+2{{struct 'Int' is imported via 'Swift'}} +// expected-remark@+1{{generic enum 'Optional' is imported via 'Swift'}} +public typealias Bar = Optional + diff --git a/test/Utils/update-verify-tests/update-existing.swift b/test/Utils/update-verify-tests/update-existing.swift new file mode 100644 index 000000000000..29c892813903 --- /dev/null +++ b/test/Utils/update-verify-tests/update-existing.swift @@ -0,0 +1,64 @@ +// RUN: %empty-directory(%t) +// RUN: split-file %s %t + +// RUN: not %target-swift-frontend-verify -typecheck %t/test.swift 2>%t/output.txt +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -typecheck %t/test.swift +// RUN: %diff %t/test.swift %t/test.swift.expected + +//--- test.swift +func foo() { + let a = 2 // expected-error@+1{{asdf}} + b = a // expected-error@+1{{asdf}} +} + +func bar() { + a = 2 // expected-error@+1{{asdf}} +} + +func baz() { + // expected-error@+2{{cannot find 'a' in scope}} + // expected-error@+1{{cannot find 'a'}} + let b = a; let c = a; // expected-error{{asdf}} +} + +func qux() { + let b = a; let c = a; // expected-error{{asdf}} +} + +func foobar() { + var b = 1 + b = a; b = a; // expected-error{{asdf}} +} + +//--- test.swift.expected +func foo() { + // expected-note@+1{{'a' declared here}} + let a = 2 // expected-error@+1{{cannot find 'b' in scope; did you mean 'a'?}} + b = a +} + +func bar() { + // expected-error@+1{{cannot find 'a' in scope}} + a = 2 +} + +func baz() { + // expected-error@+3{{cannot find 'a' in scope}} + // expected-error@+2{{cannot find 'a'}} + // expected-note@+1{{'b' declared here}} + let b = a; let c = a; +} + +func qux() { + // expected-note@+2{{'b' declared here}} + // expected-error@+1{{cannot find 'a' in scope}} + let b = a; let c = a; // expected-error{{cannot find 'a' in scope; did you mean 'b'?}} +} + +func foobar() { + // expected-note@+1 2{{'b' declared here}} + var b = 1 + b = a; b = a; // expected-error 2{{cannot find 'a' in scope; did you mean 'b'?}} +} + diff --git a/utils/update-verify-tests.py b/utils/update-verify-tests.py index de7c5fdeb60c..22c89d6a9730 100644 --- a/utils/update-verify-tests.py +++ b/utils/update-verify-tests.py @@ -30,9 +30,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--prefix", default="", help="The prefix passed to -verify" - ) + parser.add_argument("--prefix", default="", help="The prefix passed to -verify") args = parser.parse_args() (ret_code, output) = check_expectations(sys.stdin.readlines(), args.prefix) print(output) @@ -41,4 +39,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/utils/update_verify_tests/core.py b/utils/update_verify_tests/core.py index 20ad70d393b9..f076374d871a 100644 --- a/utils/update_verify_tests/core.py +++ b/utils/update_verify_tests/core.py @@ -51,7 +51,7 @@ def render(self): res = self.content.replace("{{DIAG}}", self.diag.render()) if not res.strip(): return "" - return res + return res.rstrip() + "\n" class Diag: @@ -62,11 +62,13 @@ def __init__( category, parsed_target_line_n, line_is_absolute, + col, count, line, is_re, whitespace_strings, is_from_source_file, + nested_lines, ): self.prefix = prefix self.diag_content = diag_content @@ -80,6 +82,10 @@ def __init__( self.absolute_target() self.whitespace_strings = whitespace_strings self.is_from_source_file = is_from_source_file + self._col = col + self.nested_lines = nested_lines + self.parent = None + self.closer = None def decrement_count(self): self.count -= 1 @@ -110,6 +116,12 @@ def absolute_target(self): def relative_target(self): return self.absolute_target() - self.line.line_n + def col(self): + # expected-expansion requires column. Otherwise only retain column info if it's already there. + if self._col and (self.category == "expansion" or self.is_from_source_file): + return self._col + return None + def take(self, other_diag): assert self.count == 0 assert other_diag.count > 0 @@ -142,35 +154,62 @@ def render(self): 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}expected-{self.prefix}{self.category}{re_s}{line_location_s}{whitespace2_s}{count_s}{whitespace3_s}{{{{{self.diag_content}}}}}" + col_s = f":{self.col()}" if self.col() else "" + base_s = f"//{whitespace1_s}expected-{self.prefix}{self.category}{re_s}{line_location_s}{col_s}{whitespace2_s}{count_s}" + if self.category == "expansion": + return base_s + "{{" + else: + return base_s + "{{" + self.diag_content + "}}" + + +class ExpansionDiagClose: + def __init__(self, whitespace, line): + self.whitespace = whitespace + self.line = line + self.parent = None + self.category = "closing" + + def render(self): + return "//" + self.whitespace + "}}" expected_diag_re = re.compile( - r"//(\s*)expected-([a-zA-Z-]*)(note|warning|error)(-re)?(@[+-]?\d+)?(?:(\s*)(\d+))?(\s*)\{\{(.*)\}\}" + r"//(\s*)expected-([a-zA-Z-]*)(note|warning|error|remark)(-re)?(@[+-]?\d+)?(:\d+)?(\s*)(\d+)?\{\{(.*)\}\}" +) +expected_expansion_diag_re = re.compile( + r"//(\s*)expected-([a-zA-Z-]*)(expansion)(-re)?(@[+-]?\d+)(:\d+)(\s*)(\d+)?\{\{(.*)" ) +expected_expansion_close_re = re.compile(r"//(\s*)\}\}") def parse_diag(line, filename, prefix): s = line.content ms = expected_diag_re.findall(s) + matched_re = expected_diag_re if not ms: - return None + ms = expected_expansion_diag_re.findall(s) + matched_re = expected_expansion_diag_re + if not ms: + ms = expected_expansion_close_re.findall(s) + if not ms: + return None + if len(ms) > 1: + raise KnownException( + f"multiple closed scopes on line {filename}:{line.line_n}. Aborting due to missing implementation." + ) + line.content = expected_expansion_close_re.sub("{{DIAG}}", s) + return ExpansionDiagClose(ms[0], line) if len(ms) > 1: raise KnownException( f"multiple diags on line {filename}:{line.line_n}. Aborting due to missing implementation." @@ -181,9 +220,9 @@ def parse_diag(line, filename, prefix): category_s, re_s, target_line_s, + target_col_s, whitespace2_s, count_s, - whitespace3_s, diag_s, ] = ms[0] if check_prefix != prefix and check_prefix != "": @@ -200,8 +239,9 @@ def parse_diag(line, filename, prefix): else: target_line_n = int(target_line_s[1:]) is_absolute = True + col = int(target_col_s[1:]) if target_col_s else None count = int(count_s) if count_s else 1 - line.content = expected_diag_re.sub("{{DIAG}}", s) + line.content = matched_re.sub("{{DIAG}}", s) return Diag( check_prefix, @@ -209,11 +249,13 @@ def parse_diag(line, filename, prefix): category_s, target_line_n, is_absolute, + col, count, line, bool(re_s), - [whitespace1_s, whitespace2_s, whitespace3_s], + [whitespace1_s, whitespace2_s], True, + [], ) @@ -246,9 +288,7 @@ 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] +def infer_line_context(target, line_n): for other in target.targeting_diags: if other.is_re: raise KnownException( @@ -278,36 +318,87 @@ def add_diag(orig_line_n, diag_s, diag_category, lines, orig_lines, prefix): else: new_line_n = prev_line.line_n assert new_line_n == line_n + (not reverse) - total_offset + return (prev_line, total_offset, new_line_n) + + +def add_diag( + orig_target_line_n, + col, + diag_s, + diag_category, + lines, + orig_lines, + prefix, + nested_context, +): + if nested_context: + prev_line = None + for line in lines: + if line.diag and line.diag.absolute_target() < orig_target_line_n: + prev_line = line + if prev_line: + new_line_n = prev_line.line_n + 1 + else: + prev_line = nested_context.line + new_line_n = 1 + else: + line_n = orig_line_n_to_new_line_n(orig_target_line_n, orig_lines) + target = lines[line_n - 1] - new_line = Line(get_indent(prev_line.content) + "{{DIAG}}\n", new_line_n) + prev_line, total_offset, new_line_n = infer_line_context(target, line_n) + indent = get_indent(prev_line.content) + new_line = Line(indent + "{{DIAG}}\n", new_line_n) add_line(new_line, lines) - whitespace_strings = prev_line.diag.whitespace_strings if prev_line.diag else None + whitespace_strings = None + if prev_line.diag: + whitespace_strings = ( + prev_line.diag.whitespace_strings.copy() + if prev_line.diag.whitespace_strings + else None + ) + if prev_line.diag == nested_context: + if not whitespace_strings: + whitespace_strings = [" ", "", ""] + whitespace_strings[0] += " " + new_diag = Diag( prefix, diag_s, diag_category, - total_offset, - False, + orig_target_line_n if nested_context else total_offset, + bool(nested_context), + col, 1, new_line, False, whitespace_strings, False, + [], ) new_line.diag = new_diag - new_diag.set_target(target) + if not nested_context: + new_diag.set_target(target) + return new_diag def remove_dead_diags(lines): - for line in lines: - if not line.diag or line.diag.count != 0: + for line in lines.copy(): + if not line.diag: + continue + if line.diag.category == "expansion": + remove_dead_diags(line.diag.nested_lines) + if line.diag.nested_lines: + line.diag.count = 1 + else: + line.diag.count = 0 + if 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: + for other_diag in line.diag.target.targeting_diags: if ( other_diag.is_from_source_file or other_diag.count == 0 @@ -318,57 +409,151 @@ def remove_dead_diags(lines): continue line.diag.take(other_diag) remove_line(other_diag.line, lines) + break -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") +def fold_expansions(lines): + i = 0 + while i < len(lines): + line = lines[i] + if not line.diag or not line.diag.parent: + i += 1 + continue + remove_line(line, lines) + if line.diag.category == "closing": + line.diag.parent.closer = line + else: + line.line_n = len(line.diag.parent.nested_lines) + add_line(line, line.diag.parent.nested_lines) + + +def expand_expansions(lines): + i = 0 + while i < len(lines): + line = lines[i] + if not line.diag or line.diag.category != "expansion": + i += 1 + continue + for j, nested in enumerate(line.diag.nested_lines + [line.diag.closer]): + nested.line_n = line.line_n + j + 1 + add_line(nested, lines) + i += 1 + + +def error_refers_to_diag(diag_error, diag, target_line_n): + if diag_error.col and diag.col() and diag_error.col != diag.col(): + return False + return ( + target_line_n == diag.absolute_target() + and diag_error.category == diag.category + and (diag.category == "expansion" or diag_error.content == diag.diag_content) + ) + + +def find_other_targeting(lines, orig_lines, is_nested, diag_error): + if is_nested: + other_diags = [ + line.diag + for line in lines + if line.diag + and error_refers_to_diag(diag_error, line.diag, diag_error.line) + ] 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) + target = orig_lines[diag_error.line - 1] + other_diags = [ + d + for d in target.targeting_diags + if error_refers_to_diag(diag_error, d, target.line_n) + ] + return other_diags - for line in lines: - diag = parse_diag(line, filename, prefix) - if diag: - line.diag = diag - diag.set_target(lines[diag.absolute_target() - 1]) +def update_lines(diag_errors, lines, orig_lines, prefix, filename, nested_context): for diag_error in diag_errors: if not isinstance(diag_error, NotFoundDiag): continue - # this is a diagnostic expected but not seen line_n = diag_error.line - assert lines[line_n - 1].diag - if not lines[line_n - 1].diag or diag_error.content != lines[line_n - 1].diag.diag_content: + line = orig_lines[line_n - 1] + assert line.diag or nested_context + if not line.diag or diag_error.content != line.diag.diag_content: raise KnownException( - f"{filename}:{line_n} - found diag {lines[line_n - 1].diag.diag_content} but expected {diag_error.content}" + f"{filename}:{line_n} - found diag {line.diag.diag_content} but expected {diag_error.content}" ) - if diag_error.category != lines[line_n - 1].diag.category: + if diag_error.category != line.diag.category: raise KnownException( - f"{filename}:{line_n} - found {lines[line_n - 1].diag.category} diag but expected {diag_error.category}" + f"{filename}:{line_n} - found {line.diag.category} diag but expected {diag_error.category}" ) - lines[line_n - 1].diag.decrement_count() + line.diag.decrement_count() - diag_errors.sort(reverse=True, key=lambda t: t.line) + diag_errors.sort(reverse=True, key=lambda diag_error: diag_error.line) for diag_error in diag_errors: - if not isinstance(diag_error, ExtraDiag): + if not isinstance(diag_error, ExtraDiag) and not isinstance( + diag_error, NestedDiag + ): continue - line_n = diag_error.line - target = orig_lines[line_n - 1] - other_diags = [ - d - for d in target.targeting_diags - if d.diag_content == diag_error.content and d.category == diag_error.category - ] - other_diag = other_diags[0] if other_diags else None - if other_diag: - other_diag.increment_count() + other_diags = find_other_targeting( + lines, orig_lines, bool(nested_context), diag_error + ) + diag = other_diags[0] if other_diags else None + if diag: + diag.increment_count() else: - add_diag(line_n, diag_error.content, diag_error.category, lines, orig_lines, diag_error.prefix) + diag = add_diag( + diag_error.line, + diag_error.col, + diag_error.content, + diag_error.category, + lines, + orig_lines, + diag_error.prefix, + nested_context, + ) + if isinstance(diag_error, NestedDiag): + if not diag.closer: + whitespace = ( + diag.whitespace_strings[0] if diag.whitespace_strings else " " + ) + diag.closer = Line( + get_indent(diag.line.content) + "//" + whitespace + "}}\n", None + ) + update_lines( + [diag_error.nested], + diag.nested_lines, + orig_lines, + prefix, + diag_error.file, + diag, + ) + + +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) + + expansion_context = [] + for line in lines: + diag = parse_diag(line, filename, prefix) + if diag: + line.diag = diag + if expansion_context: + diag.parent = expansion_context[-1] + else: + diag.set_target(lines[diag.absolute_target() - 1]) + if diag.category == "expansion": + expansion_context.append(diag) + elif diag.category == "closing": + expansion_context.pop() + + fold_expansions(lines) + update_lines(diag_errors, lines, orig_lines, prefix, filename, None) remove_dead_diags(lines) + expand_expansions(lines) with open(filename, "w") as f: for line in lines: f.write(line.render()) @@ -410,7 +595,9 @@ def update_test_files(errors, prefix): a = 2 ^ """ -diag_error_re2 = re.compile(r"(\S+):(\d+):(\d+): error: unexpected (\S+) produced: (.*)") +diag_error_re2 = re.compile( + r"(\S+):(\d+):(\d+): error: unexpected (\S+) produced: (.*)" +) """ @@ -432,6 +619,14 @@ def update_test_files(errors, prefix): """ diag_error_re4 = re.compile(r"(\S+):(\d+):(\d+): error: expected (\S+), not (\S+)") +""" +ex: +test.swift:12:14: note: in expansion from here +func foo() {} + ^ +""" +diag_expansion_note_re = re.compile(r"(\S+):(\d+):(\d+): note: in expansion from here") + class NotFoundDiag: def __init__(self, file, line, col, category, content, prefix): @@ -459,51 +654,141 @@ def __str__(self): return f"{self.file}:{self.line}:{self.col}: error unexpected {self.category} produced: {self.content}" +class NestedDiag: + def __init__(self, file, line, col, nested): + self.file = file + self.line = line + self.col = col + self.category = "expansion" + self.content = None + self.nested = nested + self.prefix = "" + + def __str__(self): + return f""" +{self.file}:{self.line}:{self.col}: note: in expansion from here ( + {self.nested} +) +""" + + 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 = [] + top_level = [] try: i = 0 while i < len(tool_output): line = tool_output[i].strip() + curr = [] if not "error:" in line: pass elif m := diag_error_re.match(line): - diag = parse_diag(Line(tool_output[i+1], int(m.group(2))), m.group(1), prefix) + diag = parse_diag( + Line(tool_output[i + 1], int(m.group(2))), m.group(1), prefix + ) i += 2 - curr.append(NotFoundDiag(m.group(1), int(m.group(2)), int(m.group(3)), m.group(4), diag.diag_content, diag.prefix)) + curr.append( + NotFoundDiag( + m.group(1), + int(m.group(2)), + int(m.group(3)), + m.group(4), + diag.diag_content, + diag.prefix, + ) + ) elif m := diag_error_re2.match(line): - curr.append(ExtraDiag(m.group(1), int(m.group(2)), int(m.group(3)), m.group(4), m.group(5), prefix)) + curr.append( + ExtraDiag( + m.group(1), + int(m.group(2)), + int(m.group(3)), + m.group(4), + m.group(5), + prefix, + ) + ) i += 2 # Create two mirroring mismatches when the compiler reports that the category or diagnostic is incorrect. # This makes it easier to handle cases where the same diagnostic is mentioned both in an incorrect message/category # diagnostic, as well as in an error not produced diagnostic. This can happen for things like 'expected-error 2{{foo}}' # if only one diagnostic is emitted on that line, and the content of that diagnostic is actually 'bar'. elif m := diag_error_re3.match(line): - diag = parse_diag(Line(tool_output[i+1], int(m.group(2))), m.group(1), prefix) - curr.append(NotFoundDiag(m.group(1), int(m.group(2)), int(m.group(3)), diag.category, diag.diag_content, diag.prefix)) - curr.append(ExtraDiag(m.group(1), diag.absolute_target(), int(m.group(3)), diag.category, tool_output[i+3].strip(), diag.prefix)) + diag = parse_diag( + Line(tool_output[i + 1], int(m.group(2))), m.group(1), prefix + ) + curr.append( + NotFoundDiag( + m.group(1), + int(m.group(2)), + int(m.group(3)), + diag.category, + diag.diag_content, + diag.prefix, + ) + ) + curr.append( + ExtraDiag( + m.group(1), + diag.absolute_target(), + int(m.group(3)), + diag.category, + tool_output[i + 3].strip(), + diag.prefix, + ) + ) i += 3 elif m := diag_error_re4.match(line): - diag = parse_diag(Line(tool_output[i+1], int(m.group(2))), m.group(1), prefix) + diag = parse_diag( + Line(tool_output[i + 1], int(m.group(2))), m.group(1), prefix + ) assert diag.category == m.group(4) - assert tool_output[i+3].strip() == m.group(5) - curr.append(NotFoundDiag(m.group(1), int(m.group(2)), int(m.group(3)), diag.category, diag.diag_content, diag.prefix)) - curr.append(ExtraDiag(m.group(1), diag.absolute_target(), int(m.group(3)), m.group(5), diag.diag_content, diag.prefix)) + assert tool_output[i + 3].strip() == m.group(5) + curr.append( + NotFoundDiag( + m.group(1), + int(m.group(2)), + int(m.group(3)), + diag.category, + diag.diag_content, + diag.prefix, + ) + ) + curr.append( + ExtraDiag( + m.group(1), + diag.absolute_target(), + int(m.group(3)), + m.group(5), + diag.diag_content, + diag.prefix, + ) + ) i += 3 else: dprint("no match") dprint(line.strip()) i += 1 + while ( + curr + and i < len(tool_output) + and (m := diag_expansion_note_re.match(tool_output[i].strip())) + ): + curr = [ + NestedDiag(m.group(1), int(m.group(2)), int(m.group(3)), e) + for e in curr + ] + i += 3 + top_level.extend(curr) + except KnownException as e: return (1, f"Error in update-verify-tests while parsing tool output: {e}") - if curr: - return (0, update_test_files(curr, prefix)) + if top_level: + return (0, update_test_files(top_level, prefix)) else: return (1, "no mismatching diagnostics found") -