From 357c298940fdf2d2f41f71fbc525973ecdc4cfdf Mon Sep 17 00:00:00 2001 From: "Henrik G. Olsson" Date: Fri, 14 Nov 2025 22:16:50 -0800 Subject: [PATCH] [utils] escape backslashes in diagnostics strings DiagnosticVerifier lexes the contents of expected diagnostics the same way Swift parses string literals, to allow for escape codes. This makes for problems when backslashes are followed by something that makes for a valid escape. In particular, it reaches an llvm_unreachable if it encounters `\(`, which would create a string interpolation in a string literal. --- .../Utils/update-verify-tests/expansion.swift | 45 +++++++++++++++++++ utils/update_verify_tests/core.py | 10 ++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/test/Utils/update-verify-tests/expansion.swift b/test/Utils/update-verify-tests/expansion.swift index 82d0b7318c2..bb29d80fc37 100644 --- a/test/Utils/update-verify-tests/expansion.swift +++ b/test/Utils/update-verify-tests/expansion.swift @@ -42,6 +42,11 @@ // RUN: not %target-swift-frontend-verify -I %t -plugin-path %swift-plugin-dir -typecheck %t/unparsed.swift 2>%t/output.txt -Rmacro-expansions // RUN: not %update-verify-tests < %t/output.txt | %FileCheck --check-prefix CHECK-UNPARSED %s +// RUN: not %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/escaped.swift 2>%t/output.txt -Rmacro-expansions +// RUN: %update-verify-tests < %t/output.txt +// RUN: %target-swift-frontend-verify -load-plugin-library %t/%target-library-name(UnstringifyMacroDefinition) -typecheck %t/escaped.swift -Rmacro-expansions +// RUN: %diff %t/escaped.swift %t/escaped.swift.expected + //--- single.swift @attached(peer, names: overloaded) macro unstringifyPeer(_ s: String) = @@ -260,3 +265,43 @@ func bar() { foo(a, &b) } +//--- escaped.swift +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +@unstringifyPeer(""" +func foo(_ x: Int) { + let a = "\\(x)" + let b = "\\(x)" +} +""") +// NB: DiagnosticVerifier interprets "\\(x)" as "\(x)" +// expected-expansion@+3:30{{ +// expected-remark@2{{macro content: |let a = "\\(x)"|}} +// }} +func foo() { let _ = "\(2)" } + +//--- escaped.swift.expected +@attached(peer, names: overloaded) +macro unstringifyPeer(_ s: String) = + #externalMacro(module: "UnstringifyMacroDefinition", type: "UnstringifyPeerMacro") + +// expected-note@+1 6{{in expansion of macro 'unstringifyPeer' on global function 'foo()' here}} +@unstringifyPeer(""" +func foo(_ x: Int) { + let a = "\\(x)" + let b = "\\(x)" +} +""") +// NB: DiagnosticVerifier interprets "\\(x)" as "\(x)" +// expected-expansion@+8:30{{ +// expected-remark@1{{macro content: |func foo(_ x: Int) {|}} +// expected-warning@2{{initialization of immutable value 'a' was never used; consider replacing with assignment to '_' or removing it}} +// expected-remark@2{{macro content: | let a = "\\(x)"|}} +// expected-warning@3{{initialization of immutable value 'b' was never used; consider replacing with assignment to '_' or removing it}} +// expected-remark@3{{macro content: | let b = "\\(x)"|}} +// expected-remark@4{{macro content: |}|}} +// }} +func foo() { let _ = "\(2)" } + diff --git a/utils/update_verify_tests/core.py b/utils/update_verify_tests/core.py index 79d6325bb0b..fe636deb4e3 100644 --- a/utils/update_verify_tests/core.py +++ b/utils/update_verify_tests/core.py @@ -1,5 +1,6 @@ import sys import re +from codecs import encode, decode DEBUG = False @@ -172,7 +173,9 @@ class Diag: if self.category == "expansion": return base_s + "{{" else: - return base_s + "{{" + self.diag_content + "}}" + # python trivia: raw strings can't end with a backslash + escaped_diag_s = self.diag_content.replace("\\", "\\\\") + return base_s + "{{" + escaped_diag_s + "}}" class ExpansionDiagClose: @@ -245,9 +248,12 @@ def parse_diag(line, filename, prefix): count = int(count_s) if count_s else 1 line.content = matched_re.sub("{{DIAG}}", s) + unescaped_diag_s = decode( + encode(diag_s, "utf-8", "backslashreplace"), "unicode-escape" + ) return Diag( check_prefix, - diag_s, + unescaped_diag_s, category_s, target_line_n, is_absolute,