Repository: swiftlang/swift-syntax (swift)

Issue:
Sometimes the placeholder is identified as IdentifierPattern
### Description

Sometimes the placeholder is identified as IdentifierPattern and not as EditorPlaceholderExpr.

### Steps to Reproduce

```
let <#name#> = <#value#>
```
<#name#> //Bug:  is identified as `IdentifierPattern`
<#value#> //Normal: is identified as `EditorPlaceholderExpr`

Pull request:
Fixes #2147
Fixes #2156 
Fixes #2178

We try again after #2183 

I've added the test cases from #2150.

Patch:
diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift
index 43d6edcbf70..32661f33a8d 100644
--- a/Sources/SwiftParser/Declarations.swift
+++ b/Sources/SwiftParser/Declarations.swift
@@ -1050,16 +1050,25 @@ extension Parser {
   ) -> RawFunctionDeclSyntax {
     let (unexpectedBeforeFuncKeyword, funcKeyword) = self.eat(handle)
     let unexpectedBeforeIdentifier: RawUnexpectedNodesSyntax?
+    let unexpectedAfterIdentifier: RawUnexpectedNodesSyntax?
     let identifier: RawTokenSyntax
     if self.at(anyIn: Operator.self) != nil || self.at(.exclamationMark, .prefixAmpersand) {
       var name = self.currentToken.tokenText
-      if name.count > 1 && name.hasSuffix("<") && self.peek(isAt: .identifier) {
+      if !currentToken.isEditorPlaceholder && name.count > 1 && name.hasSuffix("<") && self.peek(isAt: .identifier) {
         name = SyntaxText(rebasing: name.dropLast())
       }
       unexpectedBeforeIdentifier = nil
       identifier = self.consumePrefix(name, as: .binaryOperator)
+      unexpectedAfterIdentifier = nil
     } else {
       (unexpectedBeforeIdentifier, identifier) = self.expectIdentifier(keywordRecovery: true)
+
+      if currentToken.isEditorPlaceholder {
+        let editorPlaceholder = self.parseAnyIdentifier()
+        unexpectedAfterIdentifier = RawUnexpectedNodesSyntax([editorPlaceholder], arena: self.arena)
+      } else {
+        unexpectedAfterIdentifier = nil
+      }
     }
 
     let genericParams: RawGenericParameterClauseSyntax?
@@ -1086,6 +1095,7 @@ extension Parser {
       funcKeyword: funcKeyword,
       unexpectedBeforeIdentifier,
       name: identifier,
+      unexpectedAfterIdentifier,
       genericParameterClause: genericParams,
       signature: signature,
       genericWhereClause: generics,
diff --git a/Sources/SwiftParser/Types.swift b/Sources/SwiftParser/Types.swift
index 36bfb2c7a9e..268aa59070f 100644
--- a/Sources/SwiftParser/Types.swift
+++ b/Sources/SwiftParser/Types.swift
@@ -938,7 +938,9 @@ extension Parser {
 
 extension Parser {
   mutating func parseResultType() -> RawTypeSyntax {
-    if self.at(prefix: "<") {
+    if self.currentToken.isEditorPlaceholder {
+      return self.parseTypeIdentifier()
+    } else if self.at(prefix: "<") {
       let generics = self.parseGenericParameters()
       let baseType = self.parseType()
       return RawTypeSyntax(
diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
index 517cf281594..d0775cc2b36 100644
--- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
+++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift
@@ -388,6 +388,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
         ]
       )
     } else if let firstToken = node.first?.as(TokenSyntax.self),
+      !firstToken.isEditorPlaceholder,
       firstToken.tokenKind.isIdentifier == true,
       firstToken.presence == .present,
       let previousToken = node.previousToken(viewMode: .sourceAccurate),
diff --git a/Sources/SwiftSyntax/TokenSyntax.swift b/Sources/SwiftSyntax/TokenSyntax.swift
index f7576677bb4..6338f887262 100644
--- a/Sources/SwiftSyntax/TokenSyntax.swift
+++ b/Sources/SwiftSyntax/TokenSyntax.swift
@@ -131,6 +131,16 @@ public struct TokenSyntax: SyntaxProtocol, SyntaxHashable {
     return raw.totalLength
   }
 
+  /// Whether the token text is an editor placeholder or not.
+  public var isEditorPlaceholder: Bool {
+    switch self.tokenKind {
+    case .identifier(let text):
+      return text.hasPrefix("<#") && text.hasSuffix("#>")
+    default:
+      return false
+    }
+  }
+
   /// A token by itself has no structure, so we represent its structure by an
   /// empty layout node.
   ///


Test patch:
diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift
index 6a0aec1c807..2bd2e492fd8 100644
--- a/Tests/SwiftParserTest/DeclarationTests.swift
+++ b/Tests/SwiftParserTest/DeclarationTests.swift
@@ -2550,4 +2550,166 @@ final class DeclarationTests: ParserTestCase {
       """
     )
   }
+
+  func testDeclarationsWithPlaceholders() {
+    assertParse(
+      """
+      func 1️⃣<#name#>(2️⃣<#parameters#>3️⃣) -> 4️⃣<#return type#> {
+          5️⃣<#function body#>
+      }
+      """,
+      substructure: FunctionDeclSyntax(
+        funcKeyword: .keyword(.func),
+        name: .identifier("<#name#>"),
+        signature: FunctionSignatureSyntax(
+          parameterClause: FunctionParameterClauseSyntax(
+            parameters: FunctionParameterListSyntax([
+              FunctionParameterSyntax(
+                firstName: .identifier("<#parameters#>"),
+                colon: .colonToken(presence: .missing),
+                type: MissingTypeSyntax(
+                  placeholder: .identifier("<#type#>", presence: .missing)
+                )
+              )
+            ])
+          ),
+          returnClause: ReturnClauseSyntax(
+            type: IdentifierTypeSyntax(
+              name: .identifier("<#return type#>")
+            )
+          )
+        ),
+        body: CodeBlockSyntax(
+          statements: CodeBlockItemListSyntax([
+            CodeBlockItemSyntax(
+              item: .expr(
+                ExprSyntax(
+                  EditorPlaceholderExprSyntax(
+                    placeholder: .identifier("<#function body#>")
+                  )
+                )
+              )
+            )
+          ])
+        )
+      ),
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "3️⃣", message: "expected ':' and type in parameter", fixIts: ["insert ':' and type"]),
+        DiagnosticSpec(locationMarker: "4️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "5️⃣", message: "editor placeholder in source file"),
+      ],
+      fixedSource: """
+        func <#name#>(<#parameters#>: <#type#>) -> <#return type#> {
+            <#function body#>
+        }
+        """
+    )
+
+    assertParse(
+      """
+      func test1️⃣<#name#>() {
+          2️⃣<#function body#>
+      }
+      """,
+      substructure: FunctionDeclSyntax(
+        funcKeyword: .keyword(.func),
+        name: .identifier("test"),
+        UnexpectedNodesSyntax([
+          TokenSyntax.identifier("<#name#>")
+        ]),
+        signature: FunctionSignatureSyntax(
+          parameterClause: FunctionParameterClauseSyntax(
+            parameters: FunctionParameterListSyntax([])
+          )
+        ),
+        body: CodeBlockSyntax(
+          statements: CodeBlockItemListSyntax([
+            CodeBlockItemSyntax(
+              item: .expr(
+                ExprSyntax(
+                  EditorPlaceholderExprSyntax(
+                    placeholder: .identifier("<#function body#>")
+                  )
+                )
+              )
+            )
+          ])
+        )
+      ),
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "unexpected code '<#name#>' in function"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+
+    assertParse(
+      """
+      class 1️⃣<#name#>: 2️⃣<#super class#> {
+          3️⃣<#code#>
+      }
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "3️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+
+    assertParse(
+      """
+      enum 1️⃣<#name#> {
+          case 2️⃣<#code#>
+      }
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+
+    assertParse(
+      """
+      struct 1️⃣<#name#> {
+          2️⃣<#code#>
+      }
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+
+    assertParse(
+      """
+      protocol 1️⃣<#name#> {
+          2️⃣<#code#>
+      }
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+
+    assertParse(
+      """
+      import 1️⃣<#name#>
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file")
+      ]
+    )
+
+    assertParse(
+      """
+      typealias 1️⃣<#name#> = 2️⃣<#code#>
+      """,
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+  }
 }
diff --git a/Tests/SwiftParserTest/LexerTests.swift b/Tests/SwiftParserTest/LexerTests.swift
index f56a76f16a2..38d62424434 100644
--- a/Tests/SwiftParserTest/LexerTests.swift
+++ b/Tests/SwiftParserTest/LexerTests.swift
@@ -846,6 +846,16 @@ public class LexerTests: ParserTestCase {
         LexemeSpec(.identifier, text: "<##>", trailing: "", diagnostic: "editor placeholder in source file")
       ]
     )
