From 52c4bc2fa925fea2e5c51249ebe9a9934cb91c22 Mon Sep 17 00:00:00 2001 From: Stuart Montgomery Date: Wed, 10 Jul 2024 12:45:38 -0500 Subject: [PATCH 1/3] Include try and await expressions which are parents of a freestanding expression macro in lexicalContext Resolves rdar://109470248 --- .../SwiftSyntaxMacros/Syntax+LexicalContext.swift | 14 ++++++++++++++ .../LexicalContextTests.swift | 8 +++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift index 5f17e391db4..96110ce5803 100644 --- a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift +++ b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift @@ -67,6 +67,20 @@ extension SyntaxProtocol { case let freestandingMacro as FreestandingMacroExpansionSyntax: return Syntax(freestandingMacro.detached) as Syntax + // Try and await are preserved: A freestanding expression macro preceded + // by try or await may need to know whether those keywords are present so it + // can propagate them to any expressions in its expansion which were passed + // as arguments to the macro. The expression of the try or await is replaced + // with a trivial placeholder, though. + case var tryExpr as TryExprSyntax: + tryExpr = tryExpr.detached + tryExpr.expression = ExprSyntax(TypeExprSyntax(type: IdentifierTypeSyntax(name: .wildcardToken()))) + return Syntax(tryExpr) + case var awaitExpr as AwaitExprSyntax: + awaitExpr = awaitExpr.detached + awaitExpr.expression = ExprSyntax(TypeExprSyntax(type: IdentifierTypeSyntax(name: .wildcardToken()))) + return Syntax(awaitExpr) + default: return nil } diff --git a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift index 533a2a4d19f..6b426aa7431 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -531,7 +531,7 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - #lexicalContextDescription + try await #lexicalContextDescription } } return S(arg: c) @@ -542,7 +542,9 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - """ + try await """ + await _ + try _ contextDescription: String struct S {} { c in @@ -551,7 +553,7 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - #lexicalContextDescription + try await #lexicalContextDescription } } return S(arg: c) From 2b9a763eab621d8874122f670e0fd2367754053a Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Tue, 1 Apr 2025 21:02:09 +0100 Subject: [PATCH 2/3] Handle `try` and `await` in unfolded sequences We can say that any `try`/`await` element also covers all elements to the right of it in an unfolded sequence. Cases where this isn't true will be rejected by the compiler, e.g: ``` 0 * try foo() + bar() _ = try foo() ~~~ bar() // Assuming `~~~` has lower precedence than `=` ``` rdar://109470248 --- .../OperatorTable+Folding.swift | 2 + .../Syntax+LexicalContext.swift | 33 ++++ .../LexicalContextTests.swift | 161 ++++++++++++++++++ 3 files changed, 196 insertions(+) diff --git a/Sources/SwiftOperators/OperatorTable+Folding.swift b/Sources/SwiftOperators/OperatorTable+Folding.swift index 06371c9f50a..c1c4f7f6025 100644 --- a/Sources/SwiftOperators/OperatorTable+Folding.swift +++ b/Sources/SwiftOperators/OperatorTable+Folding.swift @@ -155,6 +155,8 @@ extension OperatorTable { ) ) } + // NOTE: If you add a new try/await/unsafe-like hoisting case here, make + // sure to also update `allMacroLexicalContexts` to handle it. // The form of the binary operation depends on the operator itself, // which will be one of the unresolved infix operators. diff --git a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift index 96110ce5803..6de517bee35 100644 --- a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift +++ b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift @@ -106,6 +106,39 @@ extension SyntaxProtocol { if let parentContext = parentNode.asMacroLexicalContext() { parentContexts.append(parentContext) } + if let sequence = parentNode.as(SequenceExprSyntax.self) { + var sequenceExprContexts: [Syntax] = [] + for elt in sequence.elements { + if elt.range.contains(self.position) { + // `sequenceExprContexts` is built from the top-down, but we + // build the rest of the contexts bottom-up. Reverse for + // consistency. + parentContexts += sequenceExprContexts.reversed() + break + } + var elt = elt + while true { + if let tryElt = elt.as(TryExprSyntax.self) { + sequenceExprContexts.append(tryElt.asMacroLexicalContext()!) + elt = tryElt.expression + continue + } + if let awaitElt = elt.as(AwaitExprSyntax.self) { + sequenceExprContexts.append(awaitElt.asMacroLexicalContext()!) + elt = awaitElt.expression + continue + } + if let unsafeElt = elt.as(UnsafeExprSyntax.self) { + // No scope for this currently, but we need to look through it + // since it's similar to 'try' in that it's hoisted above a + // binary operator when appearing on the LHS. + elt = unsafeElt.expression + continue + } + break + } + } + } currentNode = parentNode } diff --git a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift index 6b426aa7431..2e1b5c8399c 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -567,4 +567,165 @@ final class LexicalContextTests: XCTestCase { macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) } + + func testTryAwaitInSequenceLexicalContext() { + // Valid cases. + assertMacroExpansion( + "try await #lexicalContextDescription + #lexicalContextDescription", + expandedSource: #""" + try await """ + await _ + try _ + """ + """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + assertMacroExpansion( + "try await 0 + 1 + foo(#lexicalContextDescription) + 2", + expandedSource: #""" + try await 0 + 1 + foo(""" + await _ + try _ + """) + 2 + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + assertMacroExpansion( + "x = try await 0 + 1 + foo(#lexicalContextDescription) + 2", + expandedSource: #""" + x = try await 0 + 1 + foo(""" + await _ + try _ + """) + 2 + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // `try await` in the 'then' branch doesn't cover condition or else. + assertMacroExpansion( + "#lexicalContextDescription ? try await #lexicalContextDescription : #lexicalContextDescription", + expandedSource: #""" + """ + """ ? try await """ + await _ + try _ + """ : """ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // Same for else. + assertMacroExpansion( + "#lexicalContextDescription ? #lexicalContextDescription : try await #lexicalContextDescription", + expandedSource: #""" + """ + """ ? """ + """ : try await """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // 'try await' in the condition here covers the entire expression + assertMacroExpansion( + "try await #lexicalContextDescription ? #lexicalContextDescription : #lexicalContextDescription ~~ #lexicalContextDescription", + expandedSource: #""" + try await """ + await _ + try _ + """ ? """ + await _ + try _ + """ : """ + await _ + try _ + """ ~~ """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + assertMacroExpansion( + "x = unsafe try try! await 0 + #lexicalContextDescription", + expandedSource: #""" + x = unsafe try try! await 0 + """ + await _ + try! _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + + // Invalid cases + assertMacroExpansion( + "0 + try await #lexicalContextDescription", + expandedSource: #""" + 0 + try await """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // The 'try await' may not actually cover 'lexicalContextDescription' here, + // but this will be rejected by the compiler. + assertMacroExpansion( + "0 + try await 1 ^ #lexicalContextDescription", + expandedSource: #""" + 0 + try await 1 ^ """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // Invalid if '^' has a lower precedence than '='. + assertMacroExpansion( + "x = try await 0 ^ #lexicalContextDescription", + expandedSource: #""" + x = try await 0 ^ """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + // Unassignable + assertMacroExpansion( + "#lexicalContextDescription = try await 0 + 1", + expandedSource: #""" + """ + """ = try await 0 + 1 + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + assertMacroExpansion( + "try await #lexicalContextDescription = 0 + #lexicalContextDescription", + expandedSource: #""" + try await """ + await _ + try _ + """ = 0 + """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + assertMacroExpansion( + "try await foo() ? 0 : 1 = #lexicalContextDescription", + expandedSource: #""" + try await foo() ? 0 : 1 = """ + await _ + try _ + """ + """#, + macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] + ) + } } From 2c16aebd00ebe17b1f99b0cb6655b914249fed2f Mon Sep 17 00:00:00 2001 From: Hamish Knight Date: Thu, 3 Apr 2025 20:39:03 +0100 Subject: [PATCH 3/3] Add `unsafe` as a lexical context too Match the behavior of `try` and `await`. --- .../Syntax+LexicalContext.swift | 24 ++++-- .../LexicalContextTests.swift | 82 +++++++++++-------- 2 files changed, 66 insertions(+), 40 deletions(-) diff --git a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift index 6de517bee35..f9132040dff 100644 --- a/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift +++ b/Sources/SwiftSyntaxMacros/Syntax+LexicalContext.swift @@ -67,11 +67,11 @@ extension SyntaxProtocol { case let freestandingMacro as FreestandingMacroExpansionSyntax: return Syntax(freestandingMacro.detached) as Syntax - // Try and await are preserved: A freestanding expression macro preceded - // by try or await may need to know whether those keywords are present so it - // can propagate them to any expressions in its expansion which were passed - // as arguments to the macro. The expression of the try or await is replaced - // with a trivial placeholder, though. + // `try`, `await`, and `unsafe` are preserved: A freestanding expression + // macro may need to know whether those keywords are present so it can + // propagate them to any expressions in its expansion which were passed as + // arguments to the macro. The sub-expression is replaced with a trivial + // placeholder, though. case var tryExpr as TryExprSyntax: tryExpr = tryExpr.detached tryExpr.expression = ExprSyntax(TypeExprSyntax(type: IdentifierTypeSyntax(name: .wildcardToken()))) @@ -80,6 +80,10 @@ extension SyntaxProtocol { awaitExpr = awaitExpr.detached awaitExpr.expression = ExprSyntax(TypeExprSyntax(type: IdentifierTypeSyntax(name: .wildcardToken()))) return Syntax(awaitExpr) + case var unsafeExpr as UnsafeExprSyntax: + unsafeExpr = unsafeExpr.detached + unsafeExpr.expression = ExprSyntax(TypeExprSyntax(type: IdentifierTypeSyntax(name: .wildcardToken()))) + return Syntax(unsafeExpr) default: return nil @@ -106,6 +110,12 @@ extension SyntaxProtocol { if let parentContext = parentNode.asMacroLexicalContext() { parentContexts.append(parentContext) } + // Unfolded sequence expressions require special handling - effect marker + // nodes like `try`, `await`, and `unsafe` are treated as lexical contexts + // for all the nodes on their right. Cases where they don't end up + // covering nodes to their right in the folded tree are invalid and will + // be diagnosed by the compiler. This matches the compiler's ASTScope + // handling logic. if let sequence = parentNode.as(SequenceExprSyntax.self) { var sequenceExprContexts: [Syntax] = [] for elt in sequence.elements { @@ -129,9 +139,7 @@ extension SyntaxProtocol { continue } if let unsafeElt = elt.as(UnsafeExprSyntax.self) { - // No scope for this currently, but we need to look through it - // since it's similar to 'try' in that it's hoisted above a - // binary operator when appearing on the LHS. + sequenceExprContexts.append(unsafeElt.asMacroLexicalContext()!) elt = unsafeElt.expression continue } diff --git a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift index 2e1b5c8399c..e0cbe3ef9e2 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/LexicalContextTests.swift @@ -531,7 +531,7 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - try await #lexicalContextDescription + unsafe try await #lexicalContextDescription } } return S(arg: c) @@ -542,9 +542,10 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - try await """ + unsafe try await """ await _ try _ + unsafe _ contextDescription: String struct S {} { c in @@ -553,7 +554,7 @@ final class LexicalContextTests: XCTestCase { struct S { let arg: C var contextDescription: String { - try await #lexicalContextDescription + unsafe try await #lexicalContextDescription } } return S(arg: c) @@ -568,49 +569,54 @@ final class LexicalContextTests: XCTestCase { ) } - func testTryAwaitInSequenceLexicalContext() { + func testEffectMarkersInSequenceLexicalContext() { // Valid cases. assertMacroExpansion( - "try await #lexicalContextDescription + #lexicalContextDescription", + "unsafe try await #lexicalContextDescription + #lexicalContextDescription", expandedSource: #""" - try await """ + unsafe try await """ await _ try _ + unsafe _ """ + """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) assertMacroExpansion( - "try await 0 + 1 + foo(#lexicalContextDescription) + 2", + "try unsafe await 0 + 1 + foo(#lexicalContextDescription) + 2", expandedSource: #""" - try await 0 + 1 + foo(""" + try unsafe await 0 + 1 + foo(""" await _ + unsafe _ try _ """) + 2 """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) assertMacroExpansion( - "x = try await 0 + 1 + foo(#lexicalContextDescription) + 2", + "x = try await unsafe 0 + 1 + foo(#lexicalContextDescription) + 2", expandedSource: #""" - x = try await 0 + 1 + foo(""" + x = try await unsafe 0 + 1 + foo(""" + unsafe _ await _ try _ """) + 2 """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) - // `try await` in the 'then' branch doesn't cover condition or else. + // `unsafe try await` in the 'then' branch doesn't cover condition or else. assertMacroExpansion( - "#lexicalContextDescription ? try await #lexicalContextDescription : #lexicalContextDescription", + "#lexicalContextDescription ? unsafe try await #lexicalContextDescription : #lexicalContextDescription", expandedSource: #""" """ - """ ? try await """ + """ ? unsafe try await """ await _ try _ + unsafe _ """ : """ """ """#, @@ -618,33 +624,38 @@ final class LexicalContextTests: XCTestCase { ) // Same for else. assertMacroExpansion( - "#lexicalContextDescription ? #lexicalContextDescription : try await #lexicalContextDescription", + "#lexicalContextDescription ? #lexicalContextDescription : unsafe try await #lexicalContextDescription", expandedSource: #""" """ """ ? """ - """ : try await """ + """ : unsafe try await """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) - // 'try await' in the condition here covers the entire expression + // 'unsafe try await' in the condition here covers the entire expression assertMacroExpansion( - "try await #lexicalContextDescription ? #lexicalContextDescription : #lexicalContextDescription ~~ #lexicalContextDescription", + "unsafe try await #lexicalContextDescription ? #lexicalContextDescription : #lexicalContextDescription ~~ #lexicalContextDescription", expandedSource: #""" - try await """ + unsafe try await """ await _ try _ + unsafe _ """ ? """ await _ try _ + unsafe _ """ : """ await _ try _ + unsafe _ """ ~~ """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] @@ -656,6 +667,7 @@ final class LexicalContextTests: XCTestCase { await _ try! _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] @@ -663,66 +675,72 @@ final class LexicalContextTests: XCTestCase { // Invalid cases assertMacroExpansion( - "0 + try await #lexicalContextDescription", + "0 + unsafe try await #lexicalContextDescription", expandedSource: #""" - 0 + try await """ + 0 + unsafe try await """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) - // The 'try await' may not actually cover 'lexicalContextDescription' here, - // but this will be rejected by the compiler. + // The `unsafe try await` may not actually cover `lexicalContextDescription` + // here, but this will be rejected by the compiler. assertMacroExpansion( - "0 + try await 1 ^ #lexicalContextDescription", + "0 + unsafe try await 1 ^ #lexicalContextDescription", expandedSource: #""" - 0 + try await 1 ^ """ + 0 + unsafe try await 1 ^ """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) // Invalid if '^' has a lower precedence than '='. assertMacroExpansion( - "x = try await 0 ^ #lexicalContextDescription", + "x = unsafe try await 0 ^ #lexicalContextDescription", expandedSource: #""" - x = try await 0 ^ """ + x = unsafe try await 0 ^ """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) // Unassignable assertMacroExpansion( - "#lexicalContextDescription = try await 0 + 1", + "#lexicalContextDescription = unsafe try await 0 + 1", expandedSource: #""" """ - """ = try await 0 + 1 + """ = unsafe try await 0 + 1 """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) assertMacroExpansion( - "try await #lexicalContextDescription = 0 + #lexicalContextDescription", + "unsafe try await #lexicalContextDescription = 0 + #lexicalContextDescription", expandedSource: #""" - try await """ + unsafe try await """ await _ try _ + unsafe _ """ = 0 + """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self] ) assertMacroExpansion( - "try await foo() ? 0 : 1 = #lexicalContextDescription", + "unsafe try await foo() ? 0 : 1 = #lexicalContextDescription", expandedSource: #""" - try await foo() ? 0 : 1 = """ + unsafe try await foo() ? 0 : 1 = """ await _ try _ + unsafe _ """ """#, macros: ["lexicalContextDescription": LexicalContextDescriptionMacro.self]