Skip to content

Commit 2ec9844

Browse files
authored
Merge pull request #868 from ahoppen/ahoppen/initializer-expr
Add a member on `SyntaxProtocol` that returns an expression to reconstruct a syntax tree
2 parents 669a31f + cdca4de commit 2ec9844

File tree

5 files changed

+216
-14
lines changed

5 files changed

+216
-14
lines changed

CodeGeneration/Sources/generate-swiftbasicformat/BasicFormatFile.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ let basicFormatFile = SourceFile {
2525
ClassDecl(modifiers: [Token.open], identifier: "BasicFormat", inheritanceClause: TypeInheritanceClause { InheritedType(typeName: "SyntaxRewriter") }) {
2626
VariableDecl("public var indentationLevel: Int = 0")
2727
VariableDecl("open var indentation: TriviaPiece { .spaces(indentationLevel * 4) }")
28-
VariableDecl("private var indentedNewline: Trivia { Trivia(pieces: [.newlines(1), indentation]) }")
28+
VariableDecl("public var indentedNewline: Trivia { Trivia(pieces: [.newlines(1), indentation]) }")
2929
VariableDecl("private var lastRewrittenToken: TokenSyntax?")
3030

3131
for node in SYNTAX_NODES where !node.isBase {

Package.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ let package = Package(
110110
),
111111
.target(
112112
name: "_SwiftSyntaxTestSupport",
113-
dependencies: ["SwiftSyntax"]
113+
dependencies: ["SwiftBasicFormat", "SwiftSyntax", "SwiftSyntaxBuilder"]
114114
),
115115
.target(
116116
name: "SwiftParser",
@@ -142,8 +142,8 @@ let package = Package(
142142
),
143143
.executableTarget(
144144
name: "swift-parser-cli",
145-
dependencies: ["SwiftDiagnostics", "SwiftSyntax", "SwiftParser",
146-
"SwiftOperators",
145+
dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics",
146+
"SwiftSyntax", "SwiftParser", "SwiftOperators",
147147
.product(name: "ArgumentParser", package: "swift-argument-parser")]
148148
),
149149
.testTarget(

Sources/SwiftBasicFormat/generated/BasicFormat.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import SwiftSyntax
1717
open class BasicFormat: SyntaxRewriter {
1818
public var indentationLevel: Int = 0
1919
open var indentation: TriviaPiece { .spaces(indentationLevel * 4) }
20-
private var indentedNewline: Trivia { Trivia(pieces: [.newlines(1), indentation]) }
20+
public var indentedNewline: Trivia { Trivia(pieces: [.newlines(1), indentation]) }
2121
private var lastRewrittenToken: TokenSyntax?
2222

2323
open override func visit(_ node: UnknownDeclSyntax) -> DeclSyntax {
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import SwiftBasicFormat
2+
@_spi(RawSyntax) import SwiftSyntax
3+
import SwiftSyntaxBuilder
4+
5+
private class InitializerExprFormat: BasicFormat {
6+
override var indentation: TriviaPiece { return .spaces(indentationLevel * 2) }
7+
8+
private func formatChildrenSeparatedByNewline<SyntaxType: SyntaxProtocol>(children: SyntaxChildren, elementType: SyntaxType.Type) -> [SyntaxType] {
9+
indentationLevel += 1
10+
var formattedChildren = children.map {
11+
self.visit($0).as(SyntaxType.self)!
12+
}
13+
formattedChildren = formattedChildren.map {
14+
if $0.leadingTrivia?.first?.isNewline == true {
15+
return $0
16+
} else {
17+
return $0.withLeadingTrivia(indentedNewline + ($0.leadingTrivia ?? []))
18+
}
19+
}
20+
indentationLevel -= 1
21+
if !formattedChildren.isEmpty {
22+
formattedChildren[formattedChildren.count - 1] = formattedChildren[formattedChildren.count - 1].withTrailingTrivia(indentedNewline)
23+
}
24+
return formattedChildren
25+
}
26+
27+
override func visit(_ node: TupleExprElementListSyntax) -> Syntax {
28+
let children = node.children(viewMode: .all)
29+
// If the function only takes a single argument, display it on the same line
30+
if children.count > 1 {
31+
return Syntax(TupleExprElementListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: TupleExprElementSyntax.self)))
32+
} else {
33+
return super.visit(node)
34+
}
35+
}
36+
37+
override func visit(_ node: ArrayElementListSyntax) -> Syntax {
38+
let children = node.children(viewMode: .all)
39+
// Short array literals are presented on one line, list each element on a different line.
40+
if node.description.count > 30 {
41+
return Syntax(ArrayElementListSyntax(formatChildrenSeparatedByNewline(children: children, elementType: ArrayElementSyntax.self)))
42+
} else {
43+
return super.visit(node)
44+
}
45+
}
46+
}
47+
48+
private extension TriviaPiece {
49+
var initializerExpr: ExprBuildable {
50+
let (label, value) = Mirror(reflecting: self).children.first!
51+
switch value {
52+
case let value as String:
53+
return FunctionCallExpr(calledExpression: MemberAccessExpr(name: label!)) {
54+
TupleExprElement(expression: StringLiteralExpr(value))
55+
}
56+
case let value as Int:
57+
return FunctionCallExpr(calledExpression: MemberAccessExpr(name: label!)) {
58+
TupleExprElement(expression: IntegerLiteralExpr(value))
59+
}
60+
default:
61+
fatalError("Unknown associated value type")
62+
}
63+
}
64+
}
65+
66+
private extension Trivia {
67+
var initializerExpr: ExprBuildable {
68+
if pieces.count == 1 {
69+
switch pieces.first {
70+
case .spaces(1):
71+
return MemberAccessExpr(name: "space")
72+
case .newlines(1):
73+
return MemberAccessExpr(name: "newline")
74+
default:
75+
break
76+
}
77+
}
78+
return ArrayExpr() {
79+
for piece in pieces {
80+
ArrayElement(expression: piece.initializerExpr)
81+
}
82+
}
83+
}
84+
}
85+
86+
extension SyntaxProtocol {
87+
/// Returns a Swift expression that, when parsed, constructs this syntax node
88+
/// (or at least an expression that's very close to constructing this node, the addition of a few manual upcast by hand is still needed).
89+
/// The intended use case for this is to print a syntax tree and create a substructure assertion from the generated expression.
90+
public var debugInitCall: String {
91+
return self.debugInitCallExpr.build(format: InitializerExprFormat()).description
92+
}
93+
94+
private var debugInitCallExpr: ExprBuildable {
95+
let mirror = Mirror(reflecting: self)
96+
if self.isCollection {
97+
return FunctionCallExpr(calledExpression: "\(type(of: self))") {
98+
TupleExprElement(
99+
expression: ArrayExpr() {
100+
for child in mirror.children {
101+
let value = child.value as! SyntaxProtocol?
102+
ArrayElement(expression: value?.debugInitCallExpr ?? NilLiteralExpr())
103+
}
104+
}
105+
)
106+
}
107+
} else if let token = Syntax(self).as(TokenSyntax.self) {
108+
let tokenKind = token.tokenKind
109+
let tokenInitializerName: String
110+
let requiresExplicitText: Bool
111+
if tokenKind.isKeyword || tokenKind == .eof {
112+
tokenInitializerName = String(describing: tokenKind)
113+
requiresExplicitText = false
114+
} else if tokenKind.decomposeToRaw().rawKind.defaultText != nil {
115+
tokenInitializerName = "\(String(describing: tokenKind))Token"
116+
requiresExplicitText = false
117+
} else {
118+
let tokenKindStr = String(describing: tokenKind)
119+
tokenInitializerName = String(tokenKindStr[..<tokenKindStr.firstIndex(of: "(")!])
120+
requiresExplicitText = true
121+
}
122+
return FunctionCallExpr(calledExpression: MemberAccessExpr(name: tokenInitializerName)) {
123+
if requiresExplicitText {
124+
TupleExprElement(
125+
expression: StringLiteralExpr(token.text)
126+
)
127+
}
128+
if !token.leadingTrivia.isEmpty {
129+
TupleExprElement(
130+
label: .identifier("leadingTrivia"),
131+
colon: .colon,
132+
expression: token.leadingTrivia.initializerExpr
133+
)
134+
}
135+
if !token.trailingTrivia.isEmpty {
136+
TupleExprElement(
137+
label: .identifier("trailingTrivia"),
138+
colon: .colon,
139+
expression: token.trailingTrivia.initializerExpr
140+
)
141+
}
142+
if token.presence != .present {
143+
TupleExprElement(
144+
label: .identifier("presence"),
145+
colon: .colon,
146+
expression: MemberAccessExpr(name: "missing")
147+
)
148+
}
149+
}
150+
} else {
151+
return FunctionCallExpr(calledExpression: "\(type(of: self))") {
152+
for child in mirror.children {
153+
let label = child.label!
154+
let value = child.value as! SyntaxProtocol?
155+
let isUnexpected = label.hasPrefix("unexpected")
156+
if !isUnexpected || value != nil {
157+
TupleExprElement(
158+
label: isUnexpected ? nil : .identifier(label),
159+
colon: isUnexpected ? nil : .colon,
160+
expression: value?.debugInitCallExpr ?? NilLiteralExpr()
161+
)
162+
}
163+
}
164+
}
165+
}
166+
}
167+
}

Sources/swift-parser-cli/swift-parser-cli.swift

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13-
13+
import _SwiftSyntaxTestSupport
1414
import SwiftDiagnostics
1515
import SwiftSyntax
1616
import SwiftParser
@@ -98,8 +98,7 @@ class VerifyRoundTrip: ParsableCommand {
9898
@Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax")
9999
var enableBareSlashRegex: Bool?
100100

101-
@Flag(name: .long,
102-
help: "Perform sequence folding with the standard operators")
101+
@Flag(name: .long, help: "Perform sequence folding with the standard operators")
103102
var foldSequences: Bool = false
104103

105104
enum Error: Swift.Error, CustomStringConvertible {
@@ -165,8 +164,7 @@ class PrintDiags: ParsableCommand {
165164
@Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax")
166165
var enableBareSlashRegex: Bool?
167166

168-
@Flag(name: .long,
169-
help: "Perform sequence folding with the standard operators")
167+
@Flag(name: .long, help: "Perform sequence folding with the standard operators")
170168
var foldSequences: Bool = false
171169

172170
func run() throws {
@@ -193,6 +191,45 @@ class PrintDiags: ParsableCommand {
193191
}
194192
}
195193

194+
class PrintInitCall: ParsableCommand {
195+
static var configuration = CommandConfiguration(
196+
commandName: "print-init",
197+
abstract: "Print a Swift expression that creates this tree"
198+
)
199+
200+
required init() {}
201+
202+
@Argument(help: "The source file that should be parsed; if omitted, use stdin")
203+
var sourceFile: String?
204+
205+
@Option(name: .long, help: "Interpret input according to a specific Swift language version number")
206+
var swiftVersion: String?
207+
208+
@Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax")
209+
var enableBareSlashRegex: Bool?
210+
211+
@Flag(name: .long, help: "Perform sequence folding with the standard operators")
212+
var foldSequences: Bool = false
213+
214+
func run() throws {
215+
let source = try getContentsOfSourceFile(at: sourceFile)
216+
217+
try source.withUnsafeBufferPointer { sourceBuffer in
218+
var tree = try Parser.parse(
219+
source: sourceBuffer,
220+
languageVersion: swiftVersion,
221+
enableBareSlashRegexLiteral: enableBareSlashRegex
222+
)
223+
224+
if foldSequences {
225+
tree = foldAllSequences(tree).0.as(SourceFileSyntax.self)!
226+
}
227+
228+
print(tree.debugInitCall)
229+
}
230+
}
231+
}
232+
196233
class PrintTree: ParsableCommand {
197234
static var configuration = CommandConfiguration(
198235
commandName: "print-tree",
@@ -210,8 +247,7 @@ class PrintTree: ParsableCommand {
210247
@Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax")
211248
var enableBareSlashRegex: Bool?
212249

213-
@Flag(name: .long,
214-
help: "Perform sequence folding with the standard operators")
250+
@Flag(name: .long, help: "Perform sequence folding with the standard operators")
215251
var foldSequences: Bool = false
216252

217253
func run() throws {
@@ -253,8 +289,7 @@ class Reduce: ParsableCommand {
253289
@Option(name: .long, help: "Enable or disable the use of forward slash regular-expression literal syntax")
254290
var enableBareSlashRegex: Bool?
255291

256-
@Flag(name: .long,
257-
help: "Perform sequence folding with the standard operators")
292+
@Flag(name: .long, help: "Perform sequence folding with the standard operators")
258293
var foldSequences: Bool = false
259294

260295
@Flag(help: "Print status updates while reducing the test case")

0 commit comments

Comments
 (0)