diff --git a/Sources/TestingMacros/ConditionMacro.swift b/Sources/TestingMacros/ConditionMacro.swift index b7558145a..5aed1ab6b 100644 --- a/Sources/TestingMacros/ConditionMacro.swift +++ b/Sources/TestingMacros/ConditionMacro.swift @@ -190,15 +190,7 @@ extension ConditionMacro { expandedFunctionName = .identifier("__checkConditionAsync") } - var expressionContextName = TokenSyntax.identifier("__ec") - let isNameUsed = originalArgumentExpr.tokens(viewMode: .sourceAccurate).lazy - .map(\.tokenKind) - .contains(expressionContextName.tokenKind) - if isNameUsed { - // BUG: We should use the unique name directly. SEE: swift-syntax-#2256 - let uniqueName = context.makeUniqueName("") - expressionContextName = .identifier("\(expressionContextName)\(uniqueName)") - } + let expressionContextName = context.makeUniqueClosureParameterName("__ec", in: originalArgumentExpr) let (closureExpr, rewrittenNodes) = rewrite( originalArgumentExpr, usingExpressionContextNamed: expressionContextName, diff --git a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift index 8bcf2522a..3de95d365 100644 --- a/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/MacroExpansionContextAdditions.swift @@ -75,6 +75,33 @@ extension MacroExpansionContext { return makeUniqueName("\(prefix)\(suffix)") } + + /// Generate a unique name for use in the macro as a closure parameter. + /// + /// - Parameters: + /// - name: The name to use as a basis for the uniquely-generated name. + /// - node: A syntax node within which `name` must be unique. + /// + /// - Returns: an identifier token containing a unique name suitable for use + /// as a closure parameter. + func makeUniqueClosureParameterName(_ name: String, in node: some SyntaxProtocol) -> TokenSyntax { + precondition(name.isValidSwiftIdentifier(for: .variableName)) + var result = TokenSyntax.identifier(name) + + func isNameUsed(_ name: TokenSyntax) -> Bool { + node.tokens(viewMode: .sourceAccurate).lazy + .map(\.tokenKind) + .contains(name.tokenKind) + } + + var suffix = 0 + while isNameUsed(result) { + defer { suffix += 1 } + result = .identifier("\(name)\(suffix)") + } + + return result + } } // MARK: - diff --git a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift index c593a7c0a..ff248fc27 100644 --- a/Sources/TestingMacros/Support/ConditionArgumentParsing.swift +++ b/Sources/TestingMacros/Support/ConditionArgumentParsing.swift @@ -444,14 +444,35 @@ private final class _ContextInserter: SyntaxRewriter where C: MacroExpansi if let op = node.operator.as(BinaryOperatorExprSyntax.self)?.operator.textWithoutBackticks, op == "==" || op == "!=" || op == "===" || op == "!==" { + let lhsName = context.makeUniqueClosureParameterName("lhs", in: effectiveRootNode) + let rhsName = context.makeUniqueClosureParameterName("rhs", in: effectiveRootNode) return _rewrite( - ClosureExprSyntax { + ClosureExprSyntax( + signature: ClosureSignatureSyntax( + leadingTrivia: .space, + parameterClause: .simpleInput( + ClosureShorthandParameterListSyntax { + ClosureShorthandParameterSyntax(name: lhsName) + ClosureShorthandParameterSyntax(name: rhsName) + } + ), + returnClause: ReturnClauseSyntax( + leadingTrivia: .space, + type: MemberTypeSyntax( + leadingTrivia: .space, + baseType: IdentifierTypeSyntax(name: .identifier("Swift")), + name: .identifier("Bool") + ), + trailingTrivia: .space + ), + inKeyword: .keyword(.in), + trailingTrivia: .space + ) + ) { InfixOperatorExprSyntax( - leftOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$0")) - .with(\.trailingTrivia, .space), + leftOperand: DeclReferenceExprSyntax(baseName: lhsName, trailingTrivia: .space), operator: BinaryOperatorExprSyntax(text: op), - rightOperand: DeclReferenceExprSyntax(baseName: .dollarIdentifier("$1")) - .with(\.leadingTrivia, .space) + rightOperand: DeclReferenceExprSyntax(leadingTrivia: .space, baseName: rhsName, trailingTrivia: .space) ) }, originalWas: node, diff --git a/Tests/SubexpressionShowcase/SubexpressionShowcase.swift b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift index 1a8cf2568..b689dd705 100644 --- a/Tests/SubexpressionShowcase/SubexpressionShowcase.swift +++ b/Tests/SubexpressionShowcase/SubexpressionShowcase.swift @@ -42,9 +42,6 @@ func subexpressionShowcase() async throws { #expect(false || true) #expect((fff == ttt) == ttt) - Testing.__checkCondition({(__ec: Testing.__ExpectationContext) -> Swift.Bool in - __ec.__cmp(==,0x0,__ec((__ec.__cmp(==,0x3a,__ec(fff,0x7a),0x7a,__ec(ttt,0x43a),0x43a)),0x2),0x2,__ec(ttt,0x8000),0x8000) - },sourceCode: [0x0:"(fff == ttt) == ttt",0x2:"(fff == ttt)",0x3a:"fff == ttt",0x7a:"fff",0x43a:"ttt",0x8000:"ttt"],comments: [],isRequired: false,sourceLocation: Testing.SourceLocation.__here()).__expected() #expect((Int)(123) == 124) #expect((Int, Double)(123, 456.0) == (124, 457.0)) diff --git a/Tests/TestingMacrosTests/ConditionMacroTests.swift b/Tests/TestingMacrosTests/ConditionMacroTests.swift index 73483d02c..6e1a5f082 100644 --- a/Tests/TestingMacrosTests/ConditionMacroTests.swift +++ b/Tests/TestingMacrosTests/ConditionMacroTests.swift @@ -35,7 +35,7 @@ struct ConditionMacroTests { ##"#expect(9 > 8 && 7 > 6, "Some comment")"##: ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec(__ec(9 > 8, 0x2) && __ec(7 > 6, 0x400), 0x0) }, sourceCode: [0x0: "9 > 8 && 7 > 6", 0x2: "9 > 8", 0x400: "7 > 6"], comments: ["Some comment"], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect("a" == "b")"##: - ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec.__cmp({ $0 == $1 }, 0x0, "a", 0x2, "b", 0x200) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec.__cmp({ lhs, rhs -> Swift.Bool in lhs == rhs }, 0x0, "a", 0x2, "b", 0x200) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect(!Bool.random())"##: ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec(!Bool.random(), 0x0) }, sourceCode: [0x0: "!Bool.random()"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, ##"#expect((true && false))"##: @@ -116,7 +116,7 @@ struct ConditionMacroTests { ##"#require(9 > 8 && 7 > 6, "Some comment")"##: ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(__ec(9 > 8, 0x2) && __ec(7 > 6, 0x400), 0x0)) }, sourceCode: [0x0: "9 > 8 && 7 > 6", 0x2: "9 > 8", 0x400: "7 > 6"], comments: ["Some comment"], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require("a" == "b")"##: - ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec.__cmp({ $0 == $1 }, 0x0, "a", 0x2, "b", 0x200)) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, + ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec.__cmp({ lhs, rhs -> Swift.Bool in lhs == rhs }, 0x0, "a", 0x2, "b", 0x200)) }, sourceCode: [0x0: #""a" == "b""#], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require(!Bool.random())"##: ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in try Testing.__requiringTry(__ec(!Bool.random(), 0x0)) }, sourceCode: [0x0: "!Bool.random()"], comments: [], isRequired: true, sourceLocation: Testing.SourceLocation.__here()).__required()"##, ##"#require((true && false))"##: diff --git a/Tests/TestingMacrosTests/UniqueIdentifierTests.swift b/Tests/TestingMacrosTests/UniqueIdentifierTests.swift index 267c93aa9..9c94bb861 100644 --- a/Tests/TestingMacrosTests/UniqueIdentifierTests.swift +++ b/Tests/TestingMacrosTests/UniqueIdentifierTests.swift @@ -81,4 +81,21 @@ struct UniqueIdentifierTests { let uniqueName2 = try makeUniqueName("func f() { def() }") #expect(uniqueName1 == uniqueName2) } + + @Test("Synthesized closure arguments are uniqued", + arguments: [ + ##"#expect(abc)"##: + ##"Testing.__checkCondition({ (__ec: Testing.__ExpectationContext) -> Swift.Bool in __ec(abc, 0x0) }, sourceCode: [0x0: "abc"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(__ec)"##: + ##"Testing.__checkCondition({ (__ec0: Testing.__ExpectationContext) -> Swift.Bool in __ec0(__ec, 0x0) }, sourceCode: [0x0: "__ec"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ##"#expect(__ec + __ec0)"##: + ##"Testing.__checkCondition({ (__ec1: Testing.__ExpectationContext) -> Swift.Bool in __ec1(__ec1(__ec, 0x2) + __ec1(__ec0, 0x20), 0x0) }, sourceCode: [0x0: "__ec + __ec0", 0x2: "__ec", 0x20: "__ec0"], comments: [], isRequired: false, sourceLocation: Testing.SourceLocation.__here()).__expected()"##, + ] + ) + func synthesizedClosureArgumentsUniqued(input: String, expectedOutput: String) throws { + let (expectedOutput, _) = try parse(expectedOutput, removeWhitespace: true) + let (actualOutput, _) = try parse(input, removeWhitespace: true) + let (actualActual, _) = try parse(input) + #expect(expectedOutput == actualOutput, "\(actualActual)") + } }