├── Makefile
├── .editorconfig
├── .gitignore
├── Tests
└── MacroTestingTests
│ ├── BaseTestCase.swift
│ ├── FuncUniqueMacroTests.swift
│ ├── EquatableExtensionMacroTests.swift
│ ├── StringifyMacroTests.swift
│ ├── FontLiteralMacroTests.swift
│ ├── MacroExamples
│ ├── EntryMacro.swift
│ ├── MemberDeprecatedMacro.swift
│ ├── FuncUniqueMacro.swift
│ ├── Diagnostics.swift
│ ├── PeerValueWithSuffixMacro.swift
│ ├── EquatableExtensionMacro.swift
│ ├── StringifyMacro.swift
│ ├── URLMacro.swift
│ ├── CaseDetectionMacro.swift
│ ├── FontLiteralMacro.swift
│ ├── WarningMacro.swift
│ ├── DiagnosticsAndFixitsEmitterMacro.swift
│ ├── CustomCodable.swift
│ ├── NewTypeMacro.swift
│ ├── DefaultFatalErrorImplementationMacro.swift
│ ├── DictionaryIndirectionMacro.swift
│ ├── AddBlocker.swift
│ ├── WrapStoredPropertiesMacro.swift
│ ├── MetaEnumMacro.swift
│ ├── ObservableMacro.swift
│ ├── AddCompletionHandlerMacro.swift
│ ├── AddAsyncMacro.swift
│ └── OptionSetMacro.swift
│ ├── CaseDetectionMacroTests.swift
│ ├── AddBlockerTests.swift
│ ├── AssertMacroTests.swift
│ ├── MemberDeprecatedMacroTests.swift
│ ├── DiagnosticsAndFixitsEmitterMacroTests.swift
│ ├── EntryMacroTests.swift
│ ├── URLMacroTests.swift
│ ├── PeerValueWithSuffixMacroTests.swift
│ ├── MacroNameTests.swift
│ ├── WarningMacroTests.swift
│ ├── CustomCodableMacroTests.swift
│ ├── SwiftTestingTests.swift
│ ├── DefaultFatalErrorImplementationMacroTests.swift
│ ├── NewTypeMacroTests.swift
│ ├── WrapStoredPropertiesMacroTests.swift
│ ├── DictionaryStorageMacroTests.swift
│ ├── IndentationWidthTests.swift
│ ├── FixItTests.swift
│ ├── AddCompletionHandlerTests.swift
│ ├── MetaEnumMacroTests.swift
│ ├── AddAsyncTests.swift
│ ├── ObservableMacroTests.swift
│ └── OptionSetMacroTests.swift
├── .swiftpm
├── xcode
│ └── package.xcworkspace
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── configuration
│ └── Package.resolved
├── .spi.yml
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.yml
├── workflows
│ ├── format.yml
│ ├── ci.yml
│ └── release.yml
└── CODE_OF_CONDUCT.md
├── Sources
└── MacroTesting
│ ├── Documentation.docc
│ ├── AssertMacro.md
│ ├── WithMacroTesting.md
│ └── MacroTesting.md
│ ├── Internal
│ ├── RecordIssue.swift
│ ├── Diagnostic+UnderlineHighlights.swift
│ └── Deprecations.swift
│ ├── SwiftSyntax
│ └── SourceEdit.swift
│ ├── _SwiftSyntaxTestSupport
│ └── FixItApplier.swift
│ ├── MacrosTestTrait.swift
│ └── SwiftDiagnostics
│ └── DiagnosticsFormatter.swift
├── LICENSE
├── Package@swift-5.9.swift
├── Package.swift
├── Package.resolved
└── README.md
/Makefile:
--------------------------------------------------------------------------------
1 | format:
2 | swift format \
3 | --ignore-unparsable-files \
4 | --in-place \
5 | --recursive \
6 | ./Package.swift ./Sources ./Tests
7 |
8 | .PHONY: format
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 2
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/BaseTestCase.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | class BaseTestCase: XCTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | record: .missing
8 | ) {
9 | super.invokeTest()
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.spi.yml:
--------------------------------------------------------------------------------
1 | # This is manifest file for the Swift Package Index for it to auto-generate and
2 | # host DocC documentation.
3 | #
4 | # For reference see https://swiftpackageindex.com/swiftpackageindex/spimanifest/documentation/spimanifest/commonusecases#Host-DocC-documentation-in-the-Swift-Package-Index
5 |
6 | version: 1
7 | builder:
8 | configs:
9 | - documentation_targets:
10 | # First item in the list is the "landing" (default) target
11 | - MacroTesting
12 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/FuncUniqueMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class FuncUniqueMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | macros: [FuncUniqueMacro.self]
8 | ) {
9 | super.invokeTest()
10 | }
11 | }
12 |
13 | func testExpansionCreatesDeclarationWithUniqueFunction() {
14 | assertMacro {
15 | """
16 | #FuncUnique()
17 | """
18 | } expansion: {
19 | """
20 | class MyClass {
21 | func __macro_local_6uniquefMu_() {
22 | }
23 | }
24 | """
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
3 | contact_links:
4 | - name: Project Discussion
5 | url: https://github.com/pointfreeco/swift-macro-testing/discussions
6 | about: Library Q&A, ideas, and more
7 | - name: Documentation
8 | url: https://swiftpackageindex.com/pointfreeco/swift-macro-testing/main/documentation
9 | about: Read the documentation
10 | - name: Videos
11 | url: https://www.pointfree.co/episodes/ep250-testing-debugging-macros-part-1
12 | about: Watch videos to get a behind-the-scenes look at how this library was motivated and built
13 | - name: Slack
14 | url: https://www.pointfree.co/slack-invite
15 | about: Community chat
16 |
--------------------------------------------------------------------------------
/.swiftpm/configuration/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-snapshot-testing",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
7 | "state" : {
8 | "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c",
9 | "version" : "1.17.4"
10 | }
11 | },
12 | {
13 | "identity" : "swift-syntax",
14 | "kind" : "remoteSourceControl",
15 | "location" : "https://github.com/swiftlang/swift-syntax",
16 | "state" : {
17 | "revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
18 | "version" : "510.0.2"
19 | }
20 | }
21 | ],
22 | "version" : 2
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Documentation.docc/AssertMacro.md:
--------------------------------------------------------------------------------
1 | # ``MacroTesting/assertMacro(_:indentationWidth:record:of:diagnostics:fixes:expansion:fileID:file:function:line:column:)-8zqk4``
2 |
3 | ## Topics
4 |
5 | ### Overloads
6 |
7 | - ``assertMacro(_:record:of:matches:fileID:file:function:line:column:)-2wi38``
8 | - ``assertMacro(_:indentationWidth:record:of:diagnostics:fixes:expansion:fileID:file:function:line:column:)-90l38``
9 |
10 | ### Deprecations
11 |
12 | - ``assertMacro(_:record:of:matches:fileID:file:function:line:column:)-6vxvm``
13 | - ``assertMacro(_:applyFixIts:record:of:matches:fileID:file:function:line:column:)-4381w``
14 | - ``assertMacro(_:applyFixIts:record:of:matches:fileID:file:function:line:column:)-9mzoj``
15 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Documentation.docc/WithMacroTesting.md:
--------------------------------------------------------------------------------
1 | # ``MacroTesting/withMacroTesting(indentationWidth:record:macros:operation:)-6ayf5``
2 |
3 | ## Topics
4 |
5 | ### Overloads
6 |
7 | - ``withMacroTesting(indentationWidth:record:macros:operation:)-7cm1s``
8 | - ``withMacroTesting(indentationWidth:record:macros:operation:)-5a7qi``
9 | - ``withMacroTesting(indentationWidth:record:macros:operation:)-9ghea``
10 |
11 | ### Deprecations
12 |
13 | - ``withMacroTesting(indentationWidth:isRecording:macros:operation:)-1yql2``
14 | - ``withMacroTesting(indentationWidth:isRecording:macros:operation:)-9du8s``
15 | - ``withMacroTesting(indentationWidth:isRecording:macros:operation:)-91prk``
16 | - ``withMacroTesting(indentationWidth:isRecording:macros:operation:)-5id9j``
17 |
--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------
1 | name: Format
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | concurrency:
9 | group: format-${{ github.ref }}
10 | cancel-in-progress: true
11 |
12 | jobs:
13 | swift_format:
14 | name: swift-format
15 | runs-on: macos-13
16 | steps:
17 | - uses: actions/checkout@v3
18 | - name: Xcode Select
19 | run: sudo xcode-select -s /Applications/Xcode_14.3.1.app
20 | - name: Tap
21 | run: brew install swift-format
22 | - name: Format
23 | run: make format
24 | - uses: stefanzweifel/git-auto-commit-action@v4
25 | with:
26 | commit_message: Run swift-format
27 | branch: 'main'
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/EquatableExtensionMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class EquatableExtensionMacroTests: XCTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["equatable": EquatableExtensionMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsExtensionWithEquatableConformance() {
12 | assertMacro {
13 | """
14 | @equatable
15 | final public class Message {
16 | let text: String
17 | let sender: String
18 | }
19 | """
20 | } expansion: {
21 | """
22 | final public class Message {
23 | let text: String
24 | let sender: String
25 | }
26 |
27 | extension Message: Equatable {
28 | }
29 | """
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/StringifyMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class StringifyMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [StringifyMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionWithBasicArithmeticExpression() {
12 | assertMacro {
13 | """
14 | let a = #stringify(x + y)
15 | """
16 | } expansion: {
17 | """
18 | let a = (x + y, "x + y")
19 | """
20 | }
21 | }
22 |
23 | func testExpansionWithStringInterpolation() {
24 | assertMacro {
25 | #"""
26 | let b = #stringify("Hello, \(name)")
27 | """#
28 | } expansion: {
29 | #"""
30 | let b = ("Hello, \(name)", #""Hello, \(name)""#)
31 | """#
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Internal/RecordIssue.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if canImport(Testing)
4 | import Testing
5 | #endif
6 |
7 | @_spi(Internals)
8 | public func recordIssue(
9 | _ message: @autoclosure () -> String,
10 | fileID: StaticString,
11 | filePath: StaticString,
12 | line: UInt,
13 | column: UInt
14 | ) {
15 | #if canImport(Testing)
16 | if Test.current != nil {
17 | Issue.record(
18 | Comment(rawValue: message()),
19 | sourceLocation: SourceLocation(
20 | fileID: fileID.description,
21 | filePath: filePath.description,
22 | line: Int(line),
23 | column: Int(column)
24 | )
25 | )
26 | } else {
27 | XCTFail(message(), file: filePath, line: line)
28 | }
29 | #else
30 | XCTFail(message(), file: filePath, line: line)
31 | #endif
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/FontLiteralMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class FontLiteralMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [FontLiteralMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionWithNamedArguments() {
12 | assertMacro {
13 | """
14 | #fontLiteral(name: "Comic Sans", size: 14, weight: .thin)
15 | """
16 | } expansion: {
17 | """
18 | .init(fontLiteralName: "Comic Sans", size: 14, weight: .thin)
19 | """
20 | }
21 | }
22 |
23 | func testExpansionWithUnlabeledFirstArgument() {
24 | assertMacro {
25 | """
26 | #fontLiteral("Copperplate Gothic", size: 69, weight: .bold)
27 | """
28 | } expansion: {
29 | """
30 | .init(fontLiteralName: "Copperplate Gothic", size: 69, weight: .bold)
31 | """
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/EntryMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftSyntax
2 | import SwiftSyntaxMacros
3 |
4 | // Not complete, just enough to unit test lexical context.
5 | public struct EntryMacro: AccessorMacro {
6 | public static func expansion(
7 | of node: AttributeSyntax,
8 | providingAccessorsOf declaration: some DeclSyntaxProtocol,
9 | in context: some MacroExpansionContext
10 | ) throws -> [AccessorDeclSyntax] {
11 | let isInEnvironmentValues = context.lexicalContext.contains { lexicalContext in
12 | lexicalContext.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription
13 | == "EnvironmentValues"
14 | }
15 |
16 | guard isInEnvironmentValues else {
17 | throw MacroExpansionErrorMessage(
18 | "'@Entry' macro can only attach to var declarations inside extensions of EnvironmentValues")
19 | }
20 |
21 | return [
22 | AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) {
23 | "fatalError()"
24 | }
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/CaseDetectionMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class CaseDetectionMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [CaseDetectionMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsComputedProperties() {
12 | assertMacro {
13 | """
14 | @CaseDetection
15 | enum Animal {
16 | case dog
17 | case cat(curious: Bool)
18 | }
19 | """
20 | } expansion: {
21 | """
22 | enum Animal {
23 | case dog
24 | case cat(curious: Bool)
25 |
26 | var isDog: Bool {
27 | if case .dog = self {
28 | return true
29 | }
30 |
31 | return false
32 | }
33 |
34 | var isCat: Bool {
35 | if case .cat = self {
36 | return true
37 | }
38 |
39 | return false
40 | }
41 | }
42 | """
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/AddBlockerTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class AddBlockerTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [AddBlocker.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionTransformsAdditionToSubtractionAndEmitsWarning() {
12 | assertMacro {
13 | """
14 | #addBlocker(x * y + z)
15 | """
16 | } diagnostics: {
17 | """
18 | #addBlocker(x * y + z)
19 | ───── ┬ ─
20 | ╰─ ⚠️ blocked an add; did you mean to subtract?
21 | ✏️ use '-'
22 | """
23 | } fixes: {
24 | """
25 | #addBlocker(x * y - z)
26 | """
27 | } expansion: {
28 | """
29 | x * y - z
30 | """
31 | }
32 | }
33 |
34 | func testExpansionPreservesSubtraction() {
35 | assertMacro {
36 | """
37 | #addBlocker(x * y - z)
38 | """
39 | } expansion: {
40 | """
41 | x * y - z
42 | """
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/AssertMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class AssertMacroTests: BaseTestCase {
5 | #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) || os(watchOS)
6 | func testMacrosRequired() {
7 | XCTExpectFailure {
8 | assertMacro {
9 | """
10 | #forgotToConfigure()
11 | """
12 | }
13 | } issueMatcher: {
14 | $0.compactDescription == """
15 | failed - No macros configured for this assertion. Pass a mapping to this function, e.g.:
16 |
17 | assertMacro(["stringify": StringifyMacro.self]) { … }
18 |
19 | Or wrap your assertion using 'withMacroTesting', e.g. in 'invokeTest':
20 |
21 | class StringifyMacroTests: XCTestCase {
22 | override func invokeTest() {
23 | withMacroTesting(macros: ["stringify": StringifyMacro.self]) {
24 | super.invokeTest()
25 | }
26 | }
27 | …
28 | }
29 | """
30 | }
31 | }
32 | #endif
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/MemberDeprecatedMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | /// Add '@available(*, deprecated)' to members.
17 | public enum MemberDeprecatedMacro: MemberAttributeMacro {
18 | public static func expansion(
19 | of node: AttributeSyntax,
20 | attachedTo declaration: some DeclGroupSyntax,
21 | providingAttributesFor member: some DeclSyntaxProtocol,
22 | in context: some MacroExpansionContext
23 | ) throws -> [AttributeSyntax] {
24 | return ["@available(*, deprecated)"]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MemberDeprecatedMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class MemberDepreacatedMacroTests: XCTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["memberDeprecated": MemberDeprecatedMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionMarksMembersAsDeprecated() {
12 | assertMacro {
13 | """
14 | @memberDeprecated
15 | public struct SomeStruct {
16 | typealias MacroName = String
17 |
18 | public var oldProperty: Int = 420
19 |
20 | func oldMethod() {
21 | print("This is an old method.")
22 | }
23 | }
24 | """
25 | } expansion: {
26 | """
27 | public struct SomeStruct {
28 | @available(*, deprecated)
29 | typealias MacroName = String
30 | @available(*, deprecated)
31 |
32 | public var oldProperty: Int = 420
33 | @available(*, deprecated)
34 |
35 | func oldMethod() {
36 | print("This is an old method.")
37 | }
38 | }
39 | """
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Point-Free
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/FuncUniqueMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxBuilder
15 | import SwiftSyntaxMacros
16 |
17 | /// Func With unique name.
18 | public enum FuncUniqueMacro: DeclarationMacro {
19 | public static func expansion(
20 | of node: some FreestandingMacroExpansionSyntax,
21 | in context: some MacroExpansionContext
22 | ) throws -> [DeclSyntax] {
23 | let name = context.makeUniqueName("unique")
24 | return [
25 | """
26 | class MyClass {
27 | func \(name)() {}
28 | }
29 | """
30 | ]
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/DiagnosticsAndFixitsEmitterMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class DiagnosticsAndFixitsEmitterMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [DiagnosticsAndFixitsEmitterMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionEmitsDiagnosticsAndFixits() {
12 | assertMacro {
13 | """
14 | @DiagnosticsAndFixitsEmitter
15 | struct FooBar {
16 | let foo: Foo
17 | let bar: Bar
18 | }
19 | """
20 | } diagnostics: {
21 | """
22 | @DiagnosticsAndFixitsEmitter
23 | ┬──────────────────────────
24 | ├─ ⚠️ This is the first diagnostic.
25 | │ ✏️ This is the first fix-it.
26 | │ ✏️ This is the second fix-it.
27 | ╰─ ℹ️ This is the second diagnostic, it's a note.
28 | struct FooBar {
29 | let foo: Foo
30 | let bar: Bar
31 | }
32 | """
33 | } expansion: {
34 | """
35 | struct FooBar {
36 | let foo: Foo
37 | let bar: Bar
38 | }
39 | """
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/Diagnostics.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 |
16 | struct SimpleDiagnosticMessage: DiagnosticMessage, Error {
17 | let message: String
18 | let diagnosticID: MessageID
19 | let severity: DiagnosticSeverity
20 | }
21 |
22 | extension SimpleDiagnosticMessage: FixItMessage {
23 | var fixItID: MessageID { diagnosticID }
24 | }
25 |
26 | enum CustomError: Error, CustomStringConvertible {
27 | case message(String)
28 |
29 | var description: String {
30 | switch self {
31 | case .message(let text):
32 | return text
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/PeerValueWithSuffixMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | /// Peer 'var' with the name suffixed with '_peer'.
17 | public enum PeerValueWithSuffixNameMacro: PeerMacro {
18 | public static func expansion(
19 | of node: AttributeSyntax,
20 | providingPeersOf declaration: some DeclSyntaxProtocol,
21 | in context: some MacroExpansionContext
22 | ) throws -> [DeclSyntax] {
23 | guard let identified = declaration.asProtocol(NamedDeclSyntax.self) else {
24 | return []
25 | }
26 | return ["var \(raw: identified.name.text)_peer: Int { 1 }"]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/EntryMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class EntryMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | macros: [
8 | EntryMacro.self
9 | ]
10 | ) {
11 | super.invokeTest()
12 | }
13 | }
14 |
15 | func testWithinEnvironmentValues() {
16 | assertMacro {
17 | """
18 | extension EnvironmentValues {
19 | @Entry var x: String = ""
20 | }
21 | """
22 | } expansion: {
23 | """
24 | extension EnvironmentValues {
25 | var x: String {
26 | get {
27 | fatalError()
28 | }
29 | }
30 | }
31 | """
32 | }
33 | }
34 |
35 | func testNotWithinEnvironmentValues() {
36 | assertMacro {
37 | """
38 | extension String {
39 | @Entry var x: String = ""
40 | }
41 | """
42 | } diagnostics: {
43 | """
44 | extension String {
45 | @Entry var x: String = ""
46 | ┬─────
47 | ╰─ 🛑 '@Entry' macro can only attach to var declarations inside extensions of EnvironmentValues
48 | }
49 | """
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/EquatableExtensionMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | public enum EquatableExtensionMacro: ExtensionMacro {
17 | public static func expansion(
18 | of node: AttributeSyntax,
19 | attachedTo declaration: some DeclGroupSyntax,
20 | providingExtensionsOf type: some TypeSyntaxProtocol,
21 | conformingTo protocols: [TypeSyntax],
22 | in context: some MacroExpansionContext
23 | ) throws -> [ExtensionDeclSyntax] {
24 | let equatableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Equatable {}")
25 |
26 | return [equatableExtension]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Package@swift-5.9.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "swift-macro-testing",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15),
10 | .tvOS(.v13),
11 | .watchOS(.v6),
12 | ],
13 | products: [
14 | .library(
15 | name: "MacroTesting",
16 | targets: ["MacroTesting"]
17 | )
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.0"),
21 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "MacroTesting",
26 | dependencies: [
27 | .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
28 | .product(name: "SwiftDiagnostics", package: "swift-syntax"),
29 | .product(name: "SwiftOperators", package: "swift-syntax"),
30 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
31 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
32 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
33 | ]
34 | ),
35 | .testTarget(
36 | name: "MacroTestingTests",
37 | dependencies: [
38 | "MacroTesting"
39 | ]
40 | ),
41 | ]
42 | )
43 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/URLMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class URLMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["URL": URLMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionWithMalformedURLEmitsError() {
12 | assertMacro {
13 | """
14 | let invalid = #URL("https://not a url.com")
15 | """
16 | } diagnostics: {
17 | """
18 | let invalid = #URL("https://not a url.com")
19 | ┬────────────────────────────
20 | ╰─ 🛑 malformed url: "https://not a url.com"
21 | """
22 | }
23 | }
24 |
25 | func testExpansionWithStringInterpolationEmitsError() {
26 | assertMacro {
27 | #"""
28 | #URL("https://\(domain)/api/path")
29 | """#
30 | } diagnostics: {
31 | #"""
32 | #URL("https://\(domain)/api/path")
33 | ┬─────────────────────────────────
34 | ╰─ 🛑 #URL requires a static string literal
35 | """#
36 | }
37 | }
38 |
39 | func testExpansionWithValidURL() {
40 | assertMacro {
41 | """
42 | let valid = #URL("https://swift.org/")
43 | """
44 | } expansion: {
45 | """
46 | let valid = URL(string: "https://swift.org/")!
47 | """
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 6.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "swift-macro-testing",
7 | platforms: [
8 | .iOS(.v13),
9 | .macOS(.v10_15),
10 | .tvOS(.v13),
11 | .watchOS(.v6),
12 | ],
13 | products: [
14 | .library(
15 | name: "MacroTesting",
16 | targets: ["MacroTesting"]
17 | )
18 | ],
19 | dependencies: [
20 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.4"),
21 | .package(url: "https://github.com/swiftlang/swift-syntax", "509.0.0"..<"603.0.0"),
22 | ],
23 | targets: [
24 | .target(
25 | name: "MacroTesting",
26 | dependencies: [
27 | .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
28 | .product(name: "SwiftDiagnostics", package: "swift-syntax"),
29 | .product(name: "SwiftOperators", package: "swift-syntax"),
30 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
31 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
32 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
33 | ]
34 | ),
35 | .testTarget(
36 | name: "MacroTestingTests",
37 | dependencies: [
38 | "MacroTesting"
39 | ]
40 | ),
41 | ],
42 | swiftLanguageModes: [.v5]
43 | )
44 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/PeerValueWithSuffixMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class PeerValueWithSuffixNameMacroTests: XCTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [PeerValueWithSuffixNameMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsPeerValueToPrivateActor() {
12 | assertMacro {
13 | """
14 | @PeerValueWithSuffixName
15 | private actor Counter {
16 | var value = 0
17 | }
18 | """
19 | } expansion: {
20 | """
21 | private actor Counter {
22 | var value = 0
23 | }
24 |
25 | var Counter_peer: Int {
26 | 1
27 | }
28 | """
29 | }
30 | }
31 |
32 | func testExpansionAddsPeerValueToFunction() {
33 | assertMacro {
34 | """
35 | @PeerValueWithSuffixName
36 | func someFunction() {}
37 | """
38 | } expansion: {
39 | """
40 | func someFunction() {}
41 |
42 | var someFunction_peer: Int {
43 | 1
44 | }
45 | """
46 | }
47 | }
48 |
49 | func testExpansionIgnoresVariables() {
50 | assertMacro {
51 | """
52 | @PeerValueWithSuffixName
53 | var someVariable: Int
54 | """
55 | } expansion: {
56 | """
57 | var someVariable: Int
58 | """
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroNameTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | @testable import MacroTesting
4 |
5 | final class MacroNameTests: BaseTestCase {
6 | func testBasics() {
7 | XCTAssertEqual(
8 | macroName(className: "AddAsyncHandler", isExpression: false),
9 | "AddAsyncHandler"
10 | )
11 | XCTAssertEqual(
12 | macroName(className: "AddAsyncHandlerMacro", isExpression: false),
13 | "AddAsyncHandler"
14 | )
15 | XCTAssertEqual(
16 | macroName(className: "URL", isExpression: false),
17 | "URL"
18 | )
19 | XCTAssertEqual(
20 | macroName(className: "URLMacro", isExpression: false),
21 | "URL"
22 | )
23 | XCTAssertEqual(
24 | macroName(className: "URL", isExpression: true),
25 | "url"
26 | )
27 | XCTAssertEqual(
28 | macroName(className: "URLMacro", isExpression: true),
29 | "url"
30 | )
31 | XCTAssertEqual(
32 | macroName(className: "URLComponents", isExpression: true),
33 | "urlComponents"
34 | )
35 | XCTAssertEqual(
36 | macroName(className: "URLComponentsMacro", isExpression: true),
37 | "urlComponents"
38 | )
39 | XCTAssertEqual(
40 | macroName(className: "FontLiteral", isExpression: true),
41 | "fontLiteral"
42 | )
43 | XCTAssertEqual(
44 | macroName(className: "FontLiteralMacro", isExpression: true),
45 | "fontLiteral"
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "db288110c168673c49d51156064e9cf0a6c0719e024eb30dd405e28fe59a65ec",
3 | "pins" : [
4 | {
5 | "identity" : "swift-custom-dump",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/pointfreeco/swift-custom-dump",
8 | "state" : {
9 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
10 | "version" : "1.3.3"
11 | }
12 | },
13 | {
14 | "identity" : "swift-snapshot-testing",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing",
17 | "state" : {
18 | "revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b",
19 | "version" : "1.18.7"
20 | }
21 | },
22 | {
23 | "identity" : "swift-syntax",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/swiftlang/swift-syntax",
26 | "state" : {
27 | "revision" : "4799286537280063c85a32f09884cfbca301b1a1",
28 | "version" : "602.0.0"
29 | }
30 | },
31 | {
32 | "identity" : "xctest-dynamic-overlay",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
35 | "state" : {
36 | "revision" : "b2ed9eabefe56202ee4939dd9fc46b6241c88317",
37 | "version" : "1.6.1"
38 | }
39 | }
40 | ],
41 | "version" : 3
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/WarningMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class WarningMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["myWarning": WarningMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionWithValidStringLiteralEmitsWarning() {
12 | assertMacro {
13 | """
14 | #myWarning("This is a warning")
15 | """
16 | } diagnostics: {
17 | """
18 | #myWarning("This is a warning")
19 | ┬──────────────────────────────
20 | ╰─ ⚠️ This is a warning
21 | """
22 | } expansion: {
23 | """
24 | ()
25 | """
26 | }
27 | }
28 |
29 | func testExpansionWithInvalidExpressionEmitsError() {
30 | assertMacro {
31 | """
32 | #myWarning(42)
33 | """
34 | } diagnostics: {
35 | """
36 | #myWarning(42)
37 | ┬─────────────
38 | ╰─ 🛑 #myWarning macro requires a string literal
39 | """
40 | }
41 | }
42 |
43 | func testExpansionWithStringInterpolationEmitsError() {
44 | assertMacro {
45 | #"""
46 | #myWarning("Say hello \(number) times!")
47 | """#
48 | } diagnostics: {
49 | #"""
50 | #myWarning("Say hello \(number) times!")
51 | ┬───────────────────────────────────────
52 | ╰─ 🛑 #myWarning macro requires a string literal
53 | """#
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/CustomCodableMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class CustomCodableMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [CodableKey.self, CustomCodable.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsDefaultCodingKeys() {
12 | assertMacro {
13 | """
14 | @CustomCodable
15 | struct Person {
16 | let name: String
17 | let age: Int
18 | }
19 | """
20 | } expansion: {
21 | """
22 | struct Person {
23 | let name: String
24 | let age: Int
25 |
26 | enum CodingKeys: String, CodingKey {
27 | case name
28 | case age
29 | }
30 | }
31 | """
32 | }
33 | }
34 |
35 | func testExpansionWithCodableKeyAddsCustomCodingKeys() {
36 | assertMacro {
37 | """
38 | @CustomCodable
39 | struct Person {
40 | let name: String
41 | @CodableKey("user_age") let age: Int
42 |
43 | func randomFunction() {}
44 | }
45 | """
46 | } expansion: {
47 | """
48 | struct Person {
49 | let name: String
50 | let age: Int
51 |
52 | func randomFunction() {}
53 |
54 | enum CodingKeys: String, CodingKey {
55 | case name
56 | case age = "user_age"
57 | }
58 | }
59 | """
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/SwiftTestingTests.swift:
--------------------------------------------------------------------------------
1 | #if canImport(Testing)
2 | import MacroTesting
3 | import Testing
4 |
5 | @Suite(
6 | .macros(
7 | ["URL": URLMacro.self],
8 | record: .failed
9 | )
10 | )
11 | struct URLMacroSwiftTestingTests {
12 | @Test
13 | func expansionWithMalformedURLEmitsError() {
14 | assertMacro {
15 | """
16 | let invalid = #URL("https://not a url.com")
17 | """
18 | } diagnostics: {
19 | """
20 | let invalid = #URL("https://not a url.com")
21 | ┬────────────────────────────
22 | ╰─ 🛑 malformed url: "https://not a url.com"
23 | """
24 | }
25 | }
26 |
27 | @Test
28 | func expansionWithStringInterpolationEmitsError() {
29 | assertMacro {
30 | #"""
31 | #URL("https://\(domain)/api/path")
32 | """#
33 | } diagnostics: {
34 | #"""
35 | #URL("https://\(domain)/api/path")
36 | ┬─────────────────────────────────
37 | ╰─ 🛑 #URL requires a static string literal
38 | """#
39 | }
40 | }
41 |
42 | @Test
43 | func expansionWithValidURL() {
44 | assertMacro {
45 | """
46 | let valid = #URL("https://swift.org/")
47 | """
48 | } expansion: {
49 | """
50 | let valid = URL(string: "https://swift.org/")!
51 | """
52 | }
53 | }
54 | }
55 | #endif
56 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/StringifyMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxBuilder
15 | import SwiftSyntaxMacros
16 |
17 | /// Implementation of the `stringify` macro, which takes an expression
18 | /// of any type and produces a tuple containing the value of that expression
19 | /// and the source code that produced the value. For example
20 | ///
21 | /// #stringify(x + y)
22 | ///
23 | /// will expand to
24 | ///
25 | /// (x + y, "x + y")
26 | public enum StringifyMacro: ExpressionMacro {
27 | public static func expansion(
28 | of node: some FreestandingMacroExpansionSyntax,
29 | in context: some MacroExpansionContext
30 | ) -> ExprSyntax {
31 | guard let argument = node.arguments.first?.expression else {
32 | fatalError("compiler bug: the macro does not have any arguments")
33 | }
34 |
35 | return "(\(argument), \(literal: argument.description))"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/URLMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import Foundation
14 | import SwiftSyntax
15 | import SwiftSyntaxMacros
16 |
17 | /// Creates a non-optional URL from a static string. The string is checked to
18 | /// be valid during compile time.
19 | public enum URLMacro: ExpressionMacro {
20 | public static func expansion(
21 | of node: some FreestandingMacroExpansionSyntax,
22 | in context: some MacroExpansionContext
23 | ) throws -> ExprSyntax {
24 | guard let argument = node.arguments.first?.expression,
25 | let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
26 | segments.count == 1,
27 | case .stringSegment(let literalSegment)? = segments.first
28 | else {
29 | throw CustomError.message("#URL requires a static string literal")
30 | }
31 |
32 | guard URL(string: literalSegment.content.text) != nil else {
33 | throw CustomError.message("malformed url: \(argument)")
34 | }
35 |
36 | return "URL(string: \(argument))!"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/CaseDetectionMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | public enum CaseDetectionMacro: MemberMacro {
17 | public static func expansion(
18 | of node: AttributeSyntax,
19 | providingMembersOf declaration: some DeclGroupSyntax,
20 | in context: some MacroExpansionContext
21 | ) throws -> [DeclSyntax] {
22 | declaration.memberBlock.members
23 | .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
24 | .map { $0.elements.first!.name }
25 | .map { ($0, $0.initialUppercased) }
26 | .map { original, uppercased in
27 | """
28 | var is\(raw: uppercased): Bool {
29 | if case .\(raw: original) = self {
30 | return true
31 | }
32 |
33 | return false
34 | }
35 | """
36 | }
37 | }
38 | }
39 |
40 | extension TokenSyntax {
41 | fileprivate var initialUppercased: String {
42 | let name = self.text
43 | guard let initial = name.first else {
44 | return name
45 | }
46 |
47 | return "\(initial.uppercased())\(name.dropFirst())"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Internal/Diagnostic+UnderlineHighlights.swift:
--------------------------------------------------------------------------------
1 | import SwiftDiagnostics
2 | import SwiftSyntax
3 | import SwiftSyntaxMacroExpansion
4 |
5 | extension Array where Element == Diagnostic {
6 | func underlineHighlights(
7 | sourceString: String,
8 | lineNumber: Int,
9 | column: Int,
10 | context: BasicMacroExpansionContext
11 | ) -> String? {
12 | let (highlightColumns, highlightLineLength) = self.reduce(
13 | into: (highlightColumns: Set(), highlightLineLength: column + 1)
14 | ) { partialResult, diag in
15 | for highlight in diag.highlights {
16 | let startLocation = context.location(
17 | for: highlight.positionAfterSkippingLeadingTrivia, anchoredAt: diag.node, fileName: ""
18 | )
19 | let endLocation = context.location(
20 | for: highlight.endPositionBeforeTrailingTrivia, anchoredAt: diag.node, fileName: ""
21 | )
22 | guard
23 | startLocation.line == lineNumber,
24 | startLocation.line == endLocation.line,
25 | sourceString.contains(diag.node.trimmedDescription)
26 | else { continue }
27 | partialResult.highlightColumns.formUnion(startLocation.column.. ExprSyntax {
24 | let argList = replaceFirstLabel(
25 | of: node.arguments,
26 | with: "fontLiteralName"
27 | )
28 | return ".init(\(argList))"
29 | }
30 | }
31 |
32 | /// Replace the label of the first element in the tuple with the given
33 | /// new label.
34 | private func replaceFirstLabel(
35 | of tuple: LabeledExprListSyntax,
36 | with newLabel: String
37 | ) -> LabeledExprListSyntax {
38 | if tuple.isEmpty {
39 | return tuple
40 | }
41 |
42 | var tuple = tuple
43 | tuple[tuple.startIndex].label = .identifier(newLabel)
44 | tuple[tuple.startIndex].colon = .colonToken()
45 | return tuple
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/WarningMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxMacros
16 |
17 | /// Implementation of the `myWarning` macro, which mimics the behavior of the
18 | /// built-in `#warning`.
19 | public enum WarningMacro: ExpressionMacro {
20 | public static func expansion(
21 | of node: some FreestandingMacroExpansionSyntax,
22 | in context: some MacroExpansionContext
23 | ) throws -> ExprSyntax {
24 | guard let firstElement = node.arguments.first,
25 | let stringLiteral = firstElement.expression
26 | .as(StringLiteralExprSyntax.self),
27 | stringLiteral.segments.count == 1,
28 | case let .stringSegment(messageString)? = stringLiteral.segments.first
29 | else {
30 | throw CustomError.message("#myWarning macro requires a string literal")
31 | }
32 |
33 | context.diagnose(
34 | Diagnostic(
35 | node: Syntax(node),
36 | message: SimpleDiagnosticMessage(
37 | message: messageString.content.description,
38 | diagnosticID: MessageID(domain: "test123", id: "error"),
39 | severity: .warning
40 | )
41 | )
42 | )
43 |
44 | return "()"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/DefaultFatalErrorImplementationMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class DefaultFatalErrorImplementationMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | macros: ["defaultFatalErrorImplementation": DefaultFatalErrorImplementationMacro.self]
8 | ) {
9 | super.invokeTest()
10 | }
11 | }
12 |
13 | func testExpansionWhenAttachedToProtocolExpandsCorrectly() {
14 | assertMacro {
15 | """
16 | @defaultFatalErrorImplementation
17 | protocol MyProtocol {
18 | func foo()
19 | func bar() -> Int
20 | }
21 | """
22 | } expansion: {
23 | """
24 | protocol MyProtocol {
25 | func foo()
26 | func bar() -> Int
27 | }
28 |
29 | extension MyProtocol {
30 | func foo() {
31 | fatalError("whoops 😅")
32 | }
33 | func bar() -> Int {
34 | fatalError("whoops 😅")
35 | }
36 | }
37 | """
38 | }
39 | }
40 |
41 | func testExpansionWhenNotAttachedToProtocolProducesDiagnostic() {
42 | assertMacro {
43 | """
44 | @defaultFatalErrorImplementation
45 | class MyClass {}
46 | """
47 | } diagnostics: {
48 | """
49 | @defaultFatalErrorImplementation
50 | ┬───────────────────────────────
51 | ╰─ 🛑 Macro `defaultFatalErrorImplementation` can only be applied to a protocol
52 | class MyClass {}
53 | """
54 | }
55 | }
56 |
57 | func testExpansionWhenAttachedToEmptyProtocolDoesNotAddExtension() {
58 | assertMacro {
59 | """
60 | @defaultFatalErrorImplementation
61 | protocol EmptyProtocol {}
62 | """
63 | } expansion: {
64 | """
65 | protocol EmptyProtocol {}
66 | """
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/NewTypeMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class NewTypeMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [NewTypeMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsStringRawType() {
12 | assertMacro {
13 | """
14 | @NewType(String.self)
15 | struct Username {
16 | }
17 | """
18 | } expansion: {
19 | """
20 | struct Username {
21 |
22 | typealias RawValue = String
23 |
24 | var rawValue: RawValue
25 |
26 | init(_ rawValue: RawValue) {
27 | self.rawValue = rawValue
28 | }
29 | }
30 | """
31 | }
32 | }
33 |
34 | func testExpansionWithPublicAddsPublicStringRawType() {
35 | assertMacro {
36 | """
37 | @NewType(String.self)
38 | public struct MyString {
39 | }
40 | """
41 | } expansion: {
42 | """
43 | public struct MyString {
44 |
45 | public typealias RawValue = String
46 |
47 | public var rawValue: RawValue
48 |
49 | public init(_ rawValue: RawValue) {
50 | self.rawValue = rawValue
51 | }
52 | }
53 | """
54 | }
55 | }
56 |
57 | func testExpansionOnClassEmitsError() {
58 | assertMacro {
59 | """
60 | @NewType(Int.self)
61 | class NotAUsername {
62 | }
63 | """
64 | } diagnostics: {
65 | """
66 | @NewType(Int.self)
67 | ┬─────────────────
68 | ╰─ 🛑 @NewType can only be applied to a struct declarations.
69 | class NotAUsername {
70 | }
71 | """
72 | }
73 | }
74 |
75 | func testExpansionWithMissingRawTypeEmitsError() {
76 | assertMacro {
77 | """
78 | @NewType
79 | struct NoRawType {
80 | }
81 | """
82 | } diagnostics: {
83 | """
84 | @NewType
85 | ┬───────
86 | ╰─ 🛑 @NewType requires the raw type as an argument, in the form "RawType.self".
87 | struct NoRawType {
88 | }
89 | """
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/WrapStoredPropertiesMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class WrapStoredPropertiesMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["wrapStoredProperties": WrapStoredPropertiesMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsPublished() {
12 | assertMacro {
13 | """
14 | @wrapStoredProperties("Published")
15 | struct Test {
16 | var value: Int
17 | }
18 | """
19 | } expansion: {
20 | """
21 | struct Test {
22 | @Published
23 | var value: Int
24 | }
25 | """
26 | }
27 | }
28 |
29 | func testExpansionAddsDeprecationAttribute() {
30 | assertMacro {
31 | """
32 | @wrapStoredProperties(#"available(*, deprecated, message: "hands off my data")"#)
33 | struct Test {
34 | var value: Int
35 | }
36 | """
37 | } expansion: {
38 | """
39 | struct Test {
40 | @available(*, deprecated, message: "hands off my data")
41 | var value: Int
42 | }
43 | """
44 | }
45 | }
46 |
47 | func testExpansionIgnoresComputedProperty() {
48 | assertMacro {
49 | """
50 | @wrapStoredProperties("Published")
51 | struct Test {
52 | var value: Int {
53 | get { return 0 }
54 | set {}
55 | }
56 | }
57 | """
58 | } expansion: {
59 | """
60 | struct Test {
61 | var value: Int {
62 | get { return 0 }
63 | set {}
64 | }
65 | }
66 | """
67 | }
68 | }
69 |
70 | func testExpansionWithInvalidAttributeEmitsError() {
71 | assertMacro {
72 | """
73 | @wrapStoredProperties(12)
74 | struct Test {
75 | var value: Int
76 | }
77 | """
78 | } diagnostics: {
79 | """
80 | @wrapStoredProperties(12)
81 | ┬────────────────────────
82 | ╰─ 🛑 macro requires a string literal containing the name of an attribute
83 | struct Test {
84 | var value: Int
85 | }
86 | """
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/DictionaryStorageMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class DictionaryStorageMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | macros: [
8 | DictionaryStorageMacro.self,
9 | DictionaryStoragePropertyMacro.self,
10 | ]
11 | ) {
12 | super.invokeTest()
13 | }
14 | }
15 |
16 | func testExpansionConvertsStoredProperties() {
17 | assertMacro {
18 | """
19 | @DictionaryStorage
20 | struct Point {
21 | var x: Int = 1
22 | var y: Int = 2
23 | }
24 | """
25 | } expansion: {
26 | """
27 | struct Point {
28 | var x: Int {
29 | get {
30 | _storage["x", default: 1] as! Int
31 | }
32 | set {
33 | _storage["x"] = newValue
34 | }
35 | }
36 | var y: Int {
37 | get {
38 | _storage["y", default: 2] as! Int
39 | }
40 | set {
41 | _storage["y"] = newValue
42 | }
43 | }
44 |
45 | var _storage: [String: Any] = [:]
46 | }
47 | """
48 | }
49 | }
50 |
51 | func testExpansionWithoutInitializersEmitsError() {
52 | assertMacro {
53 | """
54 | @DictionaryStorage
55 | class Point {
56 | let x: Int
57 | let y: Int
58 | }
59 | """
60 | } diagnostics: {
61 | """
62 | @DictionaryStorage
63 | class Point {
64 | let x: Int
65 | ╰─ 🛑 stored property must have an initializer
66 | let y: Int
67 | ╰─ 🛑 stored property must have an initializer
68 | }
69 | """
70 | }
71 | }
72 |
73 | func testExpansionIgnoresComputedProperties() {
74 | assertMacro {
75 | """
76 | @DictionaryStorage
77 | struct Test {
78 | var value: Int {
79 | get { return 0 }
80 | set {}
81 | }
82 | }
83 | """
84 | } expansion: {
85 | """
86 | struct Test {
87 | var value: Int {
88 | get { return 0 }
89 | set {}
90 | }
91 |
92 | var _storage: [String: Any] = [:]
93 | }
94 | """
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/DiagnosticsAndFixitsEmitterMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxBuilder
16 | import SwiftSyntaxMacros
17 |
18 | /// Emits two diagnostics, the first of which is a warning and has two fix-its, and
19 | /// the second is a note and has no fix-its.
20 | public enum DiagnosticsAndFixitsEmitterMacro: MemberMacro {
21 | public static func expansion(
22 | of node: AttributeSyntax,
23 | providingMembersOf declaration: some DeclGroupSyntax,
24 | in context: some MacroExpansionContext
25 | ) throws -> [DeclSyntax] {
26 | let firstFixIt = FixIt(
27 | message: SimpleDiagnosticMessage(
28 | message: "This is the first fix-it.",
29 | diagnosticID: MessageID(domain: "domain", id: "fixit1"),
30 | severity: .error),
31 | changes: [
32 | .replace(oldNode: Syntax(node), newNode: Syntax(node)) // no-op
33 | ])
34 | let secondFixIt = FixIt(
35 | message: SimpleDiagnosticMessage(
36 | message: "This is the second fix-it.",
37 | diagnosticID: MessageID(domain: "domain", id: "fixit2"),
38 | severity: .error),
39 | changes: [
40 | .replace(oldNode: Syntax(node), newNode: Syntax(node)) // no-op
41 | ])
42 |
43 | context.diagnose(
44 | Diagnostic(
45 | node: node.attributeName,
46 | message: SimpleDiagnosticMessage(
47 | message: "This is the first diagnostic.",
48 | diagnosticID: MessageID(domain: "domain", id: "diagnostic2"),
49 | severity: .warning),
50 | fixIts: [firstFixIt, secondFixIt]))
51 | context.diagnose(
52 | Diagnostic(
53 | node: node.attributeName,
54 | message: SimpleDiagnosticMessage(
55 | message: "This is the second diagnostic, it's a note.",
56 | diagnosticID: MessageID(domain: "domain", id: "diagnostic2"),
57 | severity: .note)))
58 |
59 | return []
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/CustomCodable.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | public enum CustomCodable: MemberMacro {
17 | public static func expansion(
18 | of node: AttributeSyntax,
19 | providingMembersOf declaration: some DeclGroupSyntax,
20 | in context: some MacroExpansionContext
21 | ) throws -> [DeclSyntax] {
22 | let memberList = declaration.memberBlock.members
23 |
24 | let cases = memberList.compactMap({ member -> String? in
25 | // is a property
26 | guard
27 | let propertyName = member.decl.as(VariableDeclSyntax.self)?.bindings.first?.pattern.as(
28 | IdentifierPatternSyntax.self)?.identifier.text
29 | else {
30 | return nil
31 | }
32 |
33 | // if it has a CodableKey macro on it
34 | if let customKeyMacro = member.decl.as(VariableDeclSyntax.self)?.attributes.first(where: {
35 | element in
36 | element.as(AttributeSyntax.self)?.attributeName.as(IdentifierTypeSyntax.self)?.description
37 | == "CodableKey"
38 | }) {
39 |
40 | // Uses the value in the Macro
41 | let customKeyValue = customKeyMacro.as(AttributeSyntax.self)!.arguments!.as(
42 | LabeledExprListSyntax.self)!.first!.expression
43 |
44 | return "case \(propertyName) = \(customKeyValue)"
45 | } else {
46 | return "case \(propertyName)"
47 | }
48 | })
49 |
50 | let codingKeys: DeclSyntax = """
51 | enum CodingKeys: String, CodingKey {
52 | \(raw: cases.joined(separator: "\n"))
53 | }
54 | """
55 |
56 | return [codingKeys]
57 | }
58 | }
59 |
60 | public struct CodableKey: PeerMacro {
61 | public static func expansion(
62 | of node: AttributeSyntax,
63 | providingPeersOf declaration: some DeclSyntaxProtocol,
64 | in context: some MacroExpansionContext
65 | ) throws -> [DeclSyntax] {
66 | // Does nothing, used only to decorate members with data
67 | return []
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/IndentationWidthTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import SwiftSyntax
3 | import SwiftSyntaxMacros
4 | import XCTest
5 |
6 | private struct AddMemberMacro: MemberMacro {
7 | static func expansion(
8 | of node: AttributeSyntax,
9 | providingMembersOf declaration: some DeclGroupSyntax,
10 | in context: some MacroExpansionContext
11 | ) throws -> [DeclSyntax] {
12 | return ["let v: T"]
13 | }
14 | }
15 |
16 | final class IndentationWidthTests: XCTestCase {
17 | func testExpansionAddsMemberUsingDetectedIndentation() {
18 | assertMacro([AddMemberMacro.self]) {
19 | """
20 | @AddMember
21 | struct S {
22 | let w: T
23 | }
24 | """
25 | } expansion: {
26 | """
27 | struct S {
28 | let w: T
29 |
30 | let v: T
31 | }
32 | """
33 | }
34 | }
35 |
36 | func testExpansionAddsMemberToEmptyStructUsingDefaultIndentation() {
37 | assertMacro([AddMemberMacro.self]) {
38 | """
39 | @AddMember
40 | struct S {
41 | }
42 | """
43 | } expansion: {
44 | """
45 | struct S {
46 |
47 | let v: T
48 | }
49 | """
50 | }
51 | }
52 |
53 | func testExpansionAddsMemberToEmptyStructUsingTwoSpaceIndentation() {
54 | assertMacro(
55 | [AddMemberMacro.self],
56 | indentationWidth: .spaces(2)
57 | ) {
58 | """
59 | @AddMember
60 | struct S {
61 | }
62 | """
63 | } expansion: {
64 | """
65 | struct S {
66 |
67 | let v: T
68 | }
69 | """
70 | }
71 | }
72 |
73 | func testExpansionAddsMemberToEmptyStructUsingTwoSpaceIndentation_withMacroTesting() {
74 | withMacroTesting(
75 | indentationWidth: .spaces(2),
76 | macros: [AddMemberMacro.self]
77 | ) {
78 | assertMacro {
79 | """
80 | @AddMember
81 | struct S {
82 | }
83 | """
84 | } expansion: {
85 | """
86 | struct S {
87 |
88 | let v: T
89 | }
90 | """
91 | }
92 | }
93 | }
94 |
95 | func testExpansionAddsMemberUsingMistchedIndentation() {
96 | assertMacro(
97 | [AddMemberMacro.self],
98 | indentationWidth: .spaces(4)
99 | ) {
100 | """
101 | @AddMember
102 | struct S {
103 | let w: T
104 | }
105 | """
106 | } expansion: {
107 | """
108 | struct S {
109 | let w: T
110 |
111 | let v: T
112 | }
113 | """
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/NewTypeMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxBuilder
15 | import SwiftSyntaxMacros
16 |
17 | public enum NewTypeMacro {}
18 |
19 | extension NewTypeMacro: MemberMacro {
20 | public static func expansion(
21 | of node: AttributeSyntax,
22 | providingMembersOf declaration: Declaration,
23 | in context: Context
24 | ) throws -> [DeclSyntax] where Declaration: DeclGroupSyntax, Context: MacroExpansionContext {
25 | do {
26 | guard
27 | case .argumentList(let arguments) = node.arguments,
28 | arguments.count == 1,
29 | let memberAccessExn = arguments.first?
30 | .expression.as(MemberAccessExprSyntax.self),
31 | let rawType = memberAccessExn.base?.as(DeclReferenceExprSyntax.self)
32 | else {
33 | throw CustomError.message(
34 | #"@NewType requires the raw type as an argument, in the form "RawType.self"."#)
35 | }
36 |
37 | guard let declaration = declaration.as(StructDeclSyntax.self) else {
38 | throw CustomError.message("@NewType can only be applied to a struct declarations.")
39 | }
40 |
41 | let access = declaration.modifiers.first(where: \.isNeededAccessLevelModifier)
42 |
43 | return [
44 | "\(access)typealias RawValue = \(rawType)",
45 | "\(access)var rawValue: RawValue",
46 | "\(access)init(_ rawValue: RawValue) { self.rawValue = rawValue }",
47 | ]
48 | } catch {
49 | print("--------------- throwing \(error)")
50 | throw error
51 | }
52 | }
53 | }
54 |
55 | extension DeclModifierSyntax {
56 | var isNeededAccessLevelModifier: Bool {
57 | switch self.name.tokenKind {
58 | case .keyword(.public): return true
59 | default: return false
60 | }
61 | }
62 | }
63 |
64 | extension SyntaxStringInterpolation {
65 | // It would be nice for SwiftSyntaxBuilder to provide this out-of-the-box.
66 | mutating func appendInterpolation(_ node: Node?) {
67 | if let node {
68 | appendInterpolation(node)
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/FixItTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import SwiftDiagnostics
3 | import SwiftSyntax
4 | import SwiftSyntaxBuilder
5 | import SwiftSyntaxMacros
6 | import XCTest
7 |
8 | private enum ReplaceFirstMemberMacro: MemberMacro {
9 | public static func expansion(
10 | of node: AttributeSyntax,
11 | providingMembersOf declaration: some DeclGroupSyntax,
12 | in context: some MacroExpansionContext
13 | ) throws -> [DeclSyntax] {
14 | guard
15 | let nodeToReplace = declaration.memberBlock.members.first,
16 | let newNode = try? MemberBlockItemSyntax(
17 | decl: VariableDeclSyntax(SyntaxNodeString(stringLiteral: "\n let oye: Oye"))
18 | )
19 | else { return [] }
20 |
21 | context.diagnose(
22 | Diagnostic(
23 | node: node.attributeName,
24 | message: SimpleDiagnosticMessage(
25 | message: "First member needs to be replaced",
26 | diagnosticID: MessageID(domain: "domain", id: "diagnostic2"),
27 | severity: .warning
28 | ),
29 | fixIts: [
30 | FixIt(
31 | message: SimpleDiagnosticMessage(
32 | message: "Replace the first member",
33 | diagnosticID: MessageID(domain: "domain", id: "fixit1"),
34 | severity: .error
35 | ),
36 | changes: [
37 | .replace(oldNode: Syntax(nodeToReplace), newNode: Syntax(newNode))
38 | ]
39 | )
40 | ]
41 | )
42 | )
43 |
44 | return []
45 | }
46 | }
47 |
48 | final class FixItTests: BaseTestCase {
49 | override func invokeTest() {
50 | withMacroTesting(macros: [ReplaceFirstMemberMacro.self]) {
51 | super.invokeTest()
52 | }
53 | }
54 |
55 | func testReplaceFirstMember() {
56 | assertMacro {
57 | """
58 | @ReplaceFirstMember
59 | struct FooBar {
60 | let foo: Foo
61 | let bar: Bar
62 | let baz: Baz
63 | }
64 | """
65 | } diagnostics: {
66 | """
67 | @ReplaceFirstMember
68 | ┬─────────────────
69 | ╰─ ⚠️ First member needs to be replaced
70 | ✏️ Replace the first member
71 | struct FooBar {
72 | let foo: Foo
73 | let bar: Bar
74 | let baz: Baz
75 | }
76 | """
77 | } fixes: {
78 | """
79 | @ReplaceFirstMember
80 | struct FooBar {
81 | let oye: Oye
82 | let bar: Bar
83 | let baz: Baz
84 | }
85 | """
86 | } expansion: {
87 | """
88 | struct FooBar {
89 | let oye: Oye
90 | let bar: Bar
91 | let baz: Baz
92 | }
93 | """
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - '*'
10 | workflow_dispatch:
11 |
12 | concurrency:
13 | group: ci-${{ github.ref }}
14 | cancel-in-progress: true
15 |
16 | jobs:
17 | macos:
18 | name: macOS
19 | runs-on: macos-15
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Select Xcode 16.2
23 | run: sudo xcode-select -s /Applications/Xcode_16.2.app
24 | - name: Run tests
25 | run: swift test
26 |
27 | linux:
28 | name: Linux
29 | strategy:
30 | matrix:
31 | swift:
32 | - '6.0'
33 | runs-on: ubuntu-latest
34 | container: swift:${{ matrix.swift }}
35 | steps:
36 | - uses: actions/checkout@v4
37 | - name: Run tests
38 | run: swift test
39 |
40 | # NB: swift-snapshot-testing needs to be updated for Wasm support
41 | # wasm:
42 | # name: Wasm
43 | # runs-on: ubuntu-latest
44 | # steps:
45 | # - uses: actions/checkout@v4
46 | # - uses: bytecodealliance/actions/wasmtime/setup@v1
47 | # - name: Install Swift and Swift SDK for WebAssembly
48 | # run: |
49 | # PREFIX=/opt/swift
50 | # set -ex
51 | # curl -f -o /tmp/swift.tar.gz "https://download.swift.org/swift-6.0.2-release/ubuntu2204/swift-6.0.2-RELEASE/swift-6.0.2-RELEASE-ubuntu22.04.tar.gz"
52 | # sudo mkdir -p $PREFIX; sudo tar -xzf /tmp/swift.tar.gz -C $PREFIX --strip-component 1
53 | # $PREFIX/usr/bin/swift sdk install https://github.com/swiftwasm/swift/releases/download/swift-wasm-6.0.2-RELEASE/swift-wasm-6.0.2-RELEASE-wasm32-unknown-wasi.artifactbundle.zip --checksum 6ffedb055cb9956395d9f435d03d53ebe9f6a8d45106b979d1b7f53358e1dcb4
54 | # echo "$PREFIX/usr/bin" >> $GITHUB_PATH
55 | #
56 | # - name: Build
57 | # run: swift build --swift-sdk wasm32-unknown-wasi -Xlinker -z -Xlinker stack-size=$((1024 * 1024))
58 |
59 | # NB: 5.9 snapshot outdated, wait for release
60 | # windows:
61 | # name: Windows
62 | # runs-on: windows-latest
63 | # steps:
64 | # - uses: compnerd/gha-setup-swift@main
65 | # with:
66 | # branch: swift-5.9-release
67 | # tag: 5.9-DEVELOPMENT-SNAPSHOT-2023-09-16-a
68 | # - uses: actions/checkout@v4
69 | # - name: Run tests
70 | # run: swift test
71 |
72 | android:
73 | strategy:
74 | matrix:
75 | swift:
76 | - "6.2"
77 | name: Android
78 | runs-on: ubuntu-latest
79 | steps:
80 | - uses: actions/checkout@v4
81 | - uses: skiptools/swift-android-action@v2
82 | with:
83 | swift-version: ${{ matrix.swift }}
84 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/AddCompletionHandlerTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class AddCompletionHandlerTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [AddCompletionHandlerMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionTransformsAsyncFunctionToCompletion() {
12 | assertMacro {
13 | """
14 | @AddCompletionHandler
15 | func f(a: Int, for b: String, _ value: Double) async -> String {
16 | return b
17 | }
18 | """
19 | } expansion: {
20 | """
21 | func f(a: Int, for b: String, _ value: Double) async -> String {
22 | return b
23 | }
24 |
25 | func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) {
26 | Task {
27 | completionHandler(await f(a: a, for: b, value))
28 | }
29 |
30 | }
31 | """
32 | }
33 | }
34 |
35 | func testExpansionOnStoredPropertyEmitsError() {
36 | assertMacro {
37 | """
38 | struct Test {
39 | @AddCompletionHandler
40 | var value: Int
41 | }
42 | """
43 | } diagnostics: {
44 | """
45 | struct Test {
46 | @AddCompletionHandler
47 | ┬────────────────────
48 | ╰─ 🛑 @addCompletionHandler only works on functions
49 | var value: Int
50 | }
51 | """
52 | }
53 | }
54 |
55 | func testExpansionOnNonAsyncFunctionEmitsErrorWithFixItSuggestion() {
56 | assertMacro {
57 | """
58 | struct Test {
59 | @AddCompletionHandler
60 | func fetchData() -> String {
61 | return "Hello, World!"
62 | }
63 | }
64 | """
65 | } diagnostics: {
66 | """
67 | struct Test {
68 | @AddCompletionHandler
69 | func fetchData() -> String {
70 | ┬───
71 | ╰─ 🛑 can only add a completion-handler variant to an 'async' function
72 | ✏️ add 'async'
73 | return "Hello, World!"
74 | }
75 | }
76 | """
77 | } fixes: {
78 | """
79 | struct Test {
80 | @AddCompletionHandler
81 | func fetchData() async-> String {
82 | return "Hello, World!"
83 | }
84 | }
85 | """
86 | } expansion: {
87 | """
88 | struct Test {
89 | func fetchData() async-> String {
90 | return "Hello, World!"
91 | }
92 |
93 | func fetchData(completionHandler: @escaping (String) -> Void) {
94 | Task {
95 | completionHandler(await fetchData())
96 | }
97 |
98 | }
99 | }
100 | """
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | release:
4 | types: [published]
5 | workflow_dispatch:
6 | jobs:
7 | project-channel:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Dump Github context
11 | env:
12 | GITHUB_CONTEXT: ${{ toJSON(github) }}
13 | run: echo "$GITHUB_CONTEXT"
14 | - name: Slack Notification on SUCCESS
15 | if: success()
16 | uses: tokorom/action-slack-incoming-webhook@main
17 | env:
18 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_PROJECT_CHANNEL_WEBHOOK_URL }}
19 | with:
20 | text: swift-macro-testing ${{ github.event.release.tag_name }} has been released.
21 | blocks: |
22 | [
23 | {
24 | "type": "header",
25 | "text": {
26 | "type": "plain_text",
27 | "text": "swift-macro-testing ${{ github.event.release.tag_name}}"
28 | }
29 | },
30 | {
31 | "type": "section",
32 | "text": {
33 | "type": "mrkdwn",
34 | "text": ${{ toJSON(github.event.release.body) }}
35 | }
36 | },
37 | {
38 | "type": "section",
39 | "text": {
40 | "type": "mrkdwn",
41 | "text": "${{ github.event.release.html_url }}"
42 | }
43 | }
44 | ]
45 |
46 | releases-channel:
47 | runs-on: ubuntu-latest
48 | steps:
49 | - name: Dump Github context
50 | env:
51 | GITHUB_CONTEXT: ${{ toJSON(github) }}
52 | run: echo "$GITHUB_CONTEXT"
53 | - name: Slack Notification on SUCCESS
54 | if: success()
55 | uses: tokorom/action-slack-incoming-webhook@main
56 | env:
57 | INCOMING_WEBHOOK_URL: ${{ secrets.SLACK_RELEASES_WEBHOOK_URL }}
58 | with:
59 | text: swift-macro-testing ${{ github.event.release.tag_name }} has been released.
60 | blocks: |
61 | [
62 | {
63 | "type": "header",
64 | "text": {
65 | "type": "plain_text",
66 | "text": "swift-macro-testing ${{ github.event.release.tag_name}}"
67 | }
68 | },
69 | {
70 | "type": "section",
71 | "text": {
72 | "type": "mrkdwn",
73 | "text": ${{ toJSON(github.event.release.body) }}
74 | }
75 | },
76 | {
77 | "type": "section",
78 | "text": {
79 | "type": "mrkdwn",
80 | "text": "${{ github.event.release.html_url }}"
81 | }
82 | }
83 | ]
84 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MetaEnumMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class MetaEnumMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: [MetaEnumMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionAddsNestedMetaEnum() {
12 | assertMacro {
13 | """
14 | @MetaEnum enum Cell {
15 | case integer(Int)
16 | case text(String)
17 | case boolean(Bool)
18 | case null
19 | }
20 | """
21 | } expansion: {
22 | """
23 | enum Cell {
24 | case integer(Int)
25 | case text(String)
26 | case boolean(Bool)
27 | case null
28 |
29 | enum Meta {
30 | case integer
31 | case text
32 | case boolean
33 | case null
34 | init(_ __macro_local_6parentfMu_: Cell) {
35 | switch __macro_local_6parentfMu_ {
36 | case .integer:
37 | self = .integer
38 | case .text:
39 | self = .text
40 | case .boolean:
41 | self = .boolean
42 | case .null:
43 | self = .null
44 | }
45 | }
46 | }
47 | }
48 | """
49 | }
50 | }
51 |
52 | func testExpansionAddsPublicNestedMetaEnum() {
53 | assertMacro {
54 | """
55 | @MetaEnum public enum Cell {
56 | case integer(Int)
57 | case text(String)
58 | case boolean(Bool)
59 | }
60 | """
61 | } expansion: {
62 | """
63 | public enum Cell {
64 | case integer(Int)
65 | case text(String)
66 | case boolean(Bool)
67 |
68 | public enum Meta {
69 | case integer
70 | case text
71 | case boolean
72 | public init(_ __macro_local_6parentfMu_: Cell) {
73 | switch __macro_local_6parentfMu_ {
74 | case .integer:
75 | self = .integer
76 | case .text:
77 | self = .text
78 | case .boolean:
79 | self = .boolean
80 | }
81 | }
82 | }
83 | }
84 | """
85 | }
86 | }
87 |
88 | func testExpansionOnStructEmitsError() {
89 | assertMacro {
90 | """
91 | @MetaEnum struct Cell {
92 | let integer: Int
93 | let text: String
94 | let boolean: Bool
95 | }
96 | """
97 | } diagnostics: {
98 | """
99 | @MetaEnum struct Cell {
100 | ┬────────
101 | ╰─ 🛑 '@MetaEnum' can only be attached to an enum, not a struct
102 | let integer: Int
103 | let text: String
104 | let boolean: Bool
105 | }
106 | """
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Something isn't working as expected
3 | labels: [bug]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Thank you for contributing to swift-macro-testing!
9 |
10 | Before you submit your issue, please complete each text area below with the relevant details for your bug, and complete the steps in the checklist
11 | - type: textarea
12 | attributes:
13 | label: Description
14 | description: |
15 | A short description of the incorrect behavior.
16 |
17 | If you think this issue has been recently introduced and did not occur in an earlier version, please note that. If possible, include the last version that the behavior was correct in addition to your current version.
18 | validations:
19 | required: true
20 | - type: checkboxes
21 | attributes:
22 | label: Checklist
23 | options:
24 | - label: If possible, I've reproduced the issue using the `main` branch of this package.
25 | required: false
26 | - label: This issue hasn't been addressed in an [existing GitHub issue](https://github.com/pointfreeco/swift-macro-testing/issues) or [discussion](https://github.com/pointfreeco/swift-macro-testing/discussions).
27 | required: true
28 | - type: textarea
29 | attributes:
30 | label: Expected behavior
31 | description: Describe what you expected to happen.
32 | validations:
33 | required: false
34 | - type: textarea
35 | attributes:
36 | label: Actual behavior
37 | description: Describe or copy/paste the behavior you observe.
38 | validations:
39 | required: false
40 | - type: textarea
41 | attributes:
42 | label: Steps to reproduce
43 | description: |
44 | Explanation of how to reproduce the incorrect behavior.
45 |
46 | This could include an attached project or link to code that is exhibiting the issue, and/or a screen recording.
47 | placeholder: |
48 | 1. ...
49 | validations:
50 | required: false
51 | - type: input
52 | attributes:
53 | label: swift-macro-testing version information
54 | description: The version of swift-macro-testing used to reproduce this issue.
55 | placeholder: "'0.1.0' for example, or a commit hash"
56 | - type: input
57 | attributes:
58 | label: Destination operating system
59 | description: The OS running swift-macro-testing.
60 | placeholder: "'macOS 14' for example"
61 | - type: input
62 | attributes:
63 | label: Xcode version information
64 | description: The version of Xcode used to reproduce this issue.
65 | placeholder: "The version displayed from 'Xcode 〉About Xcode'"
66 | - type: textarea
67 | attributes:
68 | label: Swift Compiler version information
69 | description: The version of Swift used to reproduce this issue.
70 | placeholder: Output from 'xcrun swiftc --version'
71 | render: shell
72 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/DefaultFatalErrorImplementationMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxBuilder
16 | import SwiftSyntaxMacros
17 |
18 | /// Provides default `fatalError` implementations for protocol methods.
19 | ///
20 | /// This macro generates extensions that add default `fatalError` implementations
21 | /// for each method in the protocol it is attached to.
22 | public enum DefaultFatalErrorImplementationMacro: ExtensionMacro {
23 |
24 | /// Unique identifier for messages related to this macro.
25 | private static let messageID = MessageID(
26 | domain: "MacroExamples", id: "ProtocolDefaultImplementation")
27 |
28 | /// Generates extension for the protocol to which this macro is attached.
29 | public static func expansion(
30 | of node: AttributeSyntax,
31 | attachedTo declaration: some DeclGroupSyntax,
32 | providingExtensionsOf type: some TypeSyntaxProtocol,
33 | conformingTo protocols: [TypeSyntax],
34 | in context: some MacroExpansionContext
35 | ) throws -> [ExtensionDeclSyntax] {
36 |
37 | // Validate that the macro is being applied to a protocol declaration
38 | guard let protocolDecl = declaration.as(ProtocolDeclSyntax.self) else {
39 | throw SimpleDiagnosticMessage(
40 | message: "Macro `defaultFatalErrorImplementation` can only be applied to a protocol",
41 | diagnosticID: messageID,
42 | severity: .error
43 | )
44 | }
45 |
46 | // Extract all the methods from the protocol and assign default implementations
47 | let methods = protocolDecl.memberBlock.members
48 | .map(\.decl)
49 | .compactMap { declaration -> FunctionDeclSyntax? in
50 | guard var function = declaration.as(FunctionDeclSyntax.self) else {
51 | return nil
52 | }
53 | function.body = CodeBlockSyntax {
54 | ExprSyntax(#"fatalError("whoops 😅")"#)
55 | }
56 | return function
57 | }
58 |
59 | // Don't generate an extension if there are no methods
60 | if methods.isEmpty {
61 | return []
62 | }
63 |
64 | // Generate the extension containing the default implementations
65 | let extensionDecl = ExtensionDeclSyntax(extendedType: type) {
66 | for method in methods {
67 | MemberBlockItemSyntax(decl: method)
68 | }
69 | }
70 |
71 | return [extensionDecl]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/DictionaryIndirectionMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | public struct DictionaryStorageMacro {}
17 |
18 | extension DictionaryStorageMacro: MemberMacro {
19 | public static func expansion(
20 | of node: AttributeSyntax,
21 | providingMembersOf declaration: some DeclGroupSyntax,
22 | in context: some MacroExpansionContext
23 | ) throws -> [DeclSyntax] {
24 | return ["\n var _storage: [String: Any] = [:]"]
25 | }
26 | }
27 |
28 | extension DictionaryStorageMacro: MemberAttributeMacro {
29 | public static func expansion(
30 | of node: AttributeSyntax,
31 | attachedTo declaration: some DeclGroupSyntax,
32 | providingAttributesFor member: some DeclSyntaxProtocol,
33 | in context: some MacroExpansionContext
34 | ) throws -> [AttributeSyntax] {
35 | guard let property = member.as(VariableDeclSyntax.self),
36 | property.isStoredProperty
37 | else {
38 | return []
39 | }
40 |
41 | return [
42 | AttributeSyntax(
43 | leadingTrivia: [.newlines(1), .spaces(2)],
44 | attributeName: IdentifierTypeSyntax(
45 | name: .identifier("DictionaryStorageProperty")
46 | )
47 | )
48 | ]
49 | }
50 | }
51 |
52 | public struct DictionaryStoragePropertyMacro: AccessorMacro {
53 | public static func expansion<
54 | Context: MacroExpansionContext,
55 | Declaration: DeclSyntaxProtocol
56 | >(
57 | of node: AttributeSyntax,
58 | providingAccessorsOf declaration: Declaration,
59 | in context: Context
60 | ) throws -> [AccessorDeclSyntax] {
61 | guard let varDecl = declaration.as(VariableDeclSyntax.self),
62 | let binding = varDecl.bindings.first,
63 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
64 | binding.accessorBlock == nil,
65 | let type = binding.typeAnnotation?.type
66 | else {
67 | return []
68 | }
69 |
70 | // Ignore the "_storage" variable.
71 | if identifier.text == "_storage" {
72 | return []
73 | }
74 |
75 | guard let defaultValue = binding.initializer?.value else {
76 | throw CustomError.message("stored property must have an initializer")
77 | }
78 |
79 | return [
80 | """
81 | get {
82 | _storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type)
83 | }
84 | """,
85 | """
86 | set {
87 | _storage[\(literal: identifier.text)] = newValue
88 | }
89 | """,
90 | ]
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/SwiftSyntax/SourceEdit.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 |
15 | /// A textual edit to the original source represented by a range and a
16 | /// replacement.
17 | public struct SourceEdit: Equatable {
18 | /// The half-open range that this edit applies to.
19 | public let range: Range
20 | /// The text to replace the original range with. Empty for a deletion.
21 | public let replacement: String
22 |
23 | /// Length of the original source range that this edit applies to. Zero if
24 | /// this is an addition.
25 | public var length: SourceLength {
26 | return SourceLength(utf8Length: range.lowerBound.utf8Offset - range.upperBound.utf8Offset)
27 | }
28 |
29 | /// Create an edit to replace `range` in the original source with
30 | /// `replacement`.
31 | public init(range: Range, replacement: String) {
32 | self.range = range
33 | self.replacement = replacement
34 | }
35 |
36 | /// Convenience function to create a textual addition after the given node
37 | /// and its trivia.
38 | public static func insert(_ newText: String, after node: some SyntaxProtocol) -> SourceEdit {
39 | return SourceEdit(range: node.endPosition.. SourceEdit {
45 | return SourceEdit(range: node.position.. SourceEdit {
51 | return SourceEdit(range: node.position.. SourceEdit {
57 | return SourceEdit(range: node.position..) -> Void) -> Void {
16 | completionBlock(.success("a: \(a), b: \(b), value: \(value)"))
17 | }
18 | """#
19 | } expansion: {
20 | #"""
21 | func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void {
22 | completionBlock(.success("a: \(a), b: \(b), value: \(value)"))
23 | }
24 |
25 | func c(a: Int, for b: String, _ value: Double) async throws -> String {
26 | try await withCheckedThrowingContinuation { continuation in
27 | c(a: a, for: b, value) { returnValue in
28 |
29 | switch returnValue {
30 | case .success(let value):
31 | continuation.resume(returning: value)
32 | case .failure(let error):
33 | continuation.resume(throwing: error)
34 | }
35 | }
36 | }
37 |
38 | }
39 | """#
40 | }
41 | }
42 |
43 | func testExpansionTransformsFunctionWithBoolCompletionToAsync() {
44 | assertMacro {
45 | """
46 | @AddAsync
47 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void {
48 | completionBlock(true)
49 | }
50 | """
51 | } expansion: {
52 | """
53 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void {
54 | completionBlock(true)
55 | }
56 |
57 | func d(a: Int, for b: String, _ value: Double) async -> Bool {
58 | await withCheckedContinuation { continuation in
59 | d(a: a, for: b, value) { returnValue in
60 |
61 | continuation.resume(returning: returnValue)
62 | }
63 | }
64 |
65 | }
66 | """
67 | }
68 | }
69 |
70 | func testExpansionOnStoredPropertyEmitsError() {
71 | assertMacro {
72 | """
73 | struct Test {
74 | @AddAsync
75 | var name: String
76 | }
77 | """
78 | } diagnostics: {
79 | """
80 | struct Test {
81 | @AddAsync
82 | ┬────────
83 | ╰─ 🛑 @addAsync only works on functions
84 | var name: String
85 | }
86 | """
87 | }
88 | }
89 |
90 | func testExpansionOnAsyncFunctionEmitsError() {
91 | assertMacro {
92 | """
93 | struct Test {
94 | @AddAsync
95 | async func sayHello() {
96 | }
97 | }
98 | """
99 | } diagnostics: {
100 | """
101 | struct Test {
102 | @AddAsync
103 | ┬────────
104 | ╰─ 🛑 @addAsync requires an function that returns void
105 | async func sayHello() {
106 | }
107 | }
108 | """
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/AddBlocker.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftOperators
15 | import SwiftSyntax
16 | import SwiftSyntaxMacros
17 |
18 | /// Implementation of the `addBlocker` macro, which demonstrates how to
19 | /// produce detailed diagnostics from a macro implementation for an utterly
20 | /// silly task: warning about every "add" (binary +) in the argument, with a
21 | /// Fix-It that changes it to a "-".
22 | public struct AddBlocker: ExpressionMacro {
23 | class AddVisitor: SyntaxRewriter {
24 | var diagnostics: [Diagnostic] = []
25 |
26 | override func visit(
27 | _ node: InfixOperatorExprSyntax
28 | ) -> ExprSyntax {
29 | // Identify any infix operator + in the tree.
30 | if var binOp = node.operator.as(BinaryOperatorExprSyntax.self) {
31 | if binOp.operator.text == "+" {
32 | // Form the warning
33 | let messageID = MessageID(domain: "silly", id: "addblock")
34 | diagnostics.append(
35 | Diagnostic(
36 | // Where the warning should go (on the "+").
37 | node: Syntax(node.operator),
38 | // The warning message and severity.
39 | message: SimpleDiagnosticMessage(
40 | message: "blocked an add; did you mean to subtract?",
41 | diagnosticID: messageID,
42 | severity: .warning
43 | ),
44 | // Highlight the left and right sides of the `+`.
45 | highlights: [
46 | Syntax(node.leftOperand),
47 | Syntax(node.rightOperand),
48 | ],
49 | fixIts: [
50 | // Fix-It to replace the '+' with a '-'.
51 | FixIt(
52 | message: SimpleDiagnosticMessage(
53 | message: "use '-'",
54 | diagnosticID: messageID,
55 | severity: .error
56 | ),
57 | changes: [
58 | FixIt.Change.replace(
59 | oldNode: Syntax(binOp.operator),
60 | newNode: Syntax(
61 | TokenSyntax(
62 | .binaryOperator("-"),
63 | leadingTrivia: binOp.operator.leadingTrivia,
64 | trailingTrivia: binOp.operator.trailingTrivia,
65 | presence: .present
66 | )
67 | )
68 | )
69 | ]
70 | )
71 | ]
72 | )
73 | )
74 |
75 | binOp.operator.tokenKind = .binaryOperator("-")
76 |
77 | return ExprSyntax(node.with(\.operator, ExprSyntax(binOp)))
78 | }
79 | }
80 |
81 | return ExprSyntax(node)
82 | }
83 | }
84 |
85 | public static func expansion(
86 | of node: some FreestandingMacroExpansionSyntax,
87 | in context: some MacroExpansionContext
88 | ) throws -> ExprSyntax {
89 | let visitor = AddVisitor()
90 | let result = visitor.rewrite(Syntax(node))
91 |
92 | for diag in visitor.diagnostics {
93 | context.diagnose(diag)
94 | }
95 |
96 | return result.asProtocol(FreestandingMacroExpansionSyntax.self)!.arguments.first!.expression
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/ObservableMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class ObservableMacroTests: XCTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(
7 | macros: [
8 | "Observable": ObservableMacro.self,
9 | "ObservableProperty": ObservablePropertyMacro.self,
10 | ]
11 | ) {
12 | super.invokeTest()
13 | }
14 | }
15 |
16 | func testExpansion() {
17 | assertMacro {
18 | """
19 | @Observable
20 | final class Dog {
21 | var name: String?
22 | var treat: Treat?
23 |
24 | var isHappy: Bool = true
25 |
26 | init() {}
27 |
28 | func bark() {
29 | print("bork bork")
30 | }
31 | }
32 | """
33 | } expansion: {
34 | #"""
35 | final class Dog {
36 | var name: String? {
37 | get {
38 | _registrar.beginAccess(\.name)
39 | defer {
40 | _registrar.endAccess()
41 | }
42 | return _storage.name
43 | }
44 | set {
45 | _registrar.beginAccess(\.name)
46 | _registrar.register(observable: self, willSet: \.name, to: newValue)
47 | defer {
48 | _registrar.register(observable: self, didSet: \.name)
49 | _registrar.endAccess()
50 | }
51 | _storage.name = newValue
52 | }
53 | }
54 | var treat: Treat? {
55 | get {
56 | _registrar.beginAccess(\.treat)
57 | defer {
58 | _registrar.endAccess()
59 | }
60 | return _storage.treat
61 | }
62 | set {
63 | _registrar.beginAccess(\.treat)
64 | _registrar.register(observable: self, willSet: \.treat, to: newValue)
65 | defer {
66 | _registrar.register(observable: self, didSet: \.treat)
67 | _registrar.endAccess()
68 | }
69 | _storage.treat = newValue
70 | }
71 | }
72 |
73 | var isHappy: Bool {
74 | get {
75 | _registrar.beginAccess(\.isHappy)
76 | defer {
77 | _registrar.endAccess()
78 | }
79 | return _storage.isHappy
80 | }
81 | set {
82 | _registrar.beginAccess(\.isHappy)
83 | _registrar.register(observable: self, willSet: \.isHappy, to: newValue)
84 | defer {
85 | _registrar.register(observable: self, didSet: \.isHappy)
86 | _registrar.endAccess()
87 | }
88 | _storage.isHappy = newValue
89 | }
90 | }
91 |
92 | init() {}
93 |
94 | func bark() {
95 | print("bork bork")
96 | }
97 |
98 | let _registrar = ObservationRegistrar()
99 |
100 | public nonisolated func addObserver(_ observer: some Observer) {
101 | _registrar.addObserver(observer)
102 | }
103 |
104 | public nonisolated func removeObserver(_ observer: some Observer) {
105 | _registrar.removeObserver(observer)
106 | }
107 |
108 | private func withTransaction(_ apply: () throws -> T) rethrows -> T {
109 | _registrar.beginAccess()
110 | defer {
111 | _registrar.endAccess()
112 | }
113 | return try apply()
114 | }
115 |
116 | private struct Storage {
117 |
118 | var name: String?
119 | var treat: Treat?
120 |
121 | var isHappy: Bool = true
122 | }
123 |
124 | private var _storage = Storage()
125 | }
126 |
127 | extension Dog: Observable {
128 | }
129 | """#
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Documentation.docc/MacroTesting.md:
--------------------------------------------------------------------------------
1 | # ``MacroTesting``
2 |
3 | Magical testing tools for Swift macros.
4 |
5 | ## Overview
6 |
7 | This library comes with a tool for testing macros that is more powerful and ergonomic than the
8 | default tool that comes with SwiftSyntax. To use the tool, simply specify the macros that you want
9 | to expand as well as a string of Swift source code that makes use of the macro.
10 |
11 | For example, to test the `#stringify` macro that comes with SPM's macro template all one needs to
12 | do is write the following:
13 |
14 | ```swift
15 | import MacroTesting
16 | import Testing
17 |
18 | @Suite(.macros([StringifyMacro.self]))
19 | struct StringifyTests {
20 | @Test func stringify() {
21 | assertMacro {
22 | """
23 | #stringify(a + b)
24 | """
25 | }
26 | }
27 | }
28 | ```
29 |
30 | When you run this test the library will automatically expand the macros in the source code string
31 | and write the expansion into the test file:
32 |
33 | ```swift
34 | @Suite(.macros([StringifyMacro.self]))
35 | struct StringifyTests {
36 | @Test func stringify() {
37 | assertMacro {
38 | """
39 | #stringify(a + b)
40 | """
41 | } expansion: {
42 | """
43 | (a + b, "a + b")
44 | """
45 | }
46 | }
47 | ```
48 |
49 | That is all it takes.
50 |
51 | If in the future the macro's output changes, such as adding labels to the tuple's arguments, then
52 | running the test again will produce a nicely formatted message:
53 |
54 | ```diff
55 | ❌ Actual output (+) differed from expected output (−). Difference: …
56 |
57 | - (a + b, "a + b")
58 | + (result: a + b, code: "a + b")
59 | ```
60 |
61 | You can even have the library automatically re-record the macro expansion directly into your test
62 | file by providing the `record` argument to ``Testing/Trait/macros(_:indentationWidth:record:)``:
63 |
64 | ```swift
65 | @Suite(.macros([StringifyMacro.self], record: .all))
66 | ```
67 |
68 | Now when you run the test again the freshest expanded macro will be written to the `expansion`
69 | trailing closure.
70 |
71 | Macro Testing can also test diagnostics, such as warnings, errors, notes, and fix-its. When a macro
72 | expansion emits a diagnostic, it will render inline in the test. For example, a macro that adds
73 | completion handler functions to async functions may emit an error and fix-it when it is applied to a
74 | non-async function. The resulting macro test will fully capture this information, including where
75 | the diagnostics are emitted, how the fix-its are applied, and how the final macro expands:
76 |
77 | ```swift
78 | func testNonAsyncFunctionDiagnostic() {
79 | assertMacro {
80 | """
81 | @AddCompletionHandler
82 | func f(a: Int, for b: String) -> String {
83 | return b
84 | }
85 | """
86 | } diagnostics: {
87 | """
88 | @AddCompletionHandler
89 | func f(a: Int, for b: String) -> String {
90 | ┬───
91 | ╰─ 🛑 can only add a completion-handler variant to an 'async' function
92 | ✏️ add 'async'
93 | return b
94 | }
95 | """
96 | } fixes: {
97 | """
98 | @AddCompletionHandler
99 | func f(a: Int, for b: String) async -> String {
100 | return b
101 | }
102 | """
103 | } expansion: {
104 | """
105 | func f(a: Int, for b: String) async -> String {
106 | return b
107 | }
108 |
109 | func f(a: Int, for b: String, completionHandler: @escaping (String) -> Void) {
110 | Task {
111 | completionHandler(await f(a: a, for: b, value))
112 | }
113 | }
114 | """
115 | }
116 | }
117 | ```
118 |
119 | ## Topics
120 |
121 | ### Essentials
122 |
123 | - ``assertMacro(_:indentationWidth:record:of:diagnostics:fixes:expansion:fileID:file:function:line:column:)-8zqk4``
124 | - ``withMacroTesting(indentationWidth:record:macros:operation:)-7cm1s``
125 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/WrapStoredPropertiesMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxBuilder
15 | import SwiftSyntaxMacros
16 |
17 | /// Implementation of the `wrapStoredProperties` macro, which can be
18 | /// used to apply an attribute to all of the stored properties of a type.
19 | ///
20 | /// This macro demonstrates member-attribute macros, which allow an attribute
21 | /// written on a type or extension to apply attributes to the members
22 | /// declared within that type or extension.
23 | public struct WrapStoredPropertiesMacro: MemberAttributeMacro {
24 | public static func expansion<
25 | Declaration: DeclGroupSyntax,
26 | Context: MacroExpansionContext
27 | >(
28 | of node: AttributeSyntax,
29 | attachedTo decl: Declaration,
30 | providingAttributesFor member: some DeclSyntaxProtocol,
31 | in context: Context
32 | ) throws -> [AttributeSyntax] {
33 | guard let property = member.as(VariableDeclSyntax.self),
34 | property.isStoredProperty
35 | else {
36 | return []
37 | }
38 |
39 | guard case let .argumentList(arguments) = node.arguments,
40 | let firstElement = arguments.first,
41 | let stringLiteral = firstElement.expression
42 | .as(StringLiteralExprSyntax.self),
43 | stringLiteral.segments.count == 1,
44 | case let .stringSegment(wrapperName)? = stringLiteral.segments.first
45 | else {
46 | throw CustomError.message(
47 | "macro requires a string literal containing the name of an attribute")
48 | }
49 |
50 | return [
51 | AttributeSyntax(
52 | leadingTrivia: [.newlines(1), .spaces(2)],
53 | attributeName: IdentifierTypeSyntax(
54 | name: .identifier(wrapperName.content.text)
55 | )
56 | )
57 | ]
58 | }
59 | }
60 |
61 | extension VariableDeclSyntax {
62 | /// Determine whether this variable has the syntax of a stored property.
63 | ///
64 | /// This syntactic check cannot account for semantic adjustments due to,
65 | /// e.g., accessor macros or property wrappers.
66 | var isStoredProperty: Bool {
67 | if bindings.count != 1 {
68 | return false
69 | }
70 |
71 | let binding = bindings.first!
72 | switch binding.accessorBlock?.accessors {
73 | case .none:
74 | return true
75 |
76 | case .accessors(let accessors):
77 | for accessor in accessors {
78 | switch accessor.accessorSpecifier.tokenKind {
79 | case .keyword(.willSet), .keyword(.didSet):
80 | // Observers can occur on a stored property.
81 | break
82 |
83 | default:
84 | // Other accessors make it a computed property.
85 | return false
86 | }
87 | }
88 |
89 | return true
90 |
91 | case .getter:
92 | return false
93 | }
94 | }
95 | }
96 |
97 | extension DeclGroupSyntax {
98 | /// Enumerate the stored properties that syntactically occur in this
99 | /// declaration.
100 | func storedProperties() -> [VariableDeclSyntax] {
101 | return memberBlock.members.compactMap { member in
102 | guard let variable = member.decl.as(VariableDeclSyntax.self),
103 | variable.isStoredProperty
104 | else {
105 | return nil
106 | }
107 |
108 | return variable
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/_SwiftSyntaxTestSupport/FixItApplier.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxMacroExpansion
16 |
17 | public enum FixItApplier {
18 | /// Applies selected or all Fix-Its from the provided diagnostics to a given syntax tree.
19 | ///
20 | /// - Parameters:
21 | /// - diagnostics: An array of `Diagnostic` objects, each containing one or more Fix-Its.
22 | /// - filterByMessages: An optional array of message strings to filter which Fix-Its to apply.
23 | /// If `nil`, the first Fix-It from each diagnostic is applied.
24 | /// - tree: The syntax tree to which the Fix-Its will be applied.
25 | ///
26 | /// - Returns: A `String` representation of the modified syntax tree after applying the Fix-Its.
27 | // public static func applyFixes(
28 | // from diagnostics: [Diagnostic],
29 | // filterByMessages messages: [String]?,
30 | // to tree: any SyntaxProtocol
31 | // ) -> String {
32 | // let messages = messages ?? diagnostics.compactMap { $0.fixIts.first?.message.message }
33 | //
34 | // let edits =
35 | // diagnostics
36 | // .flatMap(\.fixIts)
37 | // .filter { messages.contains($0.message.message) }
38 | // .flatMap(\.edits)
39 | //
40 | // return self.apply(edits: edits, to: tree)
41 | // }
42 |
43 | /// Apply the given edits to the syntax tree.
44 | ///
45 | /// - Parameters:
46 | /// - edits: The edits to apply to the syntax tree
47 | /// - tree: he syntax tree to which the edits should be applied.
48 | /// - Returns: A `String` representation of the modified syntax tree after applying the edits.
49 | public static func apply(
50 | edits: [SourceEdit],
51 | to tree: any SyntaxProtocol
52 | ) -> String {
53 | var edits = edits
54 | var source = tree.description
55 |
56 | while let edit = edits.first {
57 | edits = Array(edits.dropFirst())
58 |
59 | let startIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.startUtf8Offset)
60 | let endIndex = source.utf8.index(source.utf8.startIndex, offsetBy: edit.endUtf8Offset)
61 |
62 | source.replaceSubrange(startIndex.. SourceEdit? in
65 | if remainingEdit.replacementRange.overlaps(edit.replacementRange) {
66 | // The edit overlaps with the previous edit. We can't apply both
67 | // without conflicts. Apply the one that's listed first and drop the
68 | // later edit.
69 | return nil
70 | }
71 |
72 | // If the remaining edit starts after or at the end of the edit that we just applied,
73 | // shift it by the current edit's difference in length.
74 | if edit.endUtf8Offset <= remainingEdit.startUtf8Offset {
75 | let startPosition = AbsolutePosition(
76 | utf8Offset: remainingEdit.startUtf8Offset - edit.replacementRange.count
77 | + edit.replacementLength)
78 | let endPosition = AbsolutePosition(
79 | utf8Offset: remainingEdit.endUtf8Offset - edit.replacementRange.count
80 | + edit.replacementLength)
81 | return SourceEdit(
82 | range: startPosition.. {
107 | return startUtf8Offset..=6)
2 | import SnapshotTesting
3 | import SwiftSyntax
4 | import SwiftSyntaxMacros
5 | import Testing
6 |
7 | /// A type representing the configuration of snapshot testing.
8 | @_documentation(visibility: private)
9 | public struct _MacrosTestTrait: SuiteTrait, TestTrait {
10 | public let isRecursive = true
11 | let configuration: MacroTestingConfiguration
12 | let record: SnapshotTestingConfiguration.Record?
13 | }
14 |
15 | extension Trait where Self == _MacrosTestTrait {
16 | /// Configure snapshot testing in a suite or test.
17 | ///
18 | /// - Parameters:
19 | /// - indentationWidth: The `Trivia` for setting indentation during macro expansion (e.g., `.spaces(2)`).
20 | /// Defaults to the original source's indentation if unspecified.
21 | /// - record: The recording strategy to use for macro expansions. This can be set to `.all`, `.missing`,
22 | /// `.never`, or `.failed`. If not provided, it uses the current configuration, which can also be set via
23 | /// the `SNAPSHOT_TESTING_RECORD` environment variable.
24 | /// - macros: A dictionary mapping macro names to their implementations. This specifies which macros
25 | /// should be expanded during testing.
26 | public static func macros(
27 | _ macros: [String: Macro.Type]? = nil,
28 | indentationWidth: Trivia? = nil,
29 | record: SnapshotTestingConfiguration.Record? = nil
30 | ) -> Self {
31 | _MacrosTestTrait(
32 | configuration: MacroTestingConfiguration(
33 | indentationWidth: indentationWidth,
34 | macros: macros
35 | ),
36 | record: record
37 | )
38 | }
39 |
40 | /// Configure snapshot testing in a suite or test.
41 | ///
42 | /// - Parameters:
43 | /// - indentationWidth: The `Trivia` for setting indentation during macro expansion (e.g., `.spaces(2)`).
44 | /// Defaults to the original source's indentation if unspecified.
45 | /// - record: The recording strategy to use for macro expansions. This can be set to `.all`, `.missing`,
46 | /// `.never`, or `.failed`. If not provided, it uses the current configuration, which can also be set via
47 | /// the `SNAPSHOT_TESTING_RECORD` environment variable.
48 | /// - macros: An array of macros. This specifies which macros should be expanded during testing.
49 | public static func macros(
50 | _ macros: [Macro.Type]? = nil,
51 | indentationWidth: Trivia? = nil,
52 | record: SnapshotTestingConfiguration.Record? = nil
53 | ) -> Self {
54 | _MacrosTestTrait(
55 | configuration: MacroTestingConfiguration(
56 | indentationWidth: indentationWidth,
57 | macros: macros.map { Dictionary(macros: $0) }
58 | ),
59 | record: record
60 | )
61 | }
62 | }
63 |
64 | #if swift(>=6.1)
65 | @_documentation(visibility: private)
66 | extension _MacrosTestTrait: TestScoping {
67 | public func provideScope(
68 | for test: Test,
69 | testCase: Test.Case?,
70 | performing function: () async throws -> Void
71 | ) async throws {
72 | try await MacroTestingConfiguration.$current.withValue(configuration) {
73 | try await SnapshotTestingConfiguration.$current.withValue(
74 | SnapshotTestingConfiguration(record: record, diffTool: nil)
75 | ) {
76 | try await function()
77 | }
78 | }
79 | }
80 | }
81 | #else
82 | extension Test {
83 | var indentationWidth: Trivia? {
84 | for trait in traits.reversed() {
85 | if let indentationWidth = (trait as? _MacrosTestTrait)?.configuration.indentationWidth {
86 | return indentationWidth
87 | }
88 | }
89 | return nil
90 | }
91 |
92 | var macros: [String: Macro.Type]? {
93 | for trait in traits.reversed() {
94 | if let macros = (trait as? _MacrosTestTrait)?.configuration.macros {
95 | return macros
96 | }
97 | }
98 | return nil
99 | }
100 |
101 | var record: SnapshotTestingConfiguration.Record? {
102 | for trait in traits.reversed() {
103 | if let macros = (trait as? _MacrosTestTrait)?.record {
104 | return macros
105 | }
106 | }
107 | return nil
108 | }
109 | }
110 | #endif
111 | #endif
112 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/MetaEnumMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxBuilder
16 | import SwiftSyntaxMacros
17 |
18 | public struct MetaEnumMacro {
19 | let parentTypeName: TokenSyntax
20 | let childCases: [EnumCaseElementSyntax]
21 | let access: DeclModifierListSyntax.Element?
22 | let parentParamName: TokenSyntax
23 |
24 | init(
25 | node: AttributeSyntax, declaration: some DeclGroupSyntax, context: some MacroExpansionContext
26 | ) throws {
27 | guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
28 | throw DiagnosticsError(diagnostics: [
29 | CaseMacroDiagnostic.notAnEnum(declaration).diagnose(at: Syntax(node))
30 | ])
31 | }
32 |
33 | parentTypeName = enumDecl.name.with(\.trailingTrivia, [])
34 |
35 | access = enumDecl.modifiers.first(where: \.isNeededAccessLevelModifier)
36 |
37 | childCases = enumDecl.caseElements.map { parentCase in
38 | parentCase.with(\.parameterClause, nil)
39 | }
40 |
41 | parentParamName = context.makeUniqueName("parent")
42 | }
43 |
44 | func makeMetaEnum() -> DeclSyntax {
45 | // FIXME: Why does this need to be a string to make trailing trivia work properly?
46 | let caseDecls =
47 | childCases
48 | .map { childCase in
49 | " case \(childCase.name)"
50 | }
51 | .joined(separator: "\n")
52 |
53 | return """
54 | \(access)enum Meta {
55 | \(raw: caseDecls)
56 | \(makeMetaInit())
57 | }
58 | """
59 | }
60 |
61 | func makeMetaInit() -> DeclSyntax {
62 | // FIXME: Why does this need to be a string to make trailing trivia work properly?
63 | let caseStatements =
64 | childCases
65 | .map { childCase in
66 | """
67 | case .\(childCase.name):
68 | self = .\(childCase.name)
69 | """
70 | }
71 | .joined(separator: "\n")
72 |
73 | return """
74 | \(access)init(_ \(parentParamName): \(parentTypeName)) {
75 | switch \(parentParamName) {
76 | \(raw: caseStatements)
77 | }
78 | }
79 | """
80 | }
81 | }
82 |
83 | extension MetaEnumMacro: MemberMacro {
84 | public static func expansion(
85 | of node: AttributeSyntax,
86 | providingMembersOf declaration: some DeclGroupSyntax,
87 | in context: some MacroExpansionContext
88 | ) throws -> [DeclSyntax] {
89 | let macro = try MetaEnumMacro(node: node, declaration: declaration, context: context)
90 |
91 | return [macro.makeMetaEnum()]
92 | }
93 | }
94 |
95 | extension EnumDeclSyntax {
96 | var caseElements: [EnumCaseElementSyntax] {
97 | memberBlock.members.flatMap { member in
98 | guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
99 | return [EnumCaseElementSyntax]()
100 | }
101 |
102 | return Array(caseDecl.elements)
103 | }
104 | }
105 | }
106 |
107 | enum CaseMacroDiagnostic {
108 | case notAnEnum(DeclGroupSyntax)
109 | }
110 |
111 | extension CaseMacroDiagnostic: DiagnosticMessage {
112 | var message: String {
113 | switch self {
114 | case .notAnEnum(let decl):
115 | return
116 | "'@MetaEnum' can only be attached to an enum, not \(decl.descriptiveDeclKind(withArticle: true))"
117 | }
118 | }
119 |
120 | var diagnosticID: MessageID {
121 | switch self {
122 | case .notAnEnum:
123 | return MessageID(domain: "MetaEnumDiagnostic", id: "notAnEnum")
124 | }
125 | }
126 |
127 | var severity: DiagnosticSeverity {
128 | switch self {
129 | case .notAnEnum:
130 | return .error
131 | }
132 | }
133 |
134 | func diagnose(at node: Syntax) -> Diagnostic {
135 | Diagnostic(node: node, message: self)
136 | }
137 | }
138 |
139 | extension DeclGroupSyntax {
140 | func descriptiveDeclKind(withArticle article: Bool = false) -> String {
141 | switch self {
142 | case is ActorDeclSyntax:
143 | return article ? "an actor" : "actor"
144 | case is ClassDeclSyntax:
145 | return article ? "a class" : "class"
146 | case is ExtensionDeclSyntax:
147 | return article ? "an extension" : "extension"
148 | case is ProtocolDeclSyntax:
149 | return article ? "a protocol" : "protocol"
150 | case is StructDeclSyntax:
151 | return article ? "a struct" : "struct"
152 | case is EnumDeclSyntax:
153 | return article ? "an enum" : "enum"
154 | default:
155 | fatalError("Unknown DeclGroupSyntax")
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/OptionSetMacroTests.swift:
--------------------------------------------------------------------------------
1 | import MacroTesting
2 | import XCTest
3 |
4 | final class OptionSetMacroTests: BaseTestCase {
5 | override func invokeTest() {
6 | withMacroTesting(macros: ["MyOptionSet": OptionSetMacro.self]) {
7 | super.invokeTest()
8 | }
9 | }
10 |
11 | func testExpansionOnStructWithNestedEnumAndStatics() {
12 | assertMacro {
13 | """
14 | @MyOptionSet
15 | struct ShippingOptions {
16 | private enum Options: Int {
17 | case nextDay
18 | case secondDay
19 | case priority
20 | case standard
21 | }
22 |
23 | static let express: ShippingOptions = [.nextDay, .secondDay]
24 | static let all: ShippingOptions = [.express, .priority, .standard]
25 | }
26 | """
27 | } expansion: {
28 | """
29 | struct ShippingOptions {
30 | private enum Options: Int {
31 | case nextDay
32 | case secondDay
33 | case priority
34 | case standard
35 | }
36 |
37 | static let express: ShippingOptions = [.nextDay, .secondDay]
38 | static let all: ShippingOptions = [.express, .priority, .standard]
39 |
40 | typealias RawValue = UInt8
41 |
42 | var rawValue: RawValue
43 |
44 | init() {
45 | self.rawValue = 0
46 | }
47 |
48 | init(rawValue: RawValue) {
49 | self.rawValue = rawValue
50 | }
51 |
52 | static let nextDay: Self =
53 | Self(rawValue: 1 << Options.nextDay.rawValue)
54 |
55 | static let secondDay: Self =
56 | Self(rawValue: 1 << Options.secondDay.rawValue)
57 |
58 | static let priority: Self =
59 | Self(rawValue: 1 << Options.priority.rawValue)
60 |
61 | static let standard: Self =
62 | Self(rawValue: 1 << Options.standard.rawValue)
63 | }
64 |
65 | extension ShippingOptions: OptionSet {
66 | }
67 | """
68 | }
69 | }
70 |
71 | func testExpansionOnPublicStructWithExplicitOptionSetConformance() {
72 | assertMacro {
73 | """
74 | @MyOptionSet
75 | public struct ShippingOptions: OptionSet {
76 | private enum Options: Int {
77 | case nextDay
78 | case standard
79 | }
80 | }
81 | """
82 | } expansion: {
83 | """
84 | public struct ShippingOptions: OptionSet {
85 | private enum Options: Int {
86 | case nextDay
87 | case standard
88 | }
89 |
90 | public typealias RawValue = UInt8
91 |
92 | public var rawValue: RawValue
93 |
94 | public init() {
95 | self.rawValue = 0
96 | }
97 |
98 | public init(rawValue: RawValue) {
99 | self.rawValue = rawValue
100 | }
101 |
102 | public static let nextDay: Self =
103 | Self(rawValue: 1 << Options.nextDay.rawValue)
104 |
105 | public static let standard: Self =
106 | Self(rawValue: 1 << Options.standard.rawValue)
107 | }
108 | """
109 | }
110 | }
111 |
112 | func testExpansionFailsOnEnumType() {
113 | assertMacro {
114 | """
115 | @MyOptionSet
116 | enum Animal {
117 | case dog
118 | }
119 | """
120 | } diagnostics: {
121 | """
122 | @MyOptionSet
123 | ├─ 🛑 'OptionSet' macro can only be applied to a struct
124 | ╰─ 🛑 'OptionSet' macro can only be applied to a struct
125 | enum Animal {
126 | case dog
127 | }
128 | """
129 | }
130 | }
131 |
132 | func testExpansionFailsWithoutNestedOptionsEnum() {
133 | assertMacro {
134 | """
135 | @MyOptionSet
136 | struct ShippingOptions {
137 | static let express: ShippingOptions = [.nextDay, .secondDay]
138 | static let all: ShippingOptions = [.express, .priority, .standard]
139 | }
140 | """
141 | } diagnostics: {
142 | """
143 | @MyOptionSet
144 | ├─ 🛑 'OptionSet' macro requires nested options enum 'Options'
145 | ╰─ 🛑 'OptionSet' macro requires nested options enum 'Options'
146 | struct ShippingOptions {
147 | static let express: ShippingOptions = [.nextDay, .secondDay]
148 | static let all: ShippingOptions = [.express, .priority, .standard]
149 | }
150 | """
151 | }
152 | }
153 |
154 | func testExpansionFailsWithoutSpecifiedRawType() {
155 | assertMacro {
156 | """
157 | @MyOptionSet
158 | struct ShippingOptions {
159 | private enum Options: Int {
160 | case nextDay
161 | }
162 | }
163 | """
164 | } diagnostics: {
165 | """
166 | @MyOptionSet
167 | ┬───────────
168 | ├─ 🛑 'OptionSet' macro requires a raw type
169 | ╰─ 🛑 'OptionSet' macro requires a raw type
170 | struct ShippingOptions {
171 | private enum Options: Int {
172 | case nextDay
173 | }
174 | }
175 | """
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | extension DeclSyntaxProtocol {
17 | fileprivate var isObservableStoredProperty: Bool {
18 | if let property = self.as(VariableDeclSyntax.self),
19 | let binding = property.bindings.first,
20 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
21 | identifier.text != "_registrar", identifier.text != "_storage",
22 | binding.accessorBlock == nil
23 | {
24 | return true
25 | }
26 |
27 | return false
28 | }
29 | }
30 |
31 | public struct ObservableMacro: MemberMacro, MemberAttributeMacro {
32 |
33 | // MARK: - MemberMacro
34 |
35 | public static func expansion(
36 | of node: AttributeSyntax,
37 | providingMembersOf declaration: some DeclGroupSyntax,
38 | in context: some MacroExpansionContext
39 | ) throws -> [DeclSyntax] {
40 | guard let identified = declaration.asProtocol(NamedDeclSyntax.self) else {
41 | return []
42 | }
43 |
44 | let parentName = identified.name
45 |
46 | let registrar: DeclSyntax =
47 | """
48 | let _registrar = ObservationRegistrar<\(parentName)>()
49 | """
50 |
51 | let addObserver: DeclSyntax =
52 | """
53 | public nonisolated func addObserver(_ observer: some Observer<\(parentName)>) {
54 | _registrar.addObserver(observer)
55 | }
56 | """
57 |
58 | let removeObserver: DeclSyntax =
59 | """
60 | public nonisolated func removeObserver(_ observer: some Observer<\(parentName)>) {
61 | _registrar.removeObserver(observer)
62 | }
63 | """
64 |
65 | let withTransaction: DeclSyntax =
66 | """
67 | private func withTransaction(_ apply: () throws -> T) rethrows -> T {
68 | _registrar.beginAccess()
69 | defer { _registrar.endAccess() }
70 | return try apply()
71 | }
72 | """
73 |
74 | let memberList = declaration.memberBlock.members.filter {
75 | $0.decl.isObservableStoredProperty
76 | }
77 |
78 | let storageStruct: DeclSyntax =
79 | """
80 | private struct Storage {
81 | \(memberList)
82 | }
83 | """
84 |
85 | let storage: DeclSyntax =
86 | """
87 | private var _storage = Storage()
88 | """
89 |
90 | return [
91 | registrar,
92 | addObserver,
93 | removeObserver,
94 | withTransaction,
95 | storageStruct,
96 | storage,
97 | ]
98 | }
99 |
100 | // MARK: - MemberAttributeMacro
101 |
102 | public static func expansion(
103 | of node: AttributeSyntax,
104 | attachedTo declaration: some DeclGroupSyntax,
105 | providingAttributesFor member: some DeclSyntaxProtocol,
106 | in context: some MacroExpansionContext
107 | ) throws -> [SwiftSyntax.AttributeSyntax] {
108 | guard member.isObservableStoredProperty else {
109 | return []
110 | }
111 |
112 | return [
113 | AttributeSyntax(
114 | attributeName: IdentifierTypeSyntax(
115 | name: .identifier("ObservableProperty")
116 | )
117 | )
118 | ]
119 | }
120 |
121 | }
122 |
123 | extension ObservableMacro: ExtensionMacro {
124 | public static func expansion(
125 | of node: AttributeSyntax,
126 | attachedTo declaration: some DeclGroupSyntax,
127 | providingExtensionsOf type: some TypeSyntaxProtocol,
128 | conformingTo protocols: [TypeSyntax],
129 | in context: some MacroExpansionContext
130 | ) throws -> [ExtensionDeclSyntax] {
131 | [try ExtensionDeclSyntax("extension \(type): Observable {}")]
132 | }
133 | }
134 |
135 | public struct ObservablePropertyMacro: AccessorMacro {
136 | public static func expansion(
137 | of node: AttributeSyntax,
138 | providingAccessorsOf declaration: some DeclSyntaxProtocol,
139 | in context: some MacroExpansionContext
140 | ) throws -> [AccessorDeclSyntax] {
141 | guard let property = declaration.as(VariableDeclSyntax.self),
142 | let binding = property.bindings.first,
143 | let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
144 | binding.accessorBlock == nil
145 | else {
146 | return []
147 | }
148 |
149 | let getAccessor: AccessorDeclSyntax =
150 | """
151 | get {
152 | _registrar.beginAccess(\\.\(identifier))
153 | defer { _registrar.endAccess() }
154 | return _storage.\(identifier)
155 | }
156 | """
157 |
158 | let setAccessor: AccessorDeclSyntax =
159 | """
160 | set {
161 | _registrar.beginAccess(\\.\(identifier))
162 | _registrar.register(observable: self, willSet: \\.\(identifier), to: newValue)
163 | defer {
164 | _registrar.register(observable: self, didSet: \\.\(identifier))
165 | _registrar.endAccess()
166 | }
167 | _storage.\(identifier) = newValue
168 | }
169 | """
170 |
171 | return [getAccessor, setAccessor]
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, caste, color, religion, or sexual
10 | identity and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the overall
26 | community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or advances of
31 | any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email address,
35 | without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when appropriate.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at
63 | .
64 | All complaints will be reviewed and investigated promptly and fairly.
65 |
66 | All community leaders are obligated to respect the privacy and security of the
67 | reporter of any incident.
68 |
69 | ## Enforcement Guidelines
70 |
71 | Community leaders will follow these Community Impact Guidelines in determining
72 | the consequences for any action they deem in violation of this Code of Conduct:
73 |
74 | ### 1. Correction
75 |
76 | **Community Impact**: Use of inappropriate language or other behavior deemed
77 | unprofessional or unwelcome in the community.
78 |
79 | **Consequence**: A private, written warning from community leaders, providing
80 | clarity around the nature of the violation and an explanation of why the
81 | behavior was inappropriate. A public apology may be requested.
82 |
83 | ### 2. Warning
84 |
85 | **Community Impact**: A violation through a single incident or series of
86 | actions.
87 |
88 | **Consequence**: A warning with consequences for continued behavior. No
89 | interaction with the people involved, including unsolicited interaction with
90 | those enforcing the Code of Conduct, for a specified period of time. This
91 | includes avoiding interactions in community spaces as well as external channels
92 | like social media. Violating these terms may lead to a temporary or permanent
93 | ban.
94 |
95 | ### 3. Temporary Ban
96 |
97 | **Community Impact**: A serious violation of community standards, including
98 | sustained inappropriate behavior.
99 |
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 |
106 | ### 4. Permanent Ban
107 |
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 |
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 |
115 | ## Attribution
116 |
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.1, available at
119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120 |
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123 |
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126 | [https://www.contributor-covenant.org/translations][translations].
127 |
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130 | [Mozilla CoC]: https://github.com/mozilla/diversity
131 | [FAQ]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
133 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/AddCompletionHandlerMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxMacros
16 |
17 | public struct AddCompletionHandlerMacro: PeerMacro {
18 | public static func expansion<
19 | Context: MacroExpansionContext,
20 | Declaration: DeclSyntaxProtocol
21 | >(
22 | of node: AttributeSyntax,
23 | providingPeersOf declaration: Declaration,
24 | in context: Context
25 | ) throws -> [DeclSyntax] {
26 | // Only on functions at the moment. We could handle initializers as well
27 | // with a bit of work.
28 | guard var funcDecl = declaration.as(FunctionDeclSyntax.self) else {
29 | throw CustomError.message("@addCompletionHandler only works on functions")
30 | }
31 |
32 | // This only makes sense for async functions.
33 | if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
34 | var newEffects: FunctionEffectSpecifiersSyntax
35 | if let existingEffects = funcDecl.signature.effectSpecifiers {
36 | newEffects = existingEffects
37 | newEffects.asyncSpecifier = .keyword(.async)
38 | } else {
39 | newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async))
40 | }
41 |
42 | var newSignature = funcDecl.signature
43 | newSignature.effectSpecifiers = newEffects
44 | let messageID = MessageID(domain: "MacroExamples", id: "MissingAsync")
45 |
46 | let diag = Diagnostic(
47 | // Where the error should go (on the "+").
48 | node: Syntax(funcDecl.funcKeyword),
49 | // The warning message and severity.
50 | message: SimpleDiagnosticMessage(
51 | message: "can only add a completion-handler variant to an 'async' function",
52 | diagnosticID: messageID,
53 | severity: .error
54 | ),
55 | fixIts: [
56 | // Fix-It to replace the '+' with a '-'.
57 | FixIt(
58 | message: SimpleDiagnosticMessage(
59 | message: "add 'async'",
60 | diagnosticID: messageID,
61 | severity: .error
62 | ),
63 | changes: [
64 | FixIt.Change.replace(
65 | oldNode: Syntax(funcDecl.signature),
66 | newNode: Syntax(newSignature)
67 | )
68 | ]
69 | )
70 | ]
71 | )
72 |
73 | context.diagnose(diag)
74 | return []
75 | }
76 |
77 | // Form the completion handler parameter.
78 | var resultType = funcDecl.signature.returnClause?.type
79 | resultType?.leadingTrivia = []
80 | resultType?.trailingTrivia = []
81 |
82 | let completionHandlerParam =
83 | FunctionParameterSyntax(
84 | firstName: .identifier("completionHandler"),
85 | colon: .colonToken(trailingTrivia: .space),
86 | type: "@escaping (\(resultType ?? "")) -> Void" as TypeSyntax
87 | )
88 |
89 | // Add the completion handler parameter to the parameter list.
90 | let parameterList = funcDecl.signature.parameterClause.parameters
91 | var newParameterList = parameterList
92 | if var lastParam = parameterList.last {
93 | // We need to add a trailing comma to the preceding list.
94 | newParameterList.removeLast()
95 | lastParam.trailingComma = .commaToken(trailingTrivia: .space)
96 | newParameterList += [
97 | lastParam,
98 | completionHandlerParam,
99 | ]
100 | } else {
101 | newParameterList.append(completionHandlerParam)
102 | }
103 |
104 | let callArguments: [String] = parameterList.map { param in
105 | let argName = param.secondName ?? param.firstName
106 |
107 | let paramName = param.firstName
108 | if paramName.text != "_" {
109 | return "\(paramName.text): \(argName.text)"
110 | }
111 |
112 | return "\(argName.text)"
113 | }
114 |
115 | let call: ExprSyntax =
116 | "\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))"
117 |
118 | // FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
119 | // so that the full body could go here.
120 | let newBody: ExprSyntax =
121 | """
122 |
123 | Task {
124 | completionHandler(await \(call))
125 | }
126 |
127 | """
128 |
129 | // Drop the @addCompletionHandler attribute from the new declaration.
130 | let newAttributeList = funcDecl.attributes.filter {
131 | guard case let .attribute(attribute) = $0,
132 | let attributeType = attribute.attributeName.as(IdentifierTypeSyntax.self),
133 | let nodeType = node.attributeName.as(IdentifierTypeSyntax.self)
134 | else {
135 | return true
136 | }
137 |
138 | return attributeType.name.text != nodeType.name.text
139 | }
140 |
141 | // drop async
142 | funcDecl.signature.effectSpecifiers?.asyncSpecifier = nil
143 |
144 | // drop result type
145 | funcDecl.signature.returnClause = nil
146 |
147 | // add completion handler parameter
148 | funcDecl.signature.parameterClause.parameters = newParameterList
149 | funcDecl.signature.parameterClause.trailingTrivia = []
150 |
151 | funcDecl.body = CodeBlockSyntax(
152 | leftBrace: .leftBraceToken(leadingTrivia: .space),
153 | statements: CodeBlockItemListSyntax(
154 | [CodeBlockItemSyntax(item: .expr(newBody))]
155 | ),
156 | rightBrace: .rightBraceToken(leadingTrivia: .newline)
157 | )
158 |
159 | funcDecl.attributes = newAttributeList
160 |
161 | funcDecl.leadingTrivia = .newlines(2)
162 |
163 | return [DeclSyntax(funcDecl)]
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/AddAsyncMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftSyntax
14 | import SwiftSyntaxMacros
15 |
16 | extension SyntaxCollection {
17 | mutating func removeLast() {
18 | self.remove(at: self.index(before: self.endIndex))
19 | }
20 | }
21 |
22 | public struct AddAsyncMacro: PeerMacro {
23 | public static func expansion<
24 | Context: MacroExpansionContext,
25 | Declaration: DeclSyntaxProtocol
26 | >(
27 | of node: AttributeSyntax,
28 | providingPeersOf declaration: Declaration,
29 | in context: Context
30 | ) throws -> [DeclSyntax] {
31 |
32 | // Only on functions at the moment.
33 | guard var funcDecl = declaration.as(FunctionDeclSyntax.self) else {
34 | throw CustomError.message("@addAsync only works on functions")
35 | }
36 |
37 | // This only makes sense for non async functions.
38 | if funcDecl.signature.effectSpecifiers?.asyncSpecifier != nil {
39 | throw CustomError.message(
40 | "@addAsync requires an non async function"
41 | )
42 | }
43 |
44 | // This only makes sense void functions
45 | if funcDecl.signature.returnClause?.type.as(IdentifierTypeSyntax.self)?.name.text != "Void" {
46 | throw CustomError.message(
47 | "@addAsync requires an function that returns void"
48 | )
49 | }
50 |
51 | // Requires a completion handler block as last parameter
52 | guard
53 | let completionHandlerParameterAttribute = funcDecl.signature.parameterClause.parameters.last?
54 | .type.as(AttributedTypeSyntax.self),
55 | let completionHandlerParameter = completionHandlerParameterAttribute.baseType.as(
56 | FunctionTypeSyntax.self
57 | )
58 | else {
59 | throw CustomError.message(
60 | "@addAsync requires an function that has a completion handler as last parameter"
61 | )
62 | }
63 |
64 | // Completion handler needs to return Void
65 | if completionHandlerParameter.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text
66 | != "Void"
67 | {
68 | throw CustomError.message(
69 | "@addAsync requires an function that has a completion handler that returns Void"
70 | )
71 | }
72 |
73 | let returnType = completionHandlerParameter.parameters.first?.type
74 | let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result"
75 |
76 | let successReturnType: TypeSyntax? = {
77 | guard isResultReturn
78 | else { return returnType }
79 |
80 | let successType = returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments
81 | .first!
82 | .argument
83 | #if canImport(SwiftSyntax601)
84 | switch successType {
85 | case .none:
86 | return nil
87 | case .type(let syntax):
88 | return syntax
89 | default:
90 | return nil
91 | }
92 | #else
93 | return successType
94 | #endif
95 | }()
96 |
97 | // Remove completionHandler and comma from the previous parameter
98 | var newParameterList = funcDecl.signature.parameterClause.parameters
99 | newParameterList.removeLast()
100 | var newParameterListLastParameter = newParameterList.last!
101 | newParameterList.removeLast()
102 | newParameterListLastParameter.trailingTrivia = []
103 | newParameterListLastParameter.trailingComma = nil
104 | newParameterList.append(newParameterListLastParameter)
105 |
106 | // Drop the @addAsync attribute from the new declaration.
107 | let newAttributeList = funcDecl.attributes.filter {
108 | guard case let .attribute(attribute) = $0,
109 | let attributeType = attribute.attributeName.as(IdentifierTypeSyntax.self),
110 | let nodeType = node.attributeName.as(IdentifierTypeSyntax.self)
111 | else {
112 | return true
113 | }
114 |
115 | return attributeType.name.text != nodeType.name.text
116 | }
117 |
118 | let callArguments: [String] = newParameterList.map { param in
119 | let argName = param.secondName ?? param.firstName
120 |
121 | let paramName = param.firstName
122 | if paramName.text != "_" {
123 | return "\(paramName.text): \(argName.text)"
124 | }
125 |
126 | return "\(argName.text)"
127 | }
128 |
129 | let switchBody: ExprSyntax =
130 | """
131 | switch returnValue {
132 | case .success(let value):
133 | continuation.resume(returning: value)
134 | case .failure(let error):
135 | continuation.resume(throwing: error)
136 | }
137 | """
138 |
139 | let newBody: ExprSyntax =
140 | """
141 |
142 | \(raw: isResultReturn ? "try await withCheckedThrowingContinuation { continuation in" : "await withCheckedContinuation { continuation in")
143 | \(raw: funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "")
144 |
145 | \(raw: isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))")
146 | }
147 | }
148 |
149 | """
150 |
151 | // add async
152 | funcDecl.signature.effectSpecifiers = FunctionEffectSpecifiersSyntax(
153 | leadingTrivia: .space,
154 | asyncSpecifier: .keyword(.async),
155 | throwsClause: isResultReturn ? ThrowsClauseSyntax(throwsSpecifier: .keyword(.throws)) : nil
156 | )
157 |
158 | // add result type
159 | if let successReturnType {
160 | funcDecl.signature.returnClause = ReturnClauseSyntax(
161 | leadingTrivia: .space,
162 | type: successReturnType.with(\.leadingTrivia, .space)
163 | )
164 | } else {
165 | funcDecl.signature.returnClause = nil
166 | }
167 |
168 | // drop completion handler
169 | funcDecl.signature.parameterClause.parameters = newParameterList
170 | funcDecl.signature.parameterClause.trailingTrivia = []
171 |
172 | funcDecl.body = CodeBlockSyntax(
173 | leftBrace: .leftBraceToken(leadingTrivia: .space),
174 | statements: CodeBlockItemListSyntax(
175 | [CodeBlockItemSyntax(item: .expr(newBody))]
176 | ),
177 | rightBrace: .rightBraceToken(leadingTrivia: .newline)
178 | )
179 |
180 | funcDecl.attributes = newAttributeList
181 |
182 | funcDecl.leadingTrivia = .newlines(2)
183 |
184 | return [DeclSyntax(funcDecl)]
185 | }
186 | }
187 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Macro Testing
2 |
3 | [](https://github.com/pointfreeco/swift-macro-testing/actions?query=workflow%3ACI)
4 | [](https://www.pointfree.co/slack-invite)
5 | [](https://swiftpackageindex.com/pointfreeco/swift-macro-testing)
6 | [](https://swiftpackageindex.com/pointfreeco/swift-macro-testing)
7 |
8 | Magical testing tools for Swift macros.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | ## Learn more
17 |
18 | This library was designed to support libraries and episodes produced for [Point-Free][point-free], a
19 | video series exploring the Swift programming language hosted by [Brandon Williams][mbrandonw] and
20 | [Stephen Celis][stephencelis].
21 |
22 | You can watch all of the episodes [here][macro-testing-episodes].
23 |
24 |
25 |
26 |
27 |
28 | ## Motivation
29 |
30 | This library comes with a tool for testing macros that is more powerful and ergonomic than the
31 | default tool that comes with SwiftSyntax. To use the tool, simply specify the macros that you want
32 | to expand as well as a string of Swift source code that makes use of the macro.
33 |
34 | For example, to test the `#stringify` macro that comes with SPM's macro template all one needs to
35 | do is write the following:
36 |
37 | ```swift
38 | import MacroTesting
39 | import Testing
40 |
41 | @Suite(.macros([StringifyMacro.self]))
42 | struct StringifyTests {
43 | @Test func stringify() {
44 | assertMacro {
45 | """
46 | #stringify(a + b)
47 | """
48 | }
49 | }
50 | }
51 | ```
52 |
53 | When you run this test the library will automatically expand the macros in the source code string
54 | and write the expansion into the test file:
55 |
56 | ```swift
57 | @Suite(.macros([StringifyMacro.self]))
58 | struct StringifyTests {
59 | @Test func stringify() {
60 | assertMacro {
61 | """
62 | #stringify(a + b)
63 | """
64 | } expansion: {
65 | """
66 | (a + b, "a + b")
67 | """
68 | }
69 | }
70 | ```
71 |
72 | That is all it takes.
73 |
74 | If in the future the macro's output changes, such as adding labels to the tuple's arguments, then
75 | running the test again will produce a nicely formatted message:
76 |
77 | ```diff
78 | ❌ Actual output (+) differed from expected output (−). Difference: …
79 |
80 | - (a + b, "a + b")
81 | + (result: a + b, code: "a + b")
82 | ```
83 |
84 | You can even have the library automatically re-record the macro expansion directly into your test
85 | file by providing the `record` argument to ``Testing/Trait/macros(_:indentationWidth:record:)``:
86 |
87 | ```swift
88 | @Suite(.macros([StringifyMacro.self], record: .all))
89 | ```
90 |
91 | Now when you run the test again the freshest expanded macro will be written to the `expansion`
92 | trailing closure.
93 |
94 | Macro Testing can also test diagnostics, such as warnings, errors, notes, and fix-its. When a macro
95 | expansion emits a diagnostic, it will render inline in the test. For example, a macro that adds
96 | completion handler functions to async functions may emit an error and fix-it when it is applied to a
97 | non-async function. The resulting macro test will fully capture this information, including where
98 | the diagnostics are emitted, how the fix-its are applied, and how the final macro expands:
99 |
100 | ```swift
101 | func testNonAsyncFunctionDiagnostic() {
102 | assertMacro {
103 | """
104 | @AddCompletionHandler
105 | func f(a: Int, for b: String) -> String {
106 | return b
107 | }
108 | """
109 | } diagnostics: {
110 | """
111 | @AddCompletionHandler
112 | func f(a: Int, for b: String) -> String {
113 | ┬───
114 | ╰─ 🛑 can only add a completion-handler variant to an 'async' function
115 | ✏️ add 'async'
116 | return b
117 | }
118 | """
119 | } fixes: {
120 | """
121 | @AddCompletionHandler
122 | func f(a: Int, for b: String) async -> String {
123 | return b
124 | }
125 | """
126 | } expansion: {
127 | """
128 | func f(a: Int, for b: String) async -> String {
129 | return b
130 | }
131 |
132 | func f(a: Int, for b: String, completionHandler: @escaping (String) -> Void) {
133 | Task {
134 | completionHandler(await f(a: a, for: b, value))
135 | }
136 | }
137 | """
138 | }
139 | }
140 | ```
141 |
142 | ## Integration with Swift Testing
143 |
144 | If you are using Swift's built-in Testing framework, this library also supports it. Instead of relying solely
145 | on XCTest, you can configure your tests using the `Trait` system provided by `swift-testing`. For example:
146 |
147 | ```swift
148 | import Testing
149 | import MacroTesting
150 |
151 | @Suite(
152 | .macros(
153 | ["stringify": StringifyMacro.self],
154 | record: .missing // Record only missing snapshots
155 | )
156 | )
157 | struct StringifyMacroSwiftTestingTests {
158 | @Test
159 | func testStringify() {
160 | assertMacro {
161 | """
162 | #stringify(a + b)
163 | """
164 | } expansion: {
165 | """
166 | (a + b, "a + b")
167 | """
168 | }
169 | }
170 | }
171 | ```
172 |
173 | Additionally, the `record` parameter in `macros` can be used to control the recording behavior for all
174 | tests in the suite. This value can also be configured using the `SNAPSHOT_TESTING_RECORD` environment
175 | variable to dynamically adjust recording behavior based on your CI or local environment.
176 |
177 | ## Documentation
178 |
179 | The latest documentation for this library is available [here][macro-testing-docs].
180 |
181 | ## License
182 |
183 | This library is released under the MIT license. See [LICENSE](LICENSE) for details.
184 |
185 | [macro-testing-docs]: https://swiftpackageindex.com/pointfreeco/swift-macro-testing/main/documentation/macrotesting
186 | [macro-testing-episodes]: https://www.pointfree.co/collections/macros
187 | [mbrandonw]: https://github.com/mbrandonw
188 | [point-free]: https://www.pointfree.co
189 | [stephencelis]: https://github.com/stephencelis
190 |
--------------------------------------------------------------------------------
/Tests/MacroTestingTests/MacroExamples/OptionSetMacro.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxBuilder
16 | import SwiftSyntaxMacros
17 |
18 | enum OptionSetMacroDiagnostic {
19 | case requiresStruct
20 | case requiresStringLiteral(String)
21 | case requiresOptionsEnum(String)
22 | case requiresOptionsEnumRawType
23 | }
24 |
25 | extension OptionSetMacroDiagnostic: DiagnosticMessage {
26 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic {
27 | Diagnostic(node: Syntax(node), message: self)
28 | }
29 |
30 | var message: String {
31 | switch self {
32 | case .requiresStruct:
33 | return "'OptionSet' macro can only be applied to a struct"
34 |
35 | case .requiresStringLiteral(let name):
36 | return "'OptionSet' macro argument \(name) must be a string literal"
37 |
38 | case .requiresOptionsEnum(let name):
39 | return "'OptionSet' macro requires nested options enum '\(name)'"
40 |
41 | case .requiresOptionsEnumRawType:
42 | return "'OptionSet' macro requires a raw type"
43 | }
44 | }
45 |
46 | var severity: DiagnosticSeverity { .error }
47 |
48 | var diagnosticID: MessageID {
49 | MessageID(domain: "Swift", id: "OptionSet.\(self)")
50 | }
51 | }
52 |
53 | /// The label used for the OptionSet macro argument that provides the name of
54 | /// the nested options enum.
55 | private let optionsEnumNameArgumentLabel = "optionsName"
56 |
57 | /// The default name used for the nested "Options" enum. This should
58 | /// eventually be overridable.
59 | private let defaultOptionsEnumName = "Options"
60 |
61 | extension LabeledExprListSyntax {
62 | /// Retrieve the first element with the given label.
63 | func first(labeled name: String) -> Element? {
64 | return first { element in
65 | if let label = element.label, label.text == name {
66 | return true
67 | }
68 |
69 | return false
70 | }
71 | }
72 | }
73 |
74 | public struct OptionSetMacro {
75 | /// Decodes the arguments to the macro expansion.
76 | ///
77 | /// - Returns: the important arguments used by the various roles of this
78 | /// macro inhabits, or nil if an error occurred.
79 | static func decodeExpansion(
80 | of attribute: AttributeSyntax,
81 | attachedTo decl: some DeclGroupSyntax,
82 | in context: some MacroExpansionContext
83 | ) -> (StructDeclSyntax, EnumDeclSyntax, TypeSyntax)? {
84 | // Determine the name of the options enum.
85 | let optionsEnumName: String
86 | if case let .argumentList(arguments) = attribute.arguments,
87 | let optionEnumNameArg = arguments.first(labeled: optionsEnumNameArgumentLabel)
88 | {
89 | // We have a options name; make sure it is a string literal.
90 | guard let stringLiteral = optionEnumNameArg.expression.as(StringLiteralExprSyntax.self),
91 | stringLiteral.segments.count == 1,
92 | case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first
93 | else {
94 | context.diagnose(
95 | OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(
96 | at: optionEnumNameArg.expression
97 | )
98 | )
99 | return nil
100 | }
101 |
102 | optionsEnumName = optionsEnumNameString.content.text
103 | } else {
104 | optionsEnumName = defaultOptionsEnumName
105 | }
106 |
107 | // Only apply to structs.
108 | guard let structDecl = decl.as(StructDeclSyntax.self) else {
109 | context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl))
110 | return nil
111 | }
112 |
113 | // Find the option enum within the struct.
114 | guard
115 | let optionsEnum = decl.memberBlock.members.compactMap({ member in
116 | if let enumDecl = member.decl.as(EnumDeclSyntax.self),
117 | enumDecl.name.text == optionsEnumName
118 | {
119 | return enumDecl
120 | }
121 |
122 | return nil
123 | }).first
124 | else {
125 | context.diagnose(
126 | OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)
127 | )
128 | return nil
129 | }
130 |
131 | // Retrieve the raw type from the attribute.
132 | guard
133 | let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)?
134 | .genericArgumentClause,
135 | let rawType = genericArgs.arguments.first?.argument
136 | else {
137 | context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute))
138 | return nil
139 | }
140 |
141 | let rawTypeSyntax: TypeSyntax?
142 | #if canImport(SwiftSyntax601)
143 | switch rawType {
144 | case .type(let typeSyntax):
145 | rawTypeSyntax = typeSyntax
146 | default:
147 | rawTypeSyntax = nil
148 | }
149 | #else
150 | rawTypeSyntax = rawType
151 | #endif
152 |
153 | guard let rawTypeSyntax
154 | else {
155 | context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute))
156 | return nil
157 | }
158 |
159 | return (structDecl, optionsEnum, rawTypeSyntax)
160 | }
161 | }
162 |
163 | extension OptionSetMacro: ExtensionMacro {
164 | public static func expansion(
165 | of node: AttributeSyntax,
166 | attachedTo declaration: some DeclGroupSyntax,
167 | providingExtensionsOf type: some TypeSyntaxProtocol,
168 | conformingTo protocols: [TypeSyntax],
169 | in context: some MacroExpansionContext
170 | ) throws -> [ExtensionDeclSyntax] {
171 | // Decode the expansion arguments.
172 | guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context)
173 | else {
174 | return []
175 | }
176 |
177 | // If there is an explicit conformance to OptionSet already, don't add one.
178 | if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypes,
179 | inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet"
180 | })
181 | {
182 | return []
183 | }
184 |
185 | return [try ExtensionDeclSyntax("extension \(type): OptionSet {}")]
186 | }
187 | }
188 |
189 | extension OptionSetMacro: MemberMacro {
190 | public static func expansion(
191 | of attribute: AttributeSyntax,
192 | providingMembersOf decl: some DeclGroupSyntax,
193 | in context: some MacroExpansionContext
194 | ) throws -> [DeclSyntax] {
195 | // Decode the expansion arguments.
196 | guard
197 | let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context)
198 | else {
199 | return []
200 | }
201 |
202 | // Find all of the case elements.
203 | let caseElements = optionsEnum.memberBlock.members.flatMap { member in
204 | guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
205 | return [EnumCaseElementSyntax]()
206 | }
207 |
208 | return Array(caseDecl.elements)
209 | }
210 |
211 | // Dig out the access control keyword we need.
212 | let access = decl.modifiers.first(where: \.isNeededAccessLevelModifier)
213 |
214 | let staticVars = caseElements.map { (element) -> DeclSyntax in
215 | """
216 | \(access) static let \(element.name): Self =
217 | Self(rawValue: 1 << \(optionsEnum.name).\(element.name).rawValue)
218 | """
219 | }
220 |
221 | return [
222 | "\(access)typealias RawValue = \(rawType)",
223 | "\(access)var rawValue: RawValue",
224 | "\(access)init() { self.rawValue = 0 }",
225 | "\(access)init(rawValue: RawValue) { self.rawValue = rawValue }",
226 | ] + staticVars
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/Internal/Deprecations.swift:
--------------------------------------------------------------------------------
1 | import InlineSnapshotTesting
2 | @_spi(Internals) import SnapshotTesting
3 | import SwiftDiagnostics
4 | import SwiftOperators
5 | import SwiftParser
6 | import SwiftParserDiagnostics
7 | import SwiftSyntax
8 | import SwiftSyntaxMacroExpansion
9 | import SwiftSyntaxMacros
10 | import XCTest
11 |
12 | #if canImport(Testing)
13 | import Testing
14 | #endif
15 |
16 | // MARK: Deprecated after 0.6.0
17 |
18 | #if canImport(Testing) && compiler(>=6)
19 | extension Trait where Self == _MacrosTestTrait {
20 |
21 | @available(*, deprecated, message: "Use `macros(_:indentationWidth:record:)` instead")
22 | public static func macros(
23 | indentationWidth: Trivia? = nil,
24 | record: SnapshotTestingConfiguration.Record? = nil,
25 | macros: [String: Macro.Type]? = nil
26 | ) -> Self {
27 | Self.macros(macros, indentationWidth: indentationWidth, record: record)
28 | }
29 |
30 | @available(*, deprecated, message: "Use `macros(_:indentationWidth:record:)` instead")
31 | public static func macros(
32 | indentationWidth: Trivia? = nil,
33 | record: SnapshotTestingConfiguration.Record? = nil,
34 | macros: [Macro.Type]? = nil
35 | ) -> Self {
36 | Self.macros(macros, indentationWidth: indentationWidth, record: record)
37 | }
38 | }
39 | #endif
40 |
41 | // MARK: Deprecated after 0.4.2
42 |
43 | @available(iOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
44 | @available(
45 | macOS,
46 | deprecated,
47 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
48 | )
49 | @available(tvOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
50 | @available(
51 | visionOS,
52 | deprecated,
53 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
54 | )
55 | @available(
56 | watchOS,
57 | deprecated,
58 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
59 | )
60 | @_disfavoredOverload
61 | public func withMacroTesting(
62 | indentationWidth: Trivia? = nil,
63 | isRecording: Bool? = nil,
64 | macros: [String: Macro.Type]? = nil,
65 | operation: () async throws -> R
66 | ) async rethrows {
67 | var configuration = MacroTestingConfiguration.current
68 | if let indentationWidth { configuration.indentationWidth = indentationWidth }
69 | let record: SnapshotTestingConfiguration.Record? = isRecording.map { $0 ? .all : .missing }
70 | if let macros { configuration.macros = macros }
71 | _ = try await withSnapshotTesting(record: record) {
72 | try await MacroTestingConfiguration.$current.withValue(configuration) {
73 | try await operation()
74 | }
75 | }
76 | }
77 |
78 | @available(iOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
79 | @available(
80 | macOS,
81 | deprecated,
82 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
83 | )
84 | @available(tvOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
85 | @available(
86 | visionOS,
87 | deprecated,
88 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
89 | )
90 | @available(
91 | watchOS,
92 | deprecated,
93 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
94 | )
95 | @_disfavoredOverload
96 | public func withMacroTesting(
97 | indentationWidth: Trivia? = nil,
98 | isRecording: Bool? = nil,
99 | macros: [String: Macro.Type]? = nil,
100 | operation: () throws -> R
101 | ) rethrows {
102 | var configuration = MacroTestingConfiguration.current
103 | if let indentationWidth { configuration.indentationWidth = indentationWidth }
104 | let record: SnapshotTestingConfiguration.Record? = isRecording.map { $0 ? .all : .missing }
105 | if let macros { configuration.macros = macros }
106 | _ = try withSnapshotTesting(record: record) {
107 | try MacroTestingConfiguration.$current.withValue(configuration) {
108 | try operation()
109 | }
110 | }
111 | }
112 |
113 | @available(iOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
114 | @available(
115 | macOS,
116 | deprecated,
117 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
118 | )
119 | @available(tvOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
120 | @available(
121 | visionOS,
122 | deprecated,
123 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
124 | )
125 | @available(
126 | watchOS,
127 | deprecated,
128 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
129 | )
130 | @_disfavoredOverload
131 | public func withMacroTesting(
132 | indentationWidth: Trivia? = nil,
133 | isRecording: Bool? = nil,
134 | macros: [Macro.Type],
135 | operation: () async throws -> R
136 | ) async rethrows {
137 | try await withMacroTesting(
138 | indentationWidth: indentationWidth,
139 | isRecording: isRecording,
140 | macros: Dictionary(macros: macros),
141 | operation: operation
142 | )
143 | }
144 |
145 | @available(iOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
146 | @available(
147 | macOS,
148 | deprecated,
149 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
150 | )
151 | @available(tvOS, deprecated, renamed: "withMacroTesting(indentationWidth:record:macros:operation:)")
152 | @available(
153 | visionOS,
154 | deprecated,
155 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
156 | )
157 | @available(
158 | watchOS,
159 | deprecated,
160 | renamed: "withMacroTesting(indentationWidth:record:macros:operation:)"
161 | )
162 | @_disfavoredOverload
163 | public func withMacroTesting(
164 | indentationWidth: Trivia? = nil,
165 | isRecording: Bool? = nil,
166 | macros: [Macro.Type],
167 | operation: () throws -> R
168 | ) rethrows {
169 | try withMacroTesting(
170 | indentationWidth: indentationWidth,
171 | isRecording: isRecording,
172 | macros: Dictionary(macros: macros),
173 | operation: operation
174 | )
175 | }
176 |
177 | // MARK: Deprecated after 0.1.0
178 |
179 | @available(*, deprecated, message: "Re-record this assertion")
180 | public func assertMacro(
181 | _ macros: [String: Macro.Type]? = nil,
182 | record isRecording: Bool? = nil,
183 | of originalSource: () throws -> String,
184 | matches expandedOrDiagnosedSource: () -> String,
185 | fileID: StaticString = #fileID,
186 | file filePath: StaticString = #filePath,
187 | function: StaticString = #function,
188 | line: UInt = #line,
189 | column: UInt = #column
190 | ) {
191 | guard isRecording ?? (SnapshotTestingConfiguration.current?.record == .all) else {
192 | recordIssue(
193 | "Re-record this assertion",
194 | fileID: fileID,
195 | filePath: filePath,
196 | line: line,
197 | column: column
198 | )
199 | return
200 | }
201 | assertMacro(
202 | macros,
203 | record: true,
204 | of: originalSource,
205 | file: filePath,
206 | function: function,
207 | line: line,
208 | column: column
209 | )
210 | }
211 |
212 | @available(*, deprecated, message: "Re-record this assertion")
213 | public func assertMacro(
214 | _ macros: [Macro.Type],
215 | record isRecording: Bool? = nil,
216 | of originalSource: () throws -> String,
217 | matches expandedOrDiagnosedSource: () -> String,
218 | fileID: StaticString = #fileID,
219 | file filePath: StaticString = #filePath,
220 | function: StaticString = #function,
221 | line: UInt = #line,
222 | column: UInt = #column
223 | ) {
224 | assertMacro(
225 | Dictionary(macros: macros),
226 | record: isRecording,
227 | of: originalSource,
228 | matches: expandedOrDiagnosedSource,
229 | fileID: fileID,
230 | file: filePath,
231 | function: function,
232 | line: line,
233 | column: column
234 | )
235 | }
236 |
237 | @available(
238 | *,
239 | deprecated,
240 | message: "Delete 'applyFixIts' and 'matches' and re-record this assertion"
241 | )
242 | public func assertMacro(
243 | _ macros: [String: Macro.Type]? = nil,
244 | applyFixIts: Bool,
245 | record isRecording: Bool? = nil,
246 | of originalSource: () throws -> String,
247 | matches expandedOrDiagnosedSource: () -> String,
248 | fileID: StaticString = #fileID,
249 | file filePath: StaticString = #filePath,
250 | function: StaticString = #function,
251 | line: UInt = #line,
252 | column: UInt = #column
253 | ) {
254 | recordIssue(
255 | "Delete 'matches' and re-record this assertion",
256 | fileID: fileID,
257 | filePath: filePath,
258 | line: line,
259 | column: column
260 | )
261 | }
262 |
263 | @available(
264 | *,
265 | deprecated,
266 | message: "Delete 'applyFixIts' and 'matches' and re-record this assertion"
267 | )
268 | public func assertMacro(
269 | _ macros: [Macro.Type],
270 | applyFixIts: Bool,
271 | record isRecording: Bool? = nil,
272 | of originalSource: () throws -> String,
273 | matches expandedOrDiagnosedSource: () -> String,
274 | fileID: StaticString = #fileID,
275 | file filePath: StaticString = #filePath,
276 | function: StaticString = #function,
277 | line: UInt = #line,
278 | column: UInt = #column
279 | ) {
280 | assertMacro(
281 | Dictionary(macros: macros),
282 | applyFixIts: applyFixIts,
283 | record: isRecording,
284 | of: originalSource,
285 | matches: expandedOrDiagnosedSource,
286 | fileID: fileID,
287 | file: filePath,
288 | function: function,
289 | line: line,
290 | column: column
291 | )
292 | }
293 |
--------------------------------------------------------------------------------
/Sources/MacroTesting/SwiftDiagnostics/DiagnosticsFormatter.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift.org open source project
4 | //
5 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See https://swift.org/LICENSE.txt for license information
9 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | import SwiftDiagnostics
14 | import SwiftSyntax
15 | import SwiftSyntaxMacroExpansion
16 |
17 | extension Sequence where Element == Range {
18 | /// Given a set of ranges that are sorted in order of nondecreasing lower
19 | /// bound, merge any overlapping ranges to produce a sequence of
20 | /// nonoverlapping ranges.
21 | fileprivate func mergingOverlappingRanges() -> [Range] {
22 | var result: [Range] = []
23 |
24 | var prior: Range? = nil
25 | for range in self {
26 | // If this is the first range we've seen, note it as the prior and
27 | // continue.
28 | guard let priorRange = prior else {
29 | prior = range
30 | continue
31 | }
32 |
33 | // If the ranges overlap, expand the prior range.
34 | precondition(priorRange.lowerBound <= range.lowerBound)
35 | if priorRange.overlaps(range) {
36 | let lower = priorRange.lowerBound
37 | let upper = Swift.max(priorRange.upperBound, range.upperBound)
38 | prior = lower.. String {
93 | let formatter = DiagnosticsFormatter(contextSize: contextSize, colorize: colorize)
94 | return formatter.annotatedSource(tree: tree, diags: diags, context: context)
95 | }
96 |
97 | /// Colorize the given source line by applying highlights from diagnostics.
98 | private func colorizeSourceLine(
99 | _ annotatedLine: AnnotatedSourceLine,
100 | lineNumber: Int,
101 | tree: some SyntaxProtocol,
102 | sourceLocationConverter slc: SourceLocationConverter
103 | ) -> String {
104 | guard colorize, !annotatedLine.diagnostics.isEmpty else {
105 | return annotatedLine.sourceString
106 | }
107 |
108 | // Compute the set of highlight ranges that land on this line. These
109 | // are column ranges, sorted in order of increasing starting column, and
110 | // with overlapping ranges merged.
111 | let highlightRanges: [Range] = annotatedLine.diagnostics.map {
112 | $0.highlights
113 | }.joined().compactMap { (highlight) -> Range? in
114 | if highlight.root != Syntax(tree) {
115 | return nil
116 | }
117 |
118 | let startLoc = highlight.startLocation(converter: slc, afterLeadingTrivia: true)
119 | let startLine = startLoc.line
120 |
121 | // Find the starting column.
122 | let startColumn: Int
123 | if startLine < lineNumber {
124 | startColumn = 1
125 | } else if startLine == lineNumber {
126 | startColumn = startLoc.column
127 | } else {
128 | return nil
129 | }
130 |
131 | // Find the ending column.
132 | let endLoc = highlight.endLocation(converter: slc, afterTrailingTrivia: false)
133 | let endLine = endLoc.line
134 |
135 | let endColumn: Int
136 | if endLine > lineNumber {
137 | endColumn = annotatedLine.sourceString.count
138 | } else if endLine == lineNumber {
139 | endColumn = endLoc.column
140 | } else {
141 | return nil
142 | }
143 |
144 | if startColumn == endColumn {
145 | return nil
146 | }
147 |
148 | return startColumn..] = highlightRanges.map { highlightRange in
156 | let startIndex = sourceStringUTF8.index(
157 | sourceStringUTF8.startIndex, offsetBy: highlightRange.lowerBound - 1)
158 | let endIndex = sourceStringUTF8.index(startIndex, offsetBy: highlightRange.count)
159 | return startIndex.. String {
197 | let slc =
198 | sourceLocationConverter ?? SourceLocationConverter(fileName: fileName ?? "", tree: tree)
199 |
200 | // First, we need to put each line and its diagnostics together
201 | var annotatedSourceLines = [AnnotatedSourceLine]()
202 |
203 | for (sourceLineIndex, sourceLine) in slc.sourceLines.enumerated() {
204 | let diagsForLine = diags.filter { diag in
205 | return diag.location(converter: slc).line == (sourceLineIndex + 1)
206 | }
207 | let suffixText = suffixTexts.compactMap { (position, text) in
208 | if slc.location(for: position).line == (sourceLineIndex + 1) {
209 | return text
210 | }
211 |
212 | return nil
213 | }.joined()
214 |
215 | annotatedSourceLines.append(
216 | AnnotatedSourceLine(
217 | diagnostics: diagsForLine, sourceString: sourceLine, suffixText: suffixText))
218 | }
219 |
220 | // Only lines with diagnostic messages should be printed, but including some context
221 | let rangesToPrint = annotatedSourceLines.enumerated().compactMap {
222 | (lineIndex, sourceLine) -> Range? in
223 | let lineNumber = lineIndex + 1
224 | if !sourceLine.isFreeOfAnnotations {
225 | return Range(
226 | uncheckedBounds: (lower: lineNumber - contextSize, upper: lineNumber + contextSize + 1))
227 | }
228 | return nil
229 | }
230 |
231 | var annotatedSource = ""
232 |
233 | // If there was a filename, add it first.
234 | if let fileName {
235 | let header = colorizeBufferOutline("===")
236 | let firstLine =
237 | 1
238 | + (annotatedSourceLines.enumerated().first { (lineIndex, sourceLine) in
239 | !sourceLine.isFreeOfAnnotations
240 | }?.offset ?? 0)
241 |
242 | annotatedSource.append("\(indentString)\(header) \(fileName):\(firstLine) \(header)\n")
243 | }
244 |
245 | /// Keep track if a line missing char should be printed
246 | var hasLineBeenSkipped = false
247 |
248 | let maxNumberOfDigits = String(annotatedSourceLines.count).count
249 |
250 | for (lineIndex, annotatedLine) in annotatedSourceLines.enumerated() {
251 | let lineNumber = lineIndex + 1
252 | guard
253 | rangesToPrint.contains(where: { range in
254 | range.contains(lineNumber)
255 | })
256 | else {
257 | hasLineBeenSkipped = true
258 | continue
259 | }
260 |
261 | // line numbers should be right aligned
262 | let lineNumberString = String(lineNumber)
263 | let leadingSpaces = String(repeating: " ", count: maxNumberOfDigits - lineNumberString.count)
264 | let linePrefix = "\(leadingSpaces)\(colorizeBufferOutline("\(lineNumberString) │")) "
265 |
266 | // If necessary, print a line that indicates that there was lines skipped in the source code
267 | if hasLineBeenSkipped && !annotatedSource.isEmpty {
268 | let lineMissingInfoLine =
269 | indentString + String(repeating: " ", count: maxNumberOfDigits)
270 | + " \(colorizeBufferOutline("┆"))"
271 | annotatedSource.append("\(lineMissingInfoLine)\n")
272 | }
273 | hasLineBeenSkipped = false
274 |
275 | // add indentation
276 | annotatedSource.append(indentString)
277 |
278 | // print the source line
279 | annotatedSource.append(linePrefix)
280 | annotatedSource.append(
281 | colorizeSourceLine(
282 | annotatedLine,
283 | lineNumber: lineNumber,
284 | tree: tree,
285 | sourceLocationConverter: slc
286 | )
287 | )
288 |
289 | // If the line did not end with \n (e.g. the last line), append it manually
290 | if annotatedSource.last != "\n" {
291 | annotatedSource.append("\n")
292 | }
293 |
294 | let columnsWithDiagnostics = Set(
295 | annotatedLine.diagnostics.map { $0.location(converter: slc).column })
296 | let diagsPerColumn = Dictionary(grouping: annotatedLine.diagnostics) { diag in
297 | diag.location(converter: slc).column
298 | }.sorted { lhs, rhs in
299 | lhs.key > rhs.key
300 | }
301 |
302 | let preMessagePrefix =
303 | indentString + String(repeating: " ", count: maxNumberOfDigits) + " "
304 | + colorizeBufferOutline("│")
305 | for (column, diags) in diagsPerColumn {
306 | // compute the string that is shown before each message
307 | var preMessage = preMessagePrefix
308 | for c in 0.. String {
351 | return annotatedSource(
352 | fileName: nil,
353 | tree: tree,
354 | diags: diags,
355 | context: context,
356 | indentString: "",
357 | suffixTexts: [:]
358 | )
359 | }
360 |
361 | /// Annotates the given ``DiagnosticMessage`` with an appropriate ANSI color code (if the value of the `colorize`
362 | /// property is `true`) and returns the result as a printable string.
363 | private func colorizeIfRequested(_ message: DiagnosticMessage) -> String {
364 | switch message.severity {
365 | case .error:
366 | let annotation = ANSIAnnotation(color: .red, trait: .bold)
367 | return colorizeIfRequested("🛑 \(message.message)", annotation: annotation)
368 |
369 | case .warning:
370 | let color = ANSIAnnotation(color: .yellow)
371 | let prefix = colorizeIfRequested("⚠️ ", annotation: color.withTrait(.bold))
372 |
373 | return prefix + colorizeIfRequested(message.message, annotation: color)
374 | default:
375 | let color = ANSIAnnotation(color: .default, trait: .bold)
376 | let prefix = colorizeIfRequested("ℹ️ ", annotation: color)
377 | return prefix + message.message
378 | }
379 | }
380 |
381 | /// Apply the given color and trait to the specified text, when we are
382 | /// supposed to color the output.
383 | private func colorizeIfRequested(
384 | _ text: String,
385 | annotation: ANSIAnnotation
386 | ) -> String {
387 | guard colorize, !text.isEmpty else {
388 | return text
389 | }
390 |
391 | return annotation.applied(to: text)
392 | }
393 |
394 | /// Colorize for the buffer outline and line numbers.
395 | func colorizeBufferOutline(_ text: String) -> String {
396 | colorizeIfRequested(text, annotation: .bufferOutline)
397 | }
398 | }
399 |
400 | struct ANSIAnnotation {
401 | enum Color: UInt8 {
402 | case normal = 0
403 | case black = 30
404 | case red = 31
405 | case green = 32
406 | case yellow = 33
407 | case blue = 34
408 | case magenta = 35
409 | case cyan = 36
410 | case white = 37
411 | case `default` = 39
412 | }
413 |
414 | enum Trait: UInt8 {
415 | case normal = 0
416 | case bold = 1
417 | case underline = 4
418 | }
419 |
420 | var color: Color
421 | var trait: Trait
422 |
423 | /// The textual representation of the annotation.
424 | var code: String {
425 | "\u{001B}[\(trait.rawValue);\(color.rawValue)m"
426 | }
427 |
428 | init(color: Color, trait: Trait = .normal) {
429 | self.color = color
430 | self.trait = trait
431 | }
432 |
433 | func withTrait(_ trait: Trait) -> Self {
434 | return ANSIAnnotation(color: self.color, trait: trait)
435 | }
436 |
437 | func applied(to message: String) -> String {
438 | // Resetting after the message ensures that we don't color unintended lines in the output
439 | return "\(code)\(message)\(ANSIAnnotation.normal.code)"
440 | }
441 |
442 | /// The "normal" or "reset" ANSI code used to unset any previously added annotation.
443 | static var normal: ANSIAnnotation {
444 | self.init(color: .normal, trait: .normal)
445 | }
446 |
447 | /// Annotation used for the outline and line numbers of a buffer.
448 | static var bufferOutline: ANSIAnnotation {
449 | ANSIAnnotation(color: .cyan, trait: .normal)
450 | }
451 |
452 | /// Annotation used for highlighting source text.
453 | static var sourceHighlight: ANSIAnnotation {
454 | ANSIAnnotation(color: .default, trait: .underline)
455 | }
456 | }
457 |
--------------------------------------------------------------------------------