Skip to content

Commit 35f821e

Browse files
committed
Add LSP support for showing @attached Macro Expansions
1 parent e75fac5 commit 35f821e

File tree

2 files changed

+273
-2
lines changed

2 files changed

+273
-2
lines changed

Sources/SourceKitLSP/Swift/MacroExpansion.swift

Lines changed: 1 addition & 1 deletion
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

Tests/SourceKitLSPTests/ExecuteCommandTests.swift

Lines changed: 272 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,278 @@ 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+
var serverOptions = SourceKitLSPServer.Options.testDefault
265+
serverOptions.experimentalFeatures.insert(.showMacroExpansions)
266+
267+
let project = try await SwiftPMTestProject(
268+
files: [
269+
"MyMacros/MyMacros.swift": #"""
270+
import SwiftCompilerPlugin
271+
import SwiftSyntax
272+
import SwiftSyntaxBuilder
273+
import SwiftSyntaxMacros
274+
275+
public struct DictionaryStorageMacro {}
276+
277+
extension DictionaryStorageMacro: MemberMacro {
278+
public static func expansion(
279+
of node: AttributeSyntax,
280+
providingMembersOf declaration: some DeclGroupSyntax,
281+
in context: some MacroExpansionContext
282+
) throws -> [DeclSyntax] {
283+
return ["\n var _storage: [String: Any] = [:]"]
284+
}
285+
}
286+
287+
extension VariableDeclSyntax {
288+
var isStoredProperty: Bool {
289+
if bindings.count != 1 {
290+
return false
291+
}
292+
293+
let binding = bindings.first!
294+
switch binding.accessorBlock?.accessors {
295+
case .none:
296+
return true
297+
298+
case .accessors(let accessors):
299+
for accessor in accessors {
300+
switch accessor.accessorSpecifier.tokenKind {
301+
case .keyword(.willSet), .keyword(.didSet):
302+
// Observers can occur on a stored property.
303+
break
304+
305+
default:
306+
// Other accessors make it a computed property.
307+
return false
308+
}
309+
}
310+
311+
return true
312+
313+
case .getter:
314+
return false
315+
}
316+
}
317+
}
318+
319+
extension DictionaryStorageMacro: MemberAttributeMacro {
320+
public static func expansion(
321+
of node: AttributeSyntax,
322+
attachedTo declaration: some DeclGroupSyntax,
323+
providingAttributesFor member: some DeclSyntaxProtocol,
324+
in context: some MacroExpansionContext
325+
) throws -> [AttributeSyntax] {
326+
guard let property = member.as(VariableDeclSyntax.self),
327+
property.isStoredProperty
328+
else {
329+
return []
330+
}
331+
332+
return [
333+
AttributeSyntax(
334+
leadingTrivia: [.newlines(1), .spaces(2)],
335+
attributeName: IdentifierTypeSyntax(
336+
name: .identifier("DictionaryStorageProperty")
337+
)
338+
)
339+
]
340+
}
341+
}
342+
343+
enum CustomError: Error, CustomStringConvertible {
344+
case message(String)
345+
346+
var description: String {
347+
switch self {
348+
case .message(let text):
349+
return text
350+
}
351+
}
352+
}
353+
354+
public struct DictionaryStoragePropertyMacro: AccessorMacro {
355+
public static func expansion<
356+
Context: MacroExpansionContext,
357+
Declaration: DeclSyntaxProtocol
358+
>(
359+
of node: AttributeSyntax,
360+
providingAccessorsOf declaration: Declaration,
361+
in context: Context
362+
) throws -> [AccessorDeclSyntax] {
363+
guard let varDecl = declaration.as(VariableDeclSyntax.self),
364+
let binding = varDecl.bindings.first,
365+
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
366+
binding.accessorBlock == nil,
367+
let type = binding.typeAnnotation?.type
368+
else {
369+
return []
370+
}
371+
372+
// Ignore the "_storage" variable.
373+
if identifier.text == "_storage" {
374+
return []
375+
}
376+
377+
guard let defaultValue = binding.initializer?.value else {
378+
throw CustomError.message("stored property must have an initializer")
379+
}
380+
381+
return [
382+
"""
383+
get {
384+
_storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type)
385+
}
386+
""",
387+
"""
388+
set {
389+
_storage[\(literal: identifier.text)] = newValue
390+
}
391+
""",
392+
]
393+
}
394+
}
395+
396+
@main
397+
struct MyMacroPlugin: CompilerPlugin {
398+
let providingMacros: [Macro.Type] = [
399+
DictionaryStorageMacro.self,
400+
DictionaryStoragePropertyMacro.self
401+
]
402+
}
403+
"""#,
404+
"MyMacroClient/MyMacroClient.swift": #"""
405+
@attached(memberAttribute)
406+
@attached(member, names: named(_storage))
407+
public macro DictionaryStorage() = #externalMacro(module: "MyMacros", type: "DictionaryStorageMacro")
408+
409+
@attached(accessor)
410+
public macro DictionaryStorageProperty() =
411+
#externalMacro(module: "MyMacros", type: "DictionaryStoragePropertyMacro")
412+
413+
1️⃣@2️⃣DictionaryStorage3️⃣
414+
struct Point {
415+
var x: Int = 1
416+
var y: Int = 2
417+
}
418+
419+
var point = Point()
420+
print("Point storage begins as an empty dictionary: \(point)")
421+
print("Default value for point.x: \(point.x)")
422+
point.y = 17
423+
print("Point storage contains only the value we set: \(point)")
424+
"""#,
425+
],
426+
manifest: SwiftPMTestProject.macroPackageManifest,
427+
serverOptions: serverOptions
428+
)
429+
try await SwiftPMTestProject.build(at: project.scratchDirectory)
430+
431+
let (uri, positions) = try project.openDocument("MyMacroClient.swift")
432+
433+
let positionMarkersToBeTested = [
434+
(start: "1️⃣", end: "1️⃣"),
435+
(start: "2️⃣", end: "2️⃣"),
436+
(start: "1️⃣", end: "3️⃣"),
437+
(start: "2️⃣", end: "3️⃣"),
438+
]
439+
440+
for positionMarker in positionMarkersToBeTested {
441+
let args = ExpandMacroCommand(
442+
positionRange: positions[positionMarker.start]..<positions[positionMarker.end],
443+
textDocument: TextDocumentIdentifier(uri)
444+
)
445+
446+
let metadata = SourceKitLSPCommandMetadata(textDocument: TextDocumentIdentifier(uri))
447+
448+
var command = args.asCommand()
449+
command.arguments?.append(metadata.encodeToLSPAny())
450+
451+
let request = ExecuteCommandRequest(command: command.command, arguments: command.arguments)
452+
453+
let expectations = [
454+
self.expectation(description: "Handle Show Document Request #1"),
455+
self.expectation(description: "Handle Show Document Request #2"),
456+
self.expectation(description: "Handle Show Document Request #3"),
457+
]
458+
459+
let showDocumentRequestURIs = ThreadSafeBox<[DocumentURI?]>(initialValue: [nil, nil, nil])
460+
461+
for i in 0...2 {
462+
project.testClient.handleSingleRequest { (req: ShowDocumentRequest) in
463+
showDocumentRequestURIs.value[i] = req.uri
464+
expectations[i].fulfill()
465+
return ShowDocumentResponse(success: true)
466+
}
467+
}
468+
469+
let result = try await project.testClient.send(request)
470+
471+
guard let resultArray: [RefactoringEdit] = Array(fromLSPArray: result ?? .null) else {
472+
XCTFail(
473+
"Result is not an array. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
474+
)
475+
return
476+
}
477+
478+
XCTAssertEqual(
479+
resultArray.count,
480+
4,
481+
"resultArray count is not equal to four. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
482+
)
483+
484+
XCTAssertEqual(
485+
resultArray.map {
486+
$0.newText
487+
}.sorted(),
488+
[
489+
"",
490+
"@DictionaryStorageProperty",
491+
"@DictionaryStorageProperty",
492+
"var _storage: [String: Any] = [:]",
493+
].sorted(),
494+
"Wrong macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
495+
)
496+
497+
try await fulfillmentOfOrThrow(expectations)
498+
499+
let urls = try showDocumentRequestURIs.value.map {
500+
try XCTUnwrap(
501+
$0?.fileURL,
502+
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
503+
)
504+
}
505+
506+
let filesContents = try urls.map {
507+
try String(contentsOf: $0, encoding: .utf8)
508+
}
509+
510+
XCTAssertEqual(
511+
filesContents.sorted(),
512+
[
513+
"@DictionaryStorageProperty",
514+
"@DictionaryStorageProperty",
515+
"var _storage: [String: Any] = [:]",
516+
].sorted(),
517+
"Files doesn't contain correct macro expansion. Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
518+
)
519+
520+
XCTAssertEqual(
521+
urls.map { $0.lastPathComponent }.sorted(),
522+
[
523+
"MyMacroClient_L11C3-L11C3.swift",
524+
"MyMacroClient_L12C3-L12C3.swift",
525+
"MyMacroClient_L13C1-L13C1.swift",
526+
].sorted(),
256527
"Failed for position range between \(positionMarker.start) and \(positionMarker.end)"
257528
)
258529
}

0 commit comments

Comments
 (0)