Skip to content

Commit 1f4fc12

Browse files
committed
Add LSP support for showing @attached Macro Expansions
1 parent abfe5e9 commit 1f4fc12

File tree

2 files changed

+205
-3
lines changed

2 files changed

+205
-3
lines changed

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ extension SwiftLanguageService {
9595

9696
// github permalink notation for position range
9797
let macroExpansionPositionRangeIndicator =
98-
"L\(macroEdit.range.lowerBound.line)C\(macroEdit.range.lowerBound.utf16index)-L\(macroEdit.range.upperBound.line)C\(macroEdit.range.upperBound.utf16index)"
98+
"L\(macroEdit.range.lowerBound.line + 1)C\(macroEdit.range.lowerBound.utf16index + 1)-L\(macroEdit.range.upperBound.line + 1)C\(macroEdit.range.upperBound.utf16index + 1)"
9999

100100
let macroExpansionFilePath =
101101
macroExpansionBufferDirectoryURL
@@ -112,7 +112,7 @@ extension SwiftLanguageService {
112112
}
113113

114114
Task {
115-
let req = ShowDocumentRequest(uri: DocumentURI(macroExpansionFilePath), selection: macroEdit.range)
115+
let req = ShowDocumentRequest(uri: DocumentURI(macroExpansionFilePath))
116116

117117
let response = await orLog("Sending ShowDocumentRequest to Client") {
118118
try await sourceKitLSPServer.sendRequestToClient(req)

Tests/SourceKitLSPTests/ExecuteCommandTests.swift

Lines changed: 203 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,209 @@ final class ExecuteCommandTests: XCTestCase {
252252

253253
XCTAssertEqual(
254254
url.lastPathComponent,
255-
"MyMacroClient_L4C2-L4C19.swift",
255+
"MyMacroClient_L5C3-L5C20.swift",
256+
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
257+
)
258+
}
259+
}
260+
261+
func testAttachedMacroExpansion() async throws {
262+
try await SkipUnless.canBuildMacroUsingSwiftSyntaxFromSourceKitLSPBuild()
263+
264+
let options = SourceKitLSPOptions.testDefault(experimentalFeatures: [.showMacroExpansions])
265+
266+
let project = try await SwiftPMTestProject(
267+
files: [
268+
"MyMacros/MyMacros.swift": #"""
269+
import SwiftCompilerPlugin
270+
import SwiftSyntax
271+
import SwiftSyntaxBuilder
272+
import SwiftSyntaxMacros
273+
274+
public struct DictionaryStorageMacro {}
275+
276+
extension DictionaryStorageMacro: MemberMacro {
277+
public static func expansion(
278+
of node: AttributeSyntax,
279+
providingMembersOf declaration: some DeclGroupSyntax,
280+
in context: some MacroExpansionContext
281+
) throws -> [DeclSyntax] {
282+
return ["\n var _storage: [String: Any] = [:]"]
283+
}
284+
}
285+
286+
extension DictionaryStorageMacro: MemberAttributeMacro {
287+
public static func expansion(
288+
of node: AttributeSyntax,
289+
attachedTo declaration: some DeclGroupSyntax,
290+
providingAttributesFor member: some DeclSyntaxProtocol,
291+
in context: some MacroExpansionContext
292+
) throws -> [AttributeSyntax] {
293+
return [
294+
AttributeSyntax(
295+
leadingTrivia: [.newlines(1), .spaces(2)],
296+
attributeName: IdentifierTypeSyntax(
297+
name: .identifier("DictionaryStorageProperty")
298+
)
299+
)
300+
]
301+
}
302+
}
303+
304+
public struct DictionaryStoragePropertyMacro: AccessorMacro {
305+
public static func expansion<
306+
Context: MacroExpansionContext,
307+
Declaration: DeclSyntaxProtocol
308+
>(
309+
of node: AttributeSyntax,
310+
providingAccessorsOf declaration: Declaration,
311+
in context: Context
312+
) throws -> [AccessorDeclSyntax] {
313+
guard let binding = declaration.as(VariableDeclSyntax.self)?.bindings.first,
314+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
315+
binding.accessorBlock == nil,
316+
let type = binding.typeAnnotation?.type,
317+
let defaultValue = binding.initializer?.value,
318+
identifier.text != "_storage"
319+
else {
320+
return []
321+
}
322+
323+
return [
324+
"""
325+
get {
326+
_storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type)
327+
}
328+
""",
329+
"""
330+
set {
331+
_storage[\(literal: identifier.text)] = newValue
332+
}
333+
""",
334+
]
335+
}
336+
}
337+
338+
@main
339+
struct MyMacroPlugin: CompilerPlugin {
340+
let providingMacros: [Macro.Type] = [
341+
DictionaryStorageMacro.self,
342+
DictionaryStoragePropertyMacro.self
343+
]
344+
}
345+
"""#,
346+
"MyMacroClient/MyMacroClient.swift": #"""
347+
@attached(memberAttribute)
348+
@attached(member, names: named(_storage))
349+
public macro DictionaryStorage() = #externalMacro(module: "MyMacros", type: "DictionaryStorageMacro")
350+
351+
@attached(accessor)
352+
public macro DictionaryStorageProperty() =
353+
#externalMacro(module: "MyMacros", type: "DictionaryStoragePropertyMacro")
354+
355+
1️⃣@2️⃣DictionaryStorage3️⃣
356+
struct Point {
357+
var x: Int = 1
358+
var y: Int = 2
359+
}
360+
"""#,
361+
],
362+
manifest: SwiftPMTestProject.macroPackageManifest,
363+
options: options
364+
)
365+
try await SwiftPMTestProject.build(at: project.scratchDirectory)
366+
367+
let (uri, positions) = try project.openDocument("MyMacroClient.swift")
368+
369+
let positionMarkersToBeTested = [
370+
(start: "1️⃣", end: "1️⃣"),
371+
(start: "2️⃣", end: "2️⃣"),
372+
(start: "1️⃣", end: "3️⃣"),
373+
(start: "2️⃣", end: "3️⃣"),
374+
]
375+
376+
for positionMarker in positionMarkersToBeTested {
377+
let args = ExpandMacroCommand(
378+
positionRange: positions[positionMarker.start]..<positions[positionMarker.end],
379+
textDocument: TextDocumentIdentifier(uri)
380+
)
381+
382+
let metadata = SourceKitLSPCommandMetadata(textDocument: TextDocumentIdentifier(uri))
383+
384+
var command = args.asCommand()
385+
command.arguments?.append(metadata.encodeToLSPAny())
386+
387+
let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments)
388+
389+
let expectation = self.expectation(description: "Handle Show Document Requests")
390+
expectation.expectedFulfillmentCount = 3
391+
392+
let showDocumentRequestURIs = ThreadSafeBox<[DocumentURI?]>(initialValue: [nil, nil, nil])
393+
394+
for i in 0...2 {
395+
project.testClient.handleSingleRequest { (req: ShowDocumentRequest) in
396+
showDocumentRequestURIs.value[i] = req.uri
397+
expectation.fulfill()
398+
return ShowDocumentResponse(success: true)
399+
}
400+
}
401+
402+
let result = try await project.testClient.send(request)
403+
404+
guard let resultArray: [RefactoringEdit] = Array(fromLSPArray: result ?? .null) else {
405+
XCTFail(
406+
"Result is not an array. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
407+
)
408+
return
409+
}
410+
411+
XCTAssertEqual(
412+
resultArray.count,
413+
4,
414+
"resultArray count is not equal to four. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
415+
)
416+
417+
XCTAssertEqual(
418+
resultArray.map { $0.newText }.sorted(),
419+
[
420+
"",
421+
"@DictionaryStorageProperty",
422+
"@DictionaryStorageProperty",
423+
"var _storage: [String: Any] = [:]",
424+
].sorted(),
425+
"Wrong macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
426+
)
427+
428+
try await fulfillmentOfOrThrow([expectation])
429+
430+
let urls = try showDocumentRequestURIs.value.map {
431+
try XCTUnwrap(
432+
$0?.fileURL,
433+
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
434+
)
435+
}
436+
437+
let filesContents = try urls.map {
438+
try String(contentsOf: $0, encoding: .utf8)
439+
}
440+
441+
XCTAssertEqual(
442+
filesContents.sorted(),
443+
[
444+
"@DictionaryStorageProperty",
445+
"@DictionaryStorageProperty",
446+
"var _storage: [String: Any] = [:]",
447+
].sorted(),
448+
"Files doesn't contain correct macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
449+
)
450+
451+
XCTAssertEqual(
452+
urls.map { $0.lastPathComponent }.sorted(),
453+
[
454+
"MyMacroClient_L11C3-L11C3.swift",
455+
"MyMacroClient_L12C3-L12C3.swift",
456+
"MyMacroClient_L13C1-L13C1.swift",
457+
].sorted(),
256458
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
257459
)
258460
}

0 commit comments

Comments
 (0)