@@ -252,7 +252,278 @@ final class ExecuteCommandTests: XCTestCase {
252
252
253
253
XCTAssertEqual (
254
254
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 ( ) ,
256
527
" Failed for position range between \( positionMarker. start) and \( positionMarker. end) "
257
528
)
258
529
}
0 commit comments