+
+    assertLexemes(
+      "let 1️⃣<#name#> = 2️⃣<#value#>",
+      lexemes: [
+        LexemeSpec(.keyword, text: "let", trailing: " "),
+        LexemeSpec(.identifier, text: "<#name#>", trailing: " ", errorLocationMarker: "1️⃣", diagnostic: "editor placeholder in source file"),
+        LexemeSpec(.equal, text: "=", trailing: " "),
+        LexemeSpec(.identifier, text: "<#value#>", errorLocationMarker: "2️⃣", diagnostic: "editor placeholder in source file"),
+      ]
+    )
   }
 
   func testCommentAttribution() {
diff --git a/Tests/SwiftParserTest/PatternTests.swift b/Tests/SwiftParserTest/PatternTests.swift
index 250f63020d8..15a212eb5b8 100644
--- a/Tests/SwiftParserTest/PatternTests.swift
+++ b/Tests/SwiftParserTest/PatternTests.swift
@@ -168,4 +168,29 @@ final class PatternTests: ParserTestCase {
       substructure: subscriptWithBindingPattern
     )
   }
+
+  func testPatternAsPlaceholderExpr() {
+    assertParse(
+      "let 1️⃣<#name#> = 2️⃣<#value#>",
+      substructure: VariableDeclSyntax(
+        bindingSpecifier: .keyword(.let),
+        bindings: [
+          PatternBindingSyntax(
+            pattern: IdentifierPatternSyntax(
+              identifier: .identifier("<#name#>")
+            ),
+            initializer: InitializerClauseSyntax(
+              value: EditorPlaceholderExprSyntax(
+                placeholder: .identifier("<#value#>")
+              )
+            )
+          )
+        ]
+      ),
+      diagnostics: [
+        DiagnosticSpec(locationMarker: "1️⃣", message: "editor placeholder in source file"),
+        DiagnosticSpec(locationMarker: "2️⃣", message: "editor placeholder in source file"),
+      ]
+    )
+  }
 }
