Skip to content

Commit d4bbf9c

Browse files
committed
Address review comments
1 parent 037e55e commit d4bbf9c

File tree

2 files changed

+123
-106
lines changed

2 files changed

+123
-106
lines changed

Sources/SourceKitLSP/Swift/CodeActions/ConvertJSONToCodableStruct.swift

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public struct ConvertJSONToCodableStruct: EditRefactoringProvider {
8888

8989
// Try to process this as JSON.
9090
guard
91-
let object = try? JSONSerialization.jsonObject(with: text.data(using: .utf8)!),
91+
let data = text.data(using: .utf8),
92+
let object = try? JSONSerialization.jsonObject(with: data),
9293
let dictionary = object as? [String: Any]
9394
else {
9495
return []
@@ -131,12 +132,10 @@ public struct ConvertJSONToCodableStruct: EditRefactoringProvider {
131132
]
132133
}
133134
}
134-
}
135135

136-
extension ConvertJSONToCodableStruct {
137136
/// The result of preflighting a syntax node to try to find potential JSON
138137
/// in it.
139-
enum Preflight {
138+
private enum Preflight {
140139
/// A closure, which is what a JSON dictionary looks like when pasted
141140
/// into Swift.
142141
case closure(ClosureExprSyntax)
@@ -150,7 +149,7 @@ extension ConvertJSONToCodableStruct {
150149
}
151150

152151
/// Look for either a closure or a string literal that might have JSON in it.
153-
static func preflightRefactoring(_ syntax: Syntax) -> Preflight? {
152+
private static func preflightRefactoring(_ syntax: Syntax) -> Preflight? {
154153
// Preflight a closure.
155154
//
156155
// A blob of JSON dropped into a Swift source file will look like a
@@ -166,7 +165,7 @@ extension ConvertJSONToCodableStruct {
166165
}
167166

168167
// We found a string literal; its contents might be JSON.
169-
if let stringLit = syntax.as(StringLiteralExprSyntax.self) {
168+
if let stringLiteral = syntax.as(StringLiteralExprSyntax.self) {
170169
// Look for an enclosing context and prefer that, because we might have
171170
// a string literal that's inside a closure where the closure itself
172171
// is the JSON.
@@ -176,11 +175,11 @@ extension ConvertJSONToCodableStruct {
176175
return enclosingPreflight
177176
}
178177

179-
guard let text = stringLit.representedLiteralValue else {
178+
guard let text = stringLiteral.representedLiteralValue else {
180179
return nil
181180
}
182181

183-
return .stringLiteral(stringLit, text)
182+
return .stringLiteral(stringLiteral, text)
184183
}
185184

186185
// Look further up the syntax tree.
@@ -216,9 +215,7 @@ fileprivate struct JSONObject {
216215
/// a JSON object
217216
func merging(with other: JSONObject) -> JSONObject {
218217
// Collect the set of all keys from both JSON objects.
219-
var allKeys: Set<String> = []
220-
allKeys.formUnion(fields.keys)
221-
allKeys.formUnion(other.fields.keys)
218+
let allKeys = Set(fields.keys).union(other.fields.keys)
222219

223220
// Form a new JSON object containing the union of the fields
224221
let newFields = allKeys.map { key in
@@ -235,7 +232,7 @@ fileprivate struct JSONObject {
235232
let sortedFields = fields.sorted(by: { $0.key < $1.key })
236233

237234
// Collect the nested types
238-
let nestedTypes: [(String, JSONObject)] = sortedFields.compactMap { (name, type) in
235+
let nestedTypes: [(name: String, type: JSONObject)] = sortedFields.compactMap { (name, type) in
239236
guard let object = type.innerObject else {
240237
return nil
241238
}
@@ -255,7 +252,7 @@ fileprivate struct JSONObject {
255252
// Print any nested types.
256253
for (typeName, object) in nestedTypes {
257254
MemberBlockItemSyntax(
258-
leadingTrivia: (typeName == nestedTypes.first?.0) ? .newlines(2) : .newline,
255+
leadingTrivia: (typeName == nestedTypes.first?.name) ? .newlines(2) : .newline,
259256
decl: object.asDeclSyntax(name: typeName)
260257
)
261258
}
@@ -298,16 +295,13 @@ fileprivate enum JSONType {
298295
init(value: Any) {
299296
switch value {
300297
case let string as String:
301-
if string == "true" || string == "false" {
302-
self = .boolean
303-
} else {
304-
self = .string
298+
switch string {
299+
case "true", "false": self = .boolean
300+
default: self = .string
305301
}
306302
case is NSNumber:
307303
self = .number
308-
case is NSArray:
309-
let array = value as! [Any]
310-
304+
case let array as [Any]:
311305
// Use null as a fallback for an empty array.
312306
guard let firstValue = array.first else {
313307
self = .array(.null)
@@ -324,8 +318,8 @@ fileprivate enum JSONType {
324318

325319
case is NSNull:
326320
self = .null
327-
case is NSDictionary:
328-
self = .object(JSONObject(dictionary: value as! [String: Any]))
321+
case let dictionary as [String: Any]:
322+
self = .object(JSONObject(dictionary: dictionary))
329323
default:
330324
self = .string
331325
}

Tests/SourceKitLSPTests/SyntaxRefactorTests.swift

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

13+
import SKTestSupport
1314
@_spi(Testing) import SourceKitLSP
1415
import SwiftParser
1516
import SwiftRefactor
@@ -20,7 +21,7 @@ final class SyntaxRefactorTests: XCTestCase {
2021
func testAddDocumentationRefactor() throws {
2122
try assertRefactor(
2223
"""
23-
func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }
24+
1️⃣func 2️⃣refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }
2425
""",
2526
context: (),
2627
provider: AddDocumentation.self,
@@ -43,21 +44,21 @@ final class SyntaxRefactorTests: XCTestCase {
4344

4445
func testConvertJSONToCodableStructClosure() throws {
4546
try assertRefactor(
46-
malformedInput: """
47-
{
48-
"name": "Produce",
49-
"shelves": [
50-
{
51-
"name": "Discount Produce",
52-
"product": {
53-
"name": "Banana",
54-
"points": 200,
55-
"description": "A banana that's perfectly ripe."
56-
}
57-
}
58-
]
59-
}
60-
""",
47+
"""
48+
4️⃣{1️⃣
49+
2️⃣"name": "Produce",
50+
"shelves": [
51+
{
52+
"name": "Discount Produce",
53+
"product": {
54+
"name": "Banana",
55+
"points": 200,
56+
"description": "A banana that's perfectly ripe."
57+
}
58+
}
59+
]
60+
}
61+
""",
6162
context: (),
6263
provider: ConvertJSONToCodableStruct.self,
6364
expected: [
@@ -87,23 +88,23 @@ final class SyntaxRefactorTests: XCTestCase {
8788

8889
func testConvertJSONToCodableStructLiteral() throws {
8990
try assertRefactor(
90-
malformedInput: #"""
91+
#"""
92+
"""
93+
{
94+
"name": "Produce",
95+
"shelves": [
96+
{
97+
"name": "Discount Produce",
98+
"product": {
99+
"name": "Banana",
100+
"points": 200,
101+
"description": "A banana that's perfectly ripe."
102+
}
103+
}
104+
]
105+
}
91106
"""
92-
{
93-
"name": "Produce",
94-
"shelves": [
95-
{
96-
"name": "Discount Produce",
97-
"product": {
98-
"name": "Banana",
99-
"points": 200,
100-
"description": "A banana that's perfectly ripe."
101-
}
102-
}
103-
]
104-
}
105-
"""
106-
"""#,
107+
"""#,
107108
context: (),
108109
provider: ConvertJSONToCodableStruct.self,
109110
expected: [
@@ -134,44 +135,44 @@ final class SyntaxRefactorTests: XCTestCase {
134135

135136
func testConvertJSONToCodableStructClosureMerging() throws {
136137
try assertRefactor(
137-
malformedInput: """
138-
{
139-
"name": "Store",
140-
"shelves": [
141-
{
142-
"name": "Discount Produce",
143-
"product": {
144-
"name": "Banana",
145-
"points": 200,
146-
"description": "A banana that's perfectly ripe.",
147-
"healthy": "true",
148-
"delicious": "true",
149-
"categories": [ "fruit", "yellow" ]
150-
}
151-
},
152-
{
153-
"name": "Meat",
154-
"product": {
155-
"name": "steak",
156-
"points": 200,
157-
"healthy": "false",
158-
"delicious": "true",
159-
"categories": [ ]
160-
}
161-
},
162-
{
163-
"name": "Cereal aisle",
164-
"product": {
165-
"name": "Sugarydoos",
166-
"points": 0.5,
167-
"healthy": "false",
168-
"delicious": "maybe",
169-
"description": "More sugar than you can imagine."
170-
}
171-
}
172-
]
173-
}
174-
""",
138+
"""
139+
{
140+
"name": "Store",
141+
"shelves": [
142+
{
143+
"name": "Discount Produce",
144+
"product": {
145+
"name": "Banana",
146+
"points": 200,
147+
"description": "A banana that's perfectly ripe.",
148+
"healthy": "true",
149+
"delicious": "true",
150+
"categories": [ "fruit", "yellow" ]
151+
}
152+
},
153+
{
154+
"name": "Meat",
155+
"product": {
156+
"name": "steak",
157+
"points": 200,
158+
"healthy": "false",
159+
"delicious": "true",
160+
"categories": [ ]
161+
}
162+
},
163+
{
164+
"name": "Cereal aisle",
165+
"product": {
166+
"name": "Sugarydoos",
167+
"points": 0.5,
168+
"healthy": "false",
169+
"delicious": "maybe",
170+
"description": "More sugar than you can imagine."
171+
}
172+
}
173+
]
174+
}
175+
""",
175176
context: (),
176177
provider: ConvertJSONToCodableStruct.self,
177178
expected: [
@@ -205,23 +206,44 @@ final class SyntaxRefactorTests: XCTestCase {
205206
}
206207

207208
func assertRefactor<R: EditRefactoringProvider>(
208-
malformedInput input: String,
209+
_ input: String,
209210
context: R.Context,
210211
provider: R.Type,
211212
expected: [SourceEdit],
212213
file: StaticString = #filePath,
213214
line: UInt = #line
214-
) throws where R.Input == Syntax {
215-
var parser = Parser(input)
216-
let syntax = ExprSyntax.parse(from: &parser)
217-
try assertRefactor(
218-
Syntax(syntax),
219-
context: context,
220-
provider: provider,
221-
expected: expected,
222-
file: file,
223-
line: line
224-
)
215+
) throws {
216+
let (markers, textWithoutMarkers) = extractMarkers(input)
217+
218+
var parser = Parser(textWithoutMarkers)
219+
let sourceFile = SourceFileSyntax.parse(from: &parser)
220+
221+
let markersToCheck = markers.isEmpty ? [("1️⃣", 0)] : markers.sorted { $0.key < $1.key }
222+
223+
for (marker, location) in markersToCheck {
224+
guard let token = sourceFile.token(at: AbsolutePosition(utf8Offset: location)) else {
225+
XCTFail("Could not find token at location \(marker)")
226+
continue
227+
}
228+
229+
let input: R.Input
230+
if let parentMatch = token.parent?.as(R.Input.self) {
231+
input = parentMatch
232+
} else {
233+
XCTFail("token at \(marker) did not match expected input: \(token)")
234+
continue
235+
}
236+
237+
try assertRefactor(
238+
input,
239+
context: context,
240+
provider: provider,
241+
expected: expected,
242+
at: marker,
243+
file: file,
244+
line: line
245+
)
246+
}
225247
}
226248

227249
// Borrowed from the swift-syntax library's SwiftRefactor tests.
@@ -231,6 +253,7 @@ func assertRefactor<R: EditRefactoringProvider>(
231253
context: R.Context,
232254
provider: R.Type,
233255
expected: [SourceEdit],
256+
at marker: String,
234257
file: StaticString = #filePath,
235258
line: UInt = #line
236259
) throws {
@@ -239,7 +262,7 @@ func assertRefactor<R: EditRefactoringProvider>(
239262
if !expected.isEmpty {
240263
XCTFail(
241264
"""
242-
Refactoring produced empty result, expected:
265+
Refactoring at \(marker) produced empty result, expected:
243266
\(expected)
244267
""",
245268
file: file,
@@ -252,7 +275,7 @@ func assertRefactor<R: EditRefactoringProvider>(
252275
if edits.count != expected.count {
253276
XCTFail(
254277
"""
255-
Refactoring produced incorrect number of edits, expected \(expected.count) not \(edits.count).
278+
Refactoring at \(marker) produced incorrect number of edits, expected \(expected.count) not \(edits.count).
256279
257280
Actual:
258281
\(edits.map({ $0.debugDescription }).joined(separator: "\n"))

0 commit comments

Comments
 (0)