@@ -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