Skip to content

Commit

Permalink
Name the arguments to the closure to __cmp so we can pass a return ty…
Browse files Browse the repository at this point in the history
…pe and help the type checker a little bit maybe
  • Loading branch information
grynspan committed Jan 27, 2025
1 parent 24a2d00 commit c20e190
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 19 deletions.
10 changes: 1 addition & 9 deletions Sources/TestingMacros/ConditionMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: -
Expand Down
31 changes: 26 additions & 5 deletions Sources/TestingMacros/Support/ConditionArgumentParsing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,35 @@ private final class _ContextInserter<C, M>: 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,
Expand Down
3 changes: 0 additions & 3 deletions Tests/SubexpressionShowcase/SubexpressionShowcase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 2 additions & 2 deletions Tests/TestingMacrosTests/ConditionMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))"##:
Expand Down Expand Up @@ -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))"##:
Expand Down
17 changes: 17 additions & 0 deletions Tests/TestingMacrosTests/UniqueIdentifierTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
}

0 comments on commit c20e190

Please sign in to comment.