SimpleTypeIdentifierSyntax for initialiazed variable generates unexpectedAfterBindings

I've found a strange behavior where a SimpleTypeIdentifierSyntax extracted from a variable declaration with a initial value generates a unexpectedAfterBindings when I use it in a DeclSyntax literal.


        let d1: DeclSyntax = "var number: Int = 5"
        let d2: DeclSyntax = "var number: Int"
        
        let t1 = d1.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(SimpleTypeIdentifierSyntax.self)!
        let t2 = d2.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(SimpleTypeIdentifierSyntax.self)!
        
        let d3: DeclSyntax = "var number: \(t1)? = nil" // var number: Int ? = nil
        
//        VariableDeclSyntax
//        ├─bindingKeyword: keyword(SwiftSyntax.Keyword.var)
//        ├─bindings: PatternBindingListSyntax
//        │ ╰─[0]: PatternBindingSyntax
//        │   ├─pattern: IdentifierPatternSyntax
//        │   │ ╰─identifier: identifier("number")
//        │   ╰─typeAnnotation: TypeAnnotationSyntax
//        │     ├─colon: colon
//        │     ╰─type: SimpleTypeIdentifierSyntax
//        │       ╰─name: identifier("Int")
//        ╰─unexpectedAfterBindings: UnexpectedNodesSyntax
//          ├─[0]: infixQuestionMark
//          ├─[1]: equal
//          ╰─[2]: keyword(SwiftSyntax.Keyword.nil)
        
        
        let d4: DeclSyntax = "var number: \(t2)? = nil" // var number: Int? = nil
        
//        VariableDeclSyntax
//        ├─bindingKeyword: keyword(SwiftSyntax.Keyword.var)
//        ╰─bindings: PatternBindingListSyntax
//          ╰─[0]: PatternBindingSyntax
//            ├─pattern: IdentifierPatternSyntax
//            │ ╰─identifier: identifier("number")
//            ├─typeAnnotation: TypeAnnotationSyntax
//            │ ├─colon: colon
//            │ ╰─type: OptionalTypeSyntax
//            │   ├─wrappedType: SimpleTypeIdentifierSyntax
//            │   │ ╰─name: identifier("Int")
//            │   ╰─questionMark: postfixQuestionMark
//            ╰─initializer: InitializerClauseSyntax
//              ├─equal: equal
//              ╰─value: NilLiteralExprSyntax
//                ╰─nilKeyword: keyword(SwiftSyntax.Keyword.nil)

Am I missing something or is this a bug?

The string that's parsed to create d3 is "var number: Int ? = nil" (notice the space before the question mark). That's invalid syntax indeed.

Whitespace is always attached to tokens in SwiftSyntax. So you should rather explicitly extract the token only instead of serializing a whole node.

That said, "var number: \(t1.name.text)? = nil" should be correct.

1 Like

Indeed, that works, thank you!

But it is intriguing to me that d3 creates a malformed string but d4 doesn't because t1 and t2 look the same.

But it is intriguing to me that d3 creates a malformed string but d4 doesn't because t1 and t2 look the same.

That depends on how you serialize the nodes to strings to look at them. The default output omits all trivia. t1.debugDescription(includeTrivia: true) shows you the whitespace as well.

I guess, the general advice is to not rely on any string representations of nodes in your code. They are for debugging purposes only and might change in every version. Rather extract the details you are interested in and only convert bare tokens to strings.

1 Like

t1.name.text will drop any generic parameters of t1, which might not be what you want. A better solution here would be to use t1.trimmed, which strips away any leading or trailing trivia.

That means node.description or \(node) are actually the right ways to expand syntax nodes? Is it guaranteed to be stable from version to version? I presume there could be slight differences depending on to which token trivia is attached.