diff --git a/Tests/SwiftParserTest/TypeTests.swift b/Tests/SwiftParserTest/TypeTests.swift
index 5be48f065fe..090c541c965 100644
--- a/Tests/SwiftParserTest/TypeTests.swift
+++ b/Tests/SwiftParserTest/TypeTests.swift
@@ -216,4 +216,92 @@ final class TypeTests: ParserTestCase {
       substructureAfterMarker: "1️⃣"
     )
   }
+
+  func testTypeWithPlaceholder() {
+    assertParse(
+      "let a: 1️⃣<#T#> = x",
+      substructure: VariableDeclSyntax(
+        bindingSpecifier: .keyword(.let),
+        bindings: [
+          PatternBindingSyntax(
+            pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
+            typeAnnotation: TypeAnnotationSyntax(
+              type: IdentifierTypeSyntax(
+                name: .identifier("<#T#>")
+              )
+            ),
+            initializer: InitializerClauseSyntax(
+              value: DeclReferenceExprSyntax(
+                baseName: .identifier("x")
+              )
+            )
+          )
+        ]
+      ),
+      diagnostics: [
+        DiagnosticSpec(message: "editor placeholder in source file")
+      ]
+    )
+
+    assertParse(
+      "let a: 1️⃣<#T#><Foo> = x",
+      substructure: VariableDeclSyntax(
+        bindingSpecifier: .keyword(.let),
+        bindings: [
+          PatternBindingSyntax(
+            pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
+            typeAnnotation: TypeAnnotationSyntax(
+              type: IdentifierTypeSyntax(
+                name: .identifier("<#T#>"),
+                genericArgumentClause: GenericArgumentClauseSyntax(
+                  arguments: GenericArgumentListSyntax([
+                    GenericArgumentSyntax(
+                      argument: IdentifierTypeSyntax(
+                        name: .identifier("Foo")
+                      )
+                    )
+                  ])
+                )
+              )
+            ),
+            initializer: InitializerClauseSyntax(
+              value: DeclReferenceExprSyntax(
+                baseName: .identifier("x")
+              )
+            )
+          )
+        ]
+      ),
+      diagnostics: [
+        DiagnosticSpec(message: "editor placeholder in source file")
+      ]
+    )
+
+    assertParse(
+      "let a: [1️⃣<#T#>] = x",
+      substructure: VariableDeclSyntax(
+        bindingSpecifier: .keyword(.let),
+        bindings: [
+          PatternBindingSyntax(
+            pattern: IdentifierPatternSyntax(identifier: .identifier("a")),
+            typeAnnotation: TypeAnnotationSyntax(
+              type: ArrayTypeSyntax(
+                element: IdentifierTypeSyntax(
+                  name: .identifier("<#T#>")
+                )
+              )
+            ),
+            initializer: InitializerClauseSyntax(
+              value: DeclReferenceExprSyntax(
+                baseName: .identifier("x")
+              )
+            )
+          )
+        ]
+      ),
+      diagnostics: [
+        DiagnosticSpec(message: "editor placeholder in source file")
+      ]
+    )
+  }
 }