├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── config.yml └── workflows │ ├── ci.yml │ └── format.yml ├── .gitignore ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── MacroTesting │ ├── AssertMacro.swift │ ├── Internal │ │ ├── AttributeRemover509.swift │ │ ├── AttributeRemover600.swift │ │ ├── Deprecations.swift │ │ ├── Diagnostic+UnderlineHighlights.swift │ │ └── RecordIssue.swift │ ├── MacrosTestTrait.swift │ ├── SwiftDiagnostics │ │ └── DiagnosticsFormatter.swift │ ├── SwiftSyntax │ │ └── SourceEdit.swift │ └── _SwiftSyntaxTestSupport │ │ └── FixItApplier.swift ├── MemberwiseInit │ └── MemberwiseInit.swift ├── MemberwiseInitClient │ └── main.swift └── MemberwiseInitMacros │ ├── MacroPlugin.swift │ └── Macros │ ├── MemberwiseInitMacro.swift │ ├── Support │ ├── AccesssLevelSyntax.swift │ ├── AttributeRemover.swift │ ├── CustomConfiguration.swift │ ├── DeprecationDiagnostics.swift │ ├── Diagnostics.swift │ ├── ExprTypeInference.swift │ ├── MemberwiseInitFormatter.swift │ ├── Models.swift │ ├── String.swift │ └── SyntaxHelpers.swift │ └── UncheckedMemberwiseInitMacro.swift ├── Tests ├── MacroTestingTests │ ├── AddAsyncTests.swift │ ├── AddBlockerTests.swift │ ├── AddCompletionHandlerTests.swift │ ├── AssertMacroTests.swift │ ├── BaseTestCase.swift │ ├── CaseDetectionMacroTests.swift │ ├── CustomCodableMacroTests.swift │ ├── DefaultFatalErrorImplementationMacroTests.swift │ ├── DiagnosticsAndFixitsEmitterMacroTests.swift │ ├── DictionaryStorageMacroTests.swift │ ├── EntryMacroTests.swift │ ├── EquatableExtensionMacroTests.swift │ ├── FixItTests.swift │ ├── FontLiteralMacroTests.swift │ ├── FuncUniqueMacroTests.swift │ ├── IndentationWidthTests.swift │ ├── MacroAttributeDiagnosticTests.swift │ ├── MacroAttributeDiagnosticTests2.swift │ ├── MacroExamples │ │ ├── AddAsyncMacro.swift │ │ ├── AddBlocker.swift │ │ ├── AddCompletionHandlerMacro.swift │ │ ├── CaseDetectionMacro.swift │ │ ├── CustomCodable.swift │ │ ├── DefaultFatalErrorImplementationMacro.swift │ │ ├── Diagnostics.swift │ │ ├── DiagnosticsAndFixitsEmitterMacro.swift │ │ ├── DictionaryIndirectionMacro.swift │ │ ├── EntryMacro.swift │ │ ├── EquatableExtensionMacro.swift │ │ ├── FontLiteralMacro.swift │ │ ├── FuncUniqueMacro.swift │ │ ├── MemberDeprecatedMacro.swift │ │ ├── MemberwiseInitMacro.swift │ │ ├── MetaEnumMacro.swift │ │ ├── NewTypeMacro.swift │ │ ├── ObservableMacro.swift │ │ ├── OptionSetMacro.swift │ │ ├── PeerValueWithSuffixMacro.swift │ │ ├── StringifyMacro.swift │ │ ├── SwiftSyntaxCompatibilityExtensions.swift │ │ ├── URLMacro.swift │ │ ├── WarningMacro.swift │ │ └── WrapStoredPropertiesMacro.swift │ ├── MacroNameTests.swift │ ├── MemberDeprecatedMacroTests.swift │ ├── MetaEnumMacroTests.swift │ ├── NewTypeMacroTests.swift │ ├── ObservableMacroTests.swift │ ├── OptionSetMacroTests.swift │ ├── PeerValueWithSuffixMacroTests.swift │ ├── StringifyMacroTests.swift │ ├── SwiftTestingTests.swift │ ├── URLMacroTests.swift │ ├── WarningMacroTests.swift │ └── WrapStoredPropertiesMacroTests.swift └── MemberwiseInitTests │ ├── CustomInitDefaultTests.swift │ ├── CustomInitRawTests.swift │ ├── CustomInitTests.swift │ ├── CustomInitWrapperTests.swift │ ├── InvalidSyntaxTests.swift │ ├── LayeredDiagnosticsTests.swift │ ├── MemberwiseInitAccessLevelTests.swift │ ├── MemberwiseInitDeprecationTests.swift │ ├── MemberwiseInitInferredTypeTests.swift │ ├── MemberwiseInitTests.swift │ ├── ReadmeTests.swift │ └── UncheckedMemberwiseInitTests.swift └── bin └── generate_access_level_tests.sh /.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 | -------------------------------------------------------------------------------- /.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-memberwise-init-macro! 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/gohanlon/swift-memberwise-init-macro/issues) or [discussion](https://github.com/gohanlon/swift-memberwise-init-macro/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-memberwise-init-macro version information 54 | description: The version of swift-memberwise-init-macro 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-memberwise-init-macro. 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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Project Discussion 5 | url: https://github.com/gohanlon/swift-memberwise-init-macro/discussions 6 | about: Library Q&A, ideas, and more 7 | - name: README 8 | url: https://github.com/gohanlon/swift-memberwise-init-macro 9 | about: Read the README 10 | -------------------------------------------------------------------------------- /.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 | env: 17 | SNAPSHOT_TESTING_RECORD: "never" 18 | 19 | jobs: 20 | macos: 21 | name: macOS 22 | runs-on: macos-13 23 | strategy: 24 | matrix: 25 | swift-syntax-version: 26 | [ 27 | "509.0.0..<510.0.0", 28 | "510.0.0..<511.0.0", 29 | "511.0.0..<601.0.0", 30 | ] 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Select Xcode 15 35 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 36 | - name: Set SWIFT_SYNTAX_VERSION environment variable 37 | run: echo "SWIFT_SYNTAX_VERSION=${{ matrix.swift-syntax-version }}" >> $GITHUB_ENV 38 | - name: Resolve Dependencies 39 | run: swift package resolve 40 | - name: Run tests 41 | run: swift test 42 | 43 | linux: 44 | name: Linux 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Install Swift 48 | uses: swift-actions/setup-swift@v2 49 | with: 50 | swift-version: "6.0.2" 51 | - uses: actions/checkout@v4 52 | - name: Run tests 53 | run: swift test 54 | 55 | # NB: 5.9 snapshot unavailable, wait for release 56 | # wasm: 57 | # name: Wasm 58 | # runs-on: ubuntu-latest 59 | # strategy: 60 | # matrix: 61 | # include: 62 | # - { toolchain: wasm-5.9-RELEASE } 63 | # steps: 64 | # - uses: actions/checkout@v4 65 | # - run: echo "${{ matrix.toolchain }}" > .swift-version 66 | # - uses: swiftwasm/swiftwasm-action@v5.9 67 | # with: 68 | # shell-action: carton test --environment node 69 | 70 | # NB: 5.9 snapshot outdated, wait for release 71 | # windows: 72 | # name: Windows 73 | # runs-on: windows-latest 74 | # steps: 75 | # - uses: compnerd/gha-setup-swift@main 76 | # with: 77 | # branch: swift-5.9-release 78 | # tag: 5.9-DEVELOPMENT-SNAPSHOT-2023-09-16-a 79 | # - uses: actions/checkout@v4 80 | # - name: Run tests 81 | # run: swift test 82 | -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.swiftinterface 6 | /*.xcodeproj 7 | xcuserdata/ 8 | tmp 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2025 Galen O’Hanlon 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | default: test 2 | 3 | test: test-swift 4 | 5 | test-swift: 6 | swift test --parallel 7 | 8 | # Remove build artifacts while tolerating SourceKit-locked files 9 | clean: 10 | @echo "Cleaning build artifacts..." 11 | @rm -rf .build/* 2>&1 | grep -v "Permission denied" || true 12 | @echo "Checking remaining build artifacts..." 13 | @if [ -d .build ]; then \ 14 | ls -R .build 2>/dev/null || echo "Unable to list some directories"; \ 15 | else \ 16 | echo "Build directory completely removed"; \ 17 | fi 18 | @echo "Build artifacts cleaned (ensure that any remaining files listed above won't affect new builds, e.g. SourceKit files)" 19 | 20 | test-swift-syntax-versions: 21 | @for version in \ 22 | "509.0.0..<510.0.0" \ 23 | "510.0.0..<511.0.0" \ 24 | "511.0.0..<601.0.0"; \ 25 | do \ 26 | echo "\n## Testing SwiftSyntax version $$version"; \ 27 | $(MAKE) clean; \ 28 | SWIFT_SYNTAX_VERSION="$$version" $(MAKE) test-swift || exit 1; \ 29 | done 30 | 31 | format: 32 | swift format \ 33 | --ignore-unparsable-files \ 34 | --in-place \ 35 | --recursive \ 36 | ./Package.swift ./Sources ./Tests 37 | 38 | .PHONY: default test test-swift clean test-swift-syntax-versions format 39 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-custom-dump", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 7 | "state" : { 8 | "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", 9 | "version" : "1.3.3" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-snapshot-testing", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 16 | "state" : { 17 | "revision" : "b2d4cb30735f4fbc3a01963a9c658336dd21e9ba", 18 | "version" : "1.18.1" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-syntax", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/swiftlang/swift-syntax", 25 | "state" : { 26 | "revision" : "0687f71944021d616d34d922343dcef086855920", 27 | "version" : "600.0.1" 28 | } 29 | }, 30 | { 31 | "identity" : "xctest-dynamic-overlay", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 34 | "state" : { 35 | "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", 36 | "version" : "1.5.2" 37 | } 38 | } 39 | ], 40 | "version" : 2 41 | } 42 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import CompilerPluginSupport 4 | import Foundation 5 | import PackageDescription 6 | 7 | let package = Package( 8 | name: "MemberwiseInit", 9 | platforms: [ 10 | .iOS(.v13), 11 | .macOS(.v10_15), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | ], 15 | products: [ 16 | .library( 17 | name: "MemberwiseInit", 18 | targets: ["MemberwiseInit"] 19 | ), 20 | .executable( 21 | name: "MemberwiseInitClient", 22 | targets: ["MemberwiseInitClient"] 23 | ), 24 | ], 25 | dependencies: [ 26 | .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.1"), 27 | //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "509.0.0..<510.0.0") 28 | //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "510.0.0..<511.0.0") 29 | //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "511.0.0..<601.0.0-prerelease") 30 | .conditionalPackage( 31 | url: "https://github.com/swiftlang/swift-syntax", 32 | envVar: "SWIFT_SYNTAX_VERSION", 33 | default: "509.0.0..<601.0.0" 34 | ), 35 | ], 36 | targets: [ 37 | .macro( 38 | name: "MemberwiseInitMacros", 39 | dependencies: [ 40 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 41 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 42 | ] 43 | ), 44 | .target( 45 | name: "MemberwiseInit", 46 | dependencies: ["MemberwiseInitMacros"] 47 | ), 48 | .executableTarget( 49 | name: "MemberwiseInitClient", 50 | dependencies: ["MemberwiseInit"] 51 | ), 52 | .testTarget( 53 | name: "MemberwiseInitTests", 54 | dependencies: [ 55 | "MemberwiseInitMacros", 56 | "MacroTesting", 57 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 58 | ] 59 | ), 60 | .target( 61 | name: "MacroTesting", 62 | dependencies: [ 63 | .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), 64 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 65 | .product(name: "SwiftOperators", package: "swift-syntax"), 66 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"), 67 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 68 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 69 | ] 70 | ), 71 | .testTarget( 72 | name: "MacroTestingTests", 73 | dependencies: [ 74 | "MacroTesting" 75 | ] 76 | ), 77 | ] 78 | ) 79 | 80 | extension Package.Dependency { 81 | /// Creates a dependency based on an environment variable or a default version range. 82 | /// 83 | /// This function allows dynamically setting the version range of a package dependency via an environment variable. 84 | /// If the environment variable is not set, it falls back to a specified default version range. 85 | /// 86 | /// - Parameters: 87 | /// - url: The URL of the package repository. 88 | /// - envVar: The name of the environment variable that contains the version range. 89 | /// - versionExpression: The default version range in case the environment variable is not set. 90 | /// Example format: `"509.0.0..<511.0.0"` or `"509.0.0...510.0.0"`. 91 | /// - Returns: A `Package.Dependency` configured with the specified or default version range. 92 | /// - Throws: A fatal error if the version expression format is invalid or the range operator is unsupported. 93 | /// 94 | static func conditionalPackage( 95 | url: String, 96 | envVar: String, 97 | default versionExpression: String 98 | ) -> Package.Dependency { 99 | let versionRangeString = ProcessInfo.processInfo.environment[envVar] ?? versionExpression 100 | let (lower, op, upper) = parseVersionExpression(from: versionRangeString) 101 | if op == "..<" { 102 | return .package(url: url, lower.. (Version, String, Version) { 113 | let rangeOperators = ["..<", "..."] 114 | for op in rangeOperators { 115 | if expression.contains(op) { 116 | let parts = expression.split(separator: op, maxSplits: 1, omittingEmptySubsequences: true) 117 | .map(String.init) 118 | guard 119 | parts.count == 2, 120 | let lower = Version(parts[0]), 121 | let upper = Version(parts[1]) 122 | else { 123 | fatalError("Invalid version expression format: \(expression)") 124 | } 125 | return (lower, op, upper) 126 | } 127 | } 128 | fatalError("No valid range operator found in expression: \(expression)") 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/MacroTesting/Internal/AttributeRemover509.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // AttributeRemover 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 | /// Removes attributes from a syntax tree while maintaining their surrounding trivia. 16 | class AttributeRemover509: SyntaxRewriter { 17 | let predicate: (AttributeSyntax) -> Bool 18 | 19 | var triviaToAttachToNextToken: Trivia = Trivia() 20 | 21 | init(removingWhere predicate: @escaping (AttributeSyntax) -> Bool) { 22 | self.predicate = predicate 23 | } 24 | 25 | override func visit(_ node: AttributeListSyntax) -> AttributeListSyntax { 26 | var filteredAttributes: [AttributeListSyntax.Element] = [] 27 | for case .attribute(let attribute) in node { 28 | if self.predicate(attribute) { 29 | var leadingTrivia = node.leadingTrivia 30 | if let lastNewline = leadingTrivia.pieces.lastIndex(where: { $0.isNewline }), 31 | leadingTrivia.pieces[lastNewline...].allSatisfy(\.isWhitespace), 32 | node.trailingTrivia.isEmpty, 33 | node.nextToken(viewMode: .sourceAccurate)?.leadingTrivia.first?.isNewline ?? false 34 | { 35 | // If the attribute is on its own line based on the following conditions, 36 | // remove the newline from it so we don’t end up with an empty line 37 | // - Trailing trivia ends with a newline followed by arbitrary number of spaces or tabs 38 | // - There is no trailing trivia and the next token starts on a new line 39 | leadingTrivia = Trivia(pieces: leadingTrivia.pieces[.. TokenSyntax { 54 | if !triviaToAttachToNextToken.isEmpty { 55 | defer { triviaToAttachToNextToken = Trivia() } 56 | return token.with(\.leadingTrivia, triviaToAttachToNextToken + token.leadingTrivia) 57 | } else { 58 | return token 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/MacroTesting/Internal/AttributeRemover600.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | public class AttributeRemover600: SyntaxRewriter { 4 | let predicate: (AttributeSyntax) -> Bool 5 | 6 | var triviaToAttachToNextToken: Trivia = Trivia() 7 | 8 | /// Initializes an attribute remover with a given predicate to determine which attributes to remove. 9 | /// 10 | /// - Parameter predicate: A closure that determines whether a given `AttributeSyntax` should be removed. 11 | /// If this closure returns `true` for an attribute, that attribute will be removed. 12 | public init(removingWhere predicate: @escaping (AttributeSyntax) -> Bool) { 13 | self.predicate = predicate 14 | } 15 | 16 | public override func visit(_ node: AttributeListSyntax) -> AttributeListSyntax { 17 | var filteredAttributes: [AttributeListSyntax.Element] = [] 18 | for case .attribute(let attribute) in node { 19 | if self.predicate(attribute) { 20 | var leadingTrivia = attribute.leadingTrivia 21 | 22 | // Don't leave behind an empty line when the attribute being removed is on its own line, 23 | // based on the following conditions: 24 | // - Leading trivia ends with a newline followed by arbitrary number of spaces or tabs 25 | // - All leading trivia pieces after the last newline are just whitespace, ensuring 26 | // there are no comments or other non-whitespace characters on the same line 27 | // preceding the attribute. 28 | // - There is no trailing trivia and the next token has leading trivia. 29 | if let lastNewline = leadingTrivia.pieces.lastIndex(where: \.isNewline), 30 | leadingTrivia.pieces[lastNewline...].allSatisfy(\.isWhitespace), 31 | attribute.trailingTrivia.isEmpty, 32 | let nextToken = attribute.nextToken(viewMode: .sourceAccurate), 33 | !nextToken.leadingTrivia.isEmpty 34 | { 35 | leadingTrivia = Trivia(pieces: leadingTrivia.pieces[.. TokenSyntax { 76 | return prependAndClearAccumulatedTrivia(to: token) 77 | } 78 | 79 | /// Prepends the accumulated trivia to the given node's leading trivia. 80 | /// 81 | /// To preserve correct formatting after attribute removal, this function reassigns 82 | /// significant trivia accumulated from removed attributes to the provided subsequent node. 83 | /// Once attached, the accumulated trivia is cleared. 84 | /// 85 | /// - Parameter node: The syntax node receiving the accumulated trivia. 86 | /// - Returns: The modified syntax node with the prepended trivia. 87 | private func prependAndClearAccumulatedTrivia(to syntaxNode: T) -> T { 88 | defer { triviaToAttachToNextToken = Trivia() } 89 | return syntaxNode.with(\.leadingTrivia, triviaToAttachToNextToken + syntaxNode.leadingTrivia) 90 | } 91 | } 92 | 93 | extension Trivia { 94 | fileprivate func trimmingPrefix( 95 | while predicate: (TriviaPiece) -> Bool 96 | ) -> Trivia { 97 | Trivia(pieces: self.drop(while: predicate)) 98 | } 99 | 100 | fileprivate func trimmingSuffix( 101 | while predicate: (TriviaPiece) -> Bool 102 | ) -> Trivia { 103 | Trivia( 104 | pieces: self[...] 105 | .reversed() 106 | .drop(while: predicate) 107 | .reversed() 108 | ) 109 | } 110 | 111 | fileprivate var startsWithNewline: Bool { 112 | self.first?.isNewline ?? false 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /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.. 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 | -------------------------------------------------------------------------------- /Sources/MacroTesting/MacrosTestTrait.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) 2 | import SnapshotTesting 3 | import SwiftSyntax 4 | import SwiftSyntaxMacros 5 | import Testing 6 | 7 | @_spi(Experimental) 8 | extension Trait where Self == _MacrosTestTrait { 9 | /// Configure snapshot testing in a suite or test. 10 | /// 11 | /// - Parameters: 12 | /// - record: The record mode of the test. 13 | /// - diffTool: The diff tool to use in failure messages. 14 | public static func macros( 15 | indentationWidth: Trivia? = nil, 16 | record: SnapshotTestingConfiguration.Record? = nil, 17 | macros: [String: Macro.Type]? = nil 18 | ) -> Self { 19 | _MacrosTestTrait( 20 | configuration: MacroTestingConfiguration( 21 | indentationWidth: indentationWidth, 22 | macros: macros 23 | ), 24 | record: record 25 | ) 26 | } 27 | } 28 | 29 | /// A type representing the configuration of snapshot testing. 30 | @_spi(Experimental) 31 | public struct _MacrosTestTrait: SuiteTrait, TestTrait { 32 | public let isRecursive = true 33 | let configuration: MacroTestingConfiguration 34 | let record: SnapshotTestingConfiguration.Record? 35 | } 36 | 37 | extension Test { 38 | var indentationWidth: Trivia? { 39 | for trait in traits.reversed() { 40 | if let indentationWidth = (trait as? _MacrosTestTrait)?.configuration.indentationWidth { 41 | return indentationWidth 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | var macros: [String: Macro.Type]? { 48 | for trait in traits.reversed() { 49 | if let macros = (trait as? _MacrosTestTrait)?.configuration.macros { 50 | return macros 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | var record: SnapshotTestingConfiguration.Record? { 57 | for trait in traits.reversed() { 58 | if let macros = (trait as? _MacrosTestTrait)?.record { 59 | return macros 60 | } 61 | } 62 | return nil 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /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.. 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.. Bool { 36 | let lhs = Self.allCases.firstIndex(of: lhs)! 37 | let rhs = Self.allCases.firstIndex(of: rhs)! 38 | return lhs < rhs 39 | } 40 | } 41 | 42 | public protocol AccessLevelSyntax { 43 | var parent: Syntax? { get } 44 | var modifiers: DeclModifierListSyntax { get set } 45 | } 46 | 47 | extension AccessLevelSyntax { 48 | var accessLevelModifiers: [AccessLevelModifier]? { 49 | get { 50 | let accessLevels = modifiers.lazy.compactMap { AccessLevelModifier(rawValue: $0.name.text) } 51 | return accessLevels.isEmpty ? nil : Array(accessLevels) 52 | } 53 | set { 54 | guard let newModifiers = newValue else { 55 | modifiers = [] 56 | return 57 | } 58 | let newModifierKeywords = newModifiers.map { DeclModifierSyntax(name: .keyword($0.keyword)) } 59 | let filteredModifiers = modifiers.filter { 60 | AccessLevelModifier(rawValue: $0.name.text) == nil 61 | } 62 | modifiers = filteredModifiers + newModifierKeywords 63 | } 64 | } 65 | } 66 | 67 | protocol DeclGroupAccessLevelSyntax: AccessLevelSyntax { 68 | } 69 | extension DeclGroupAccessLevelSyntax { 70 | public var accessLevel: AccessLevelModifier { 71 | self.accessLevelModifiers?.first ?? .internal 72 | } 73 | } 74 | 75 | extension ActorDeclSyntax: DeclGroupAccessLevelSyntax {} 76 | extension ClassDeclSyntax: DeclGroupAccessLevelSyntax {} 77 | extension EnumDeclSyntax: DeclGroupAccessLevelSyntax {} 78 | extension StructDeclSyntax: DeclGroupAccessLevelSyntax {} 79 | 80 | // NB: MemberwiseInit doesn't need this on FunctionDeclSyntax extension 81 | //extension FunctionDeclSyntax: AccessLevelSyntax { 82 | // public var accessLevel: AccessLevelModifier { 83 | // get { 84 | // // a decl (function, variable) can 85 | // if let formalModifier = self.accessLevelModifiers?.first { 86 | // return formalModifier 87 | // } 88 | // 89 | // guard let parent = self.parent else { return .internal } 90 | // 91 | // if let parent = parent as? DeclGroupSyntax { 92 | // return [parent.declAccessLevel, .internal].min()! 93 | // } else { 94 | // return .internal 95 | // } 96 | // } 97 | // } 98 | //} 99 | 100 | extension VariableDeclSyntax: AccessLevelSyntax { 101 | var accessLevel: AccessLevelModifier { 102 | // TODO: assuming the least access of the modifiers may not be correct, but it suits the special case of MemberwiseInit 103 | // maybe this is generally okay, since the "set" detail must be given less access than then get? either way, this needs to be made clearer 104 | self.accessLevelModifiers?.min() ?? inferDefaultAccessLevel(node: self._syntaxNode) 105 | } 106 | } 107 | 108 | private func inferDefaultAccessLevel(node: Syntax?) -> AccessLevelModifier { 109 | guard let node else { return .internal } 110 | guard let decl = node.asProtocol(DeclGroupSyntax.self) else { 111 | return inferDefaultAccessLevel(node: node.parent) 112 | } 113 | 114 | return [decl.declAccessLevel, .internal].min()! 115 | } 116 | 117 | // NB: This extension is sugar to avoid user needing to first cast to a specific kind of decl group syntax 118 | extension DeclGroupSyntax { 119 | var declAccessLevel: AccessLevelModifier { 120 | (self as? DeclGroupAccessLevelSyntax)!.accessLevel 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/AttributeRemover.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | /// Removes attributes from a syntax tree while maintaining their surrounding trivia. 4 | public class AttributeRemover: SyntaxRewriter { 5 | let predicate: (AttributeSyntax) -> Bool 6 | 7 | var triviaToAttachToNextToken: Trivia = Trivia() 8 | 9 | /// Initializes an attribute remover with a given predicate to determine which attributes to remove. 10 | /// 11 | /// - Parameter predicate: A closure that determines whether a given `AttributeSyntax` should be removed. 12 | /// If this closure returns `true` for an attribute, that attribute will be removed. 13 | public init(removingWhere predicate: @escaping (AttributeSyntax) -> Bool) { 14 | self.predicate = predicate 15 | } 16 | 17 | public override func visit(_ nodeList: AttributeListSyntax) -> AttributeListSyntax { 18 | var filteredAttributes: [AttributeListSyntax.Element] = [] 19 | 20 | for node in nodeList { 21 | switch node { 22 | case .attribute(let attribute): 23 | guard self.predicate(attribute) else { 24 | filteredAttributes.append(.attribute(prependAndClearAccumulatedTrivia(to: attribute))) 25 | continue 26 | } 27 | 28 | var leadingTrivia = attribute.leadingTrivia 29 | 30 | // Don't leave behind an empty line when the attribute being removed is on its own line, 31 | // based on the following conditions: 32 | // - Leading trivia ends with a newline followed by arbitrary number of spaces or tabs 33 | // - All leading trivia pieces after the last newline are just whitespace, ensuring 34 | // there are no comments or other non-whitespace characters on the same line 35 | // preceding the attribute. 36 | // - There is no trailing trivia and the next token has leading trivia. 37 | if let lastNewline = leadingTrivia.pieces.lastIndex(where: \.isNewline), 38 | leadingTrivia.pieces[lastNewline...].allSatisfy(\.isWhitespace), 39 | attribute.trailingTrivia.isEmpty, 40 | let nextToken = attribute.nextToken(viewMode: .sourceAccurate), 41 | !nextToken.leadingTrivia.isEmpty 42 | { 43 | leadingTrivia = Trivia(pieces: leadingTrivia.pieces[.. TokenSyntax { 85 | return prependAndClearAccumulatedTrivia(to: token) 86 | } 87 | 88 | /// Prepends the accumulated trivia to the given node's leading trivia. 89 | /// 90 | /// To preserve correct formatting after attribute removal, this function reassigns 91 | /// significant trivia accumulated from removed attributes to the provided subsequent node. 92 | /// Once attached, the accumulated trivia is cleared. 93 | /// 94 | /// - Parameter node: The syntax node receiving the accumulated trivia. 95 | /// - Returns: The modified syntax node with the prepended trivia. 96 | private func prependAndClearAccumulatedTrivia(to syntaxNode: T) -> T { 97 | defer { self.triviaToAttachToNextToken = Trivia() } 98 | return syntaxNode.with( 99 | \.leadingTrivia, self.triviaToAttachToNextToken + syntaxNode.leadingTrivia) 100 | } 101 | } 102 | 103 | extension Trivia { 104 | fileprivate func trimmingPrefix( 105 | while predicate: (TriviaPiece) -> Bool 106 | ) -> Trivia { 107 | Trivia(pieces: self.drop(while: predicate)) 108 | } 109 | 110 | fileprivate func trimmingSuffix( 111 | while predicate: (TriviaPiece) -> Bool 112 | ) -> Trivia { 113 | Trivia( 114 | pieces: self[...] 115 | .reversed() 116 | .drop(while: predicate) 117 | .reversed() 118 | ) 119 | } 120 | 121 | fileprivate var startsWithNewline: Bool { 122 | self.first?.isNewline ?? false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/CustomConfiguration.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | 3 | extension VariableDeclSyntax { 4 | var customConfigurationAttributes: [AttributeSyntax] { 5 | self.attributes 6 | .compactMap { $0.as(AttributeSyntax.self) } 7 | .filter { 8 | ["Init", "InitWrapper", "InitRaw"].contains($0.attributeName.trimmedDescription) 9 | } 10 | } 11 | 12 | var customConfigurationAttribute: AttributeSyntax? { 13 | self.customConfigurationAttributes.first 14 | } 15 | 16 | var hasNonConfigurationAttributes: Bool { 17 | self.attributes.filter { attribute in 18 | switch attribute { 19 | case .attribute: 20 | true 21 | case .ifConfigDecl: 22 | false 23 | } 24 | }.count != self.customConfigurationAttributes.count 25 | } 26 | 27 | var hasCustomConfigurationAttribute: Bool { 28 | !self.customConfigurationAttributes.isEmpty 29 | } 30 | 31 | var customConfigurationArguments: LabeledExprListSyntax? { 32 | self.customConfigurationAttribute? 33 | .arguments? 34 | .as(LabeledExprListSyntax.self) 35 | } 36 | 37 | func hasSoleArgument(_ label: String) -> Bool { 38 | guard let arguments = self.customConfigurationArguments else { return false } 39 | return arguments.count == 1 && arguments.first?.label?.text == label 40 | } 41 | 42 | func includesArgument(_ label: String) -> Bool { 43 | guard let arguments = self.customConfigurationArguments else { return false } 44 | return arguments.first(where: { $0.label?.text == label }) != nil 45 | } 46 | } 47 | 48 | extension LabeledExprListSyntax { 49 | func firstWhereLabel(_ label: String) -> Element? { 50 | first(where: { $0.label?.text == label }) 51 | } 52 | } 53 | 54 | extension AttributeSyntax { 55 | var isInitWrapper: Bool { 56 | self.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "InitWrapper" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/DeprecationDiagnostics.swift: -------------------------------------------------------------------------------- 1 | import SwiftDiagnostics 2 | import SwiftSyntax 3 | import SwiftSyntaxMacroExpansion 4 | 5 | func deprecationDiagnostics( 6 | node: AttributeSyntax, 7 | declaration decl: some DeclGroupSyntax 8 | ) -> [Diagnostic] { 9 | return diagnoseDotEscaping(decl) 10 | } 11 | 12 | private func diagnoseDotEscaping(_ decl: D) -> [Diagnostic] { 13 | guard let decl = decl.as(StructDeclSyntax.self) else { return [] } 14 | 15 | return decl.memberBlock.members.compactMap { element -> Diagnostic? in 16 | guard 17 | let configuration = element.decl 18 | .as(VariableDeclSyntax.self)? 19 | .customConfigurationArguments 20 | else { return nil } 21 | 22 | let dotEscapingIndex = configuration.firstIndex( 23 | where: { 24 | $0.expression 25 | .as(MemberAccessExprSyntax.self)? 26 | .declName.baseName.text == "escaping" 27 | } 28 | ) 29 | guard let dotEscapingIndex else { return nil } 30 | 31 | let newIndex = 32 | configuration.firstIndex(where: { $0.label?.text == "label" }) 33 | .map { configuration.index(before: $0) } 34 | ?? configuration.endIndex 35 | 36 | let newEscaping = LabeledExprSyntax( 37 | label: .identifier("escaping"), 38 | colon: .colonToken(trailingTrivia: .space), 39 | expression: BooleanLiteralExprSyntax(booleanLiteral: true), 40 | trailingComma: newIndex != configuration.endIndex ? .commaToken(trailingTrivia: .space) : nil 41 | ) 42 | 43 | var newConfiguration = configuration 44 | newConfiguration.remove(at: dotEscapingIndex) 45 | newConfiguration.insert(newEscaping, at: newIndex) 46 | 47 | return Diagnostic( 48 | node: configuration, 49 | message: MacroExpansionWarningMessage( 50 | """ 51 | @Init(.escaping) is deprecated 52 | """ 53 | ), 54 | fixIt: FixIt( 55 | message: MacroExpansionFixItMessage( 56 | "Replace '@Init(.escaping)' with '@Init(escaping: true)'" 57 | ), 58 | changes: [ 59 | FixIt.Change.replace( 60 | oldNode: Syntax(configuration), 61 | newNode: Syntax(newConfiguration) 62 | ) 63 | ] 64 | ) 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | 4 | struct MemberwiseInitFormatter { 5 | static func formatInitializer( 6 | properties: [MemberProperty], 7 | accessLevel: AccessLevelModifier, 8 | deunderscoreParameters: Bool, 9 | optionalsDefaultNil: Bool? 10 | ) -> InitializerDeclSyntax { 11 | let formattedParameters = formatParameters( 12 | properties: properties, 13 | deunderscoreParameters: deunderscoreParameters, 14 | optionalsDefaultNil: optionalsDefaultNil, 15 | accessLevel: accessLevel 16 | ) 17 | 18 | let formattedInitSignature = "\n\(accessLevel) init(\(formattedParameters))" 19 | 20 | return try! InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) { 21 | CodeBlockItemListSyntax( 22 | properties.map { property in 23 | CodeBlockItemSyntax( 24 | stringLiteral: formatInitializerAssignmentStatement( 25 | for: property, 26 | considering: properties, 27 | deunderscoreParameters: deunderscoreParameters 28 | ) 29 | ) 30 | } 31 | ) 32 | } 33 | } 34 | 35 | private static func formatParameters( 36 | properties: [MemberProperty], 37 | deunderscoreParameters: Bool, 38 | optionalsDefaultNil: Bool?, 39 | accessLevel: AccessLevelModifier 40 | ) -> String { 41 | guard !properties.isEmpty else { return "" } 42 | 43 | return "\n" 44 | + properties 45 | .map { property in 46 | formatParameter( 47 | for: property, 48 | considering: properties, 49 | deunderscoreParameters: deunderscoreParameters, 50 | optionalsDefaultNil: optionalsDefaultNil 51 | ?? MemberwiseInitMacro.defaultOptionalsDefaultNil( 52 | for: property.keywordToken, 53 | initAccessLevel: accessLevel 54 | ) 55 | ) 56 | } 57 | .joined(separator: ",\n") + "\n" 58 | } 59 | 60 | private static func formatParameter( 61 | for property: MemberProperty, 62 | considering allProperties: [MemberProperty], 63 | deunderscoreParameters: Bool, 64 | optionalsDefaultNil: Bool 65 | ) -> String { 66 | let defaultValue = 67 | property.initializerValue.map { " = \($0.description)" } 68 | ?? property.customSettings?.defaultValue.map { " = \($0)" } 69 | ?? (optionalsDefaultNil && property.type.isOptionalType ? " = nil" : "") 70 | 71 | let escaping = 72 | (property.customSettings?.forceEscaping ?? false || property.type.isFunctionType) 73 | ? "@escaping " : "" 74 | 75 | let label = property.initParameterLabel( 76 | considering: allProperties, deunderscoreParameters: deunderscoreParameters) 77 | 78 | let parameterName = property.initParameterName( 79 | considering: allProperties, deunderscoreParameters: deunderscoreParameters) 80 | 81 | return "\(label)\(parameterName): \(escaping)\(property.type.description)\(defaultValue)" 82 | } 83 | 84 | private static func formatInitializerAssignmentStatement( 85 | for property: MemberProperty, 86 | considering allProperties: [MemberProperty], 87 | deunderscoreParameters: Bool 88 | ) -> String { 89 | let assignee = 90 | switch property.customSettings?.assignee { 91 | case .none: 92 | "self.\(property.name)" 93 | case .wrapper: 94 | "self._\(property.name)" 95 | case let .raw(assignee): 96 | assignee 97 | } 98 | 99 | let parameterName = property.initParameterName( 100 | considering: allProperties, 101 | deunderscoreParameters: deunderscoreParameters 102 | ) 103 | return "\(assignee) = \(parameterName)" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/Models.swift: -------------------------------------------------------------------------------- 1 | import SwiftDiagnostics 2 | import SwiftSyntax 3 | 4 | struct VariableCustomSettings: Equatable { 5 | enum Assignee: Equatable { 6 | case wrapper 7 | case raw(String) 8 | } 9 | 10 | let accessLevel: AccessLevelModifier? 11 | let assignee: Assignee? 12 | let defaultValue: String? 13 | let forceEscaping: Bool 14 | let ignore: Bool 15 | let label: String? 16 | let type: TypeSyntax? 17 | let _syntaxNode: AttributeSyntax 18 | 19 | var customAttributeName: String { 20 | self._syntaxNode.attributeName.trimmedDescription 21 | } 22 | 23 | func diagnosticOnDefault(_ message: DiagnosticMessage, fixIts: [FixIt] = []) -> Diagnostic { 24 | let labelNode = self._syntaxNode 25 | .arguments? 26 | .as(LabeledExprListSyntax.self)? 27 | .firstWhereLabel("default") 28 | 29 | return diagnostic(node: labelNode ?? self._syntaxNode, message: message, fixIts: fixIts) 30 | } 31 | 32 | func diagnosticOnLabel(_ message: DiagnosticMessage, fixIts: [FixIt] = []) -> Diagnostic { 33 | let labelNode = self._syntaxNode 34 | .arguments? 35 | .as(LabeledExprListSyntax.self)? 36 | .firstWhereLabel("label") 37 | 38 | return diagnostic(node: labelNode ?? self._syntaxNode, message: message, fixIts: fixIts) 39 | } 40 | 41 | func diagnosticOnLabelValue(_ message: DiagnosticMessage) -> Diagnostic { 42 | let labelValueNode = self._syntaxNode 43 | .arguments? 44 | .as(LabeledExprListSyntax.self)? 45 | .firstWhereLabel("label")? 46 | .expression 47 | 48 | return diagnostic(node: labelValueNode ?? self._syntaxNode, message: message) 49 | } 50 | 51 | private func diagnostic( 52 | node: any SyntaxProtocol, 53 | message: DiagnosticMessage, 54 | fixIts: [FixIt] = [] 55 | ) -> Diagnostic { 56 | Diagnostic(node: node, message: message, fixIts: fixIts) 57 | } 58 | } 59 | 60 | struct PropertyBinding { 61 | let typeFromTrailingBinding: TypeSyntax? 62 | let syntax: PatternBindingSyntax 63 | let variable: MemberVariable 64 | 65 | var effectiveType: TypeSyntax? { 66 | variable.customSettings?.type 67 | ?? self.syntax.typeAnnotation?.type 68 | ?? self.syntax.initializer?.value.inferredTypeSyntax 69 | ?? self.typeFromTrailingBinding 70 | } 71 | 72 | var initializerValue: ExprSyntax? { 73 | self.syntax.initializer?.trimmed.value 74 | } 75 | 76 | var isTuplePattern: Bool { 77 | self.syntax.pattern.isTuplePattern 78 | } 79 | 80 | var name: String? { 81 | self.syntax.pattern.as(IdentifierPatternSyntax.self)?.identifier.text 82 | } 83 | 84 | var isInitializedVarWithoutType: Bool { 85 | self.initializerValue != nil 86 | && self.variable.keywordToken == .keyword(.var) 87 | && self.effectiveType == nil 88 | && self.initializerValue?.inferredTypeSyntax == nil 89 | } 90 | 91 | var isInitializedLet: Bool { 92 | self.initializerValue != nil && self.variable.keywordToken == .keyword(.let) 93 | } 94 | 95 | func diagnostic(_ message: DiagnosticMessage) -> Diagnostic { 96 | Diagnostic(node: self.syntax._syntaxNode, message: message) 97 | } 98 | } 99 | 100 | struct MemberVariable { 101 | let customSettings: VariableCustomSettings? 102 | let syntax: VariableDeclSyntax 103 | 104 | var accessLevel: AccessLevelModifier { 105 | self.syntax.accessLevel 106 | } 107 | 108 | var bindings: PatternBindingListSyntax { 109 | self.syntax.bindings 110 | } 111 | 112 | var keywordToken: TokenKind { 113 | self.syntax.bindingSpecifier.tokenKind 114 | } 115 | } 116 | 117 | struct MemberProperty: Equatable { 118 | let accessLevel: AccessLevelModifier 119 | let customSettings: VariableCustomSettings? 120 | let initializerValue: ExprSyntax? 121 | let keywordToken: TokenKind 122 | let name: String 123 | let type: TypeSyntax 124 | 125 | func initParameterLabel( 126 | considering allProperties: [MemberProperty], 127 | deunderscoreParameters: Bool 128 | ) -> String { 129 | guard 130 | let customSettings = self.customSettings, 131 | customSettings.label 132 | != self.initParameterName( 133 | considering: allProperties, 134 | deunderscoreParameters: deunderscoreParameters 135 | ) 136 | else { return "" } 137 | 138 | return customSettings.label.map { "\($0) " } ?? "" 139 | } 140 | 141 | func initParameterName( 142 | considering allProperties: [MemberProperty], 143 | deunderscoreParameters: Bool 144 | ) -> String { 145 | guard 146 | self.customSettings?.label == nil, 147 | deunderscoreParameters 148 | else { return self.name } 149 | 150 | let potentialName = self.name.hasPrefix("_") ? String(name.dropFirst()) : self.name 151 | return allProperties.contains(where: { $0.name == potentialName }) ? self.name : potentialName 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/Support/String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | var isValidSwiftLabel: Bool { 5 | let pattern = #"^[_a-zA-Z][_a-zA-Z0-9]*$"# 6 | let regex = try! NSRegularExpression(pattern: pattern) 7 | let range = NSRange(self.startIndex.. Bool { 5 | return !self.modifiers.containsAny(of: keywords.map { TokenSyntax.keyword($0) }) 6 | } 7 | 8 | func firstModifierWhere(keyword: Keyword) -> DeclModifierSyntax? { 9 | let keywordText = TokenSyntax.keyword(keyword).text 10 | return self.modifiers.first { modifier in 11 | modifier.name.text == keywordText 12 | } 13 | } 14 | } 15 | 16 | extension DeclModifierListSyntax { 17 | fileprivate func containsAny(of tokens: [TokenSyntax]) -> Bool { 18 | return self.contains { modifier in 19 | tokens.contains { $0.text == modifier.name.text } 20 | } 21 | } 22 | } 23 | 24 | extension PatternBindingSyntax { 25 | var isComputedProperty: Bool { 26 | guard let accessors = self.accessorBlock?.accessors else { return false } 27 | 28 | switch accessors { 29 | case .accessors(let accessors): 30 | let tokenKinds = accessors.compactMap { $0.accessorSpecifier.tokenKind } 31 | let propertyObservers: [TokenKind] = [.keyword(.didSet), .keyword(.willSet)] 32 | 33 | return !tokenKinds.allSatisfy(propertyObservers.contains) 34 | 35 | case .getter(_): 36 | return true 37 | } 38 | } 39 | } 40 | 41 | extension TypeSyntax { 42 | var isFunctionType: Bool { 43 | // NB: Check for `FunctionTypeSyntax` directly or when wrapped within `AttributedTypeSyntax`, 44 | // e.g., `@Sendable () -> Void`. 45 | return self.is(FunctionTypeSyntax.self) 46 | || (self.as(AttributedTypeSyntax.self)?.baseType.is(FunctionTypeSyntax.self) ?? false) 47 | } 48 | } 49 | 50 | extension TypeSyntax { 51 | var isOptionalType: Bool { 52 | self.as(OptionalTypeSyntax.self) != nil 53 | } 54 | } 55 | 56 | extension PatternSyntax { 57 | var isTuplePattern: Bool { 58 | self.as(TuplePatternSyntax.self) != nil 59 | } 60 | } 61 | 62 | extension VariableDeclSyntax { 63 | var isComputedProperty: Bool { 64 | guard 65 | self.bindings.count == 1, 66 | let binding = self.bindings.first?.as(PatternBindingSyntax.self) 67 | else { return false } 68 | 69 | return self.bindingSpecifier.tokenKind == .keyword(.var) && binding.isComputedProperty 70 | } 71 | 72 | var isFullyInitialized: Bool { 73 | self.bindings.allSatisfy { $0.initializer != nil } 74 | } 75 | 76 | var isFullyInitializedLet: Bool { 77 | self.isLet && self.isFullyInitialized 78 | } 79 | 80 | var isLet: Bool { 81 | self.bindingSpecifier.tokenKind == .keyword(.let) 82 | } 83 | 84 | var isVar: Bool { 85 | self.bindingSpecifier.tokenKind == .keyword(.var) 86 | } 87 | } 88 | 89 | extension ExprSyntax { 90 | var trimmedStringLiteral: String? { 91 | self.as(StringLiteralExprSyntax.self)? 92 | .segments 93 | .trimmedDescription 94 | .trimmingCharacters(in: .whitespacesAndNewlines) 95 | } 96 | } 97 | 98 | extension DeclGroupSyntax { 99 | func descriptiveDeclKind(withArticle article: Bool = false) -> String { 100 | switch self { 101 | case is ActorDeclSyntax: 102 | return article ? "an actor" : "actor" 103 | case is ClassDeclSyntax: 104 | return article ? "a class" : "class" 105 | case is ExtensionDeclSyntax: 106 | return article ? "an extension" : "extension" 107 | case is ProtocolDeclSyntax: 108 | return article ? "a protocol" : "protocol" 109 | case is StructDeclSyntax: 110 | return article ? "a struct" : "struct" 111 | case is EnumDeclSyntax: 112 | return article ? "an enum" : "enum" 113 | default: 114 | return "`\(self.kind)`" 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftDiagnostics 3 | import SwiftSyntax 4 | import SwiftSyntaxBuilder 5 | import SwiftSyntaxMacroExpansion 6 | import SwiftSyntaxMacros 7 | 8 | public struct UncheckedMemberwiseInitMacro: MemberMacro { 9 | public static func expansion( 10 | of node: AttributeSyntax, 11 | providingMembersOf decl: D, 12 | in context: C 13 | ) throws -> [SwiftSyntax.DeclSyntax] 14 | where D: DeclGroupSyntax, C: MacroExpansionContext { 15 | guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(decl.kind) else { 16 | throw MacroExpansionErrorMessage( 17 | """ 18 | @_UncheckedMemberwiseInit can only be attached to a struct, class, or actor; \ 19 | not to \(decl.descriptiveDeclKind(withArticle: true)). 20 | """ 21 | ) 22 | } 23 | 24 | let accessLevel = 25 | MemberwiseInitMacro.extractConfiguredAccessLevel(from: node) ?? .internal 26 | let optionalsDefaultNil: Bool? = 27 | MemberwiseInitMacro.extractLabeledBoolArgument("_optionalsDefaultNil", from: node) 28 | let deunderscoreParameters: Bool = 29 | MemberwiseInitMacro.extractLabeledBoolArgument("_deunderscoreParameters", from: node) ?? false 30 | 31 | let properties = try collectUncheckedMemberProperties( 32 | from: decl.memberBlock.members 33 | ) 34 | 35 | return [ 36 | DeclSyntax( 37 | MemberwiseInitFormatter.formatInitializer( 38 | properties: properties, 39 | accessLevel: accessLevel, 40 | deunderscoreParameters: deunderscoreParameters, 41 | optionalsDefaultNil: optionalsDefaultNil 42 | ) 43 | ) 44 | ] 45 | } 46 | 47 | private static func collectUncheckedMemberProperties( 48 | from memberBlockItemList: MemberBlockItemListSyntax 49 | ) throws -> [MemberProperty] { 50 | memberBlockItemList.compactMap { member -> MemberProperty? in 51 | guard let variable = member.decl.as(VariableDeclSyntax.self), 52 | !variable.isComputedProperty, 53 | variable.modifiersExclude([.static, .lazy]), 54 | let binding = variable.bindings.first, 55 | let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text, 56 | let type = binding.typeAnnotation?.type ?? binding.initializer?.value.inferredTypeSyntax 57 | else { return nil } 58 | 59 | let customSettings = MemberwiseInitMacro.extractVariableCustomSettings(from: variable) 60 | if customSettings?.ignore == true { 61 | return nil 62 | } 63 | 64 | return MemberProperty( 65 | accessLevel: variable.accessLevel, 66 | customSettings: customSettings, 67 | initializerValue: binding.initializer?.value, 68 | keywordToken: variable.bindingSpecifier.tokenKind, 69 | name: name, 70 | type: type.trimmed 71 | ) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/MacroTestingTests/AddAsyncTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import XCTest 3 | 4 | final class AddAsyncMacroTests: BaseTestCase { 5 | override func invokeTest() { 6 | withMacroTesting(macros: [AddAsyncMacro.self]) { 7 | super.invokeTest() 8 | } 9 | } 10 | 11 | func testExpansionTransformsFunctionWithResultCompletionToAsyncThrows() { 12 | #if canImport(SwiftSyntax600) 13 | assertMacro { 14 | #""" 15 | @AddAsync 16 | func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { 17 | completionBlock(.success("a: \(a), b: \(b), value: \(value)")) 18 | } 19 | """# 20 | } expansion: { 21 | #""" 22 | func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { 23 | completionBlock(.success("a: \(a), b: \(b), value: \(value)")) 24 | } 25 | 26 | func c(a: Int, for b: String, _ value: Double) async throws -> String { 27 | try await withCheckedThrowingContinuation { continuation in 28 | c(a: a, for: b, value) { returnValue in 29 | 30 | switch returnValue { 31 | case .success(let value): 32 | continuation.resume(returning: value) 33 | case .failure(let error): 34 | continuation.resume(throwing: error) 35 | } 36 | } 37 | } 38 | 39 | } 40 | """# 41 | } 42 | #else 43 | assertMacro { 44 | #""" 45 | @AddAsync 46 | func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { 47 | completionBlock(.success("a: \(a), b: \(b), value: \(value)")) 48 | } 49 | """# 50 | } expansion: { 51 | #""" 52 | func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result) -> Void) -> Void { 53 | completionBlock(.success("a: \(a), b: \(b), value: \(value)")) 54 | } 55 | 56 | func c(a: Int, for b: String, _ value: Double) async throws -> String { 57 | try await withCheckedThrowingContinuation { continuation in 58 | c(a: a, for: b, value) { returnValue in 59 | 60 | switch returnValue { 61 | case .success(let value): 62 | continuation.resume(returning: value) 63 | case .failure(let error): 64 | continuation.resume(throwing: error) 65 | } 66 | } 67 | } 68 | } 69 | """# 70 | } 71 | #endif 72 | } 73 | 74 | func testExpansionTransformsFunctionWithBoolCompletionToAsync() { 75 | #if canImport(SwiftSyntax600) 76 | assertMacro { 77 | """ 78 | @AddAsync 79 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { 80 | completionBlock(true) 81 | } 82 | """ 83 | } expansion: { 84 | """ 85 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { 86 | completionBlock(true) 87 | } 88 | 89 | func d(a: Int, for b: String, _ value: Double) async -> Bool { 90 | await withCheckedContinuation { continuation in 91 | d(a: a, for: b, value) { returnValue in 92 | 93 | continuation.resume(returning: returnValue) 94 | } 95 | } 96 | 97 | } 98 | """ 99 | } 100 | #else 101 | assertMacro { 102 | """ 103 | @AddAsync 104 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { 105 | completionBlock(true) 106 | } 107 | """ 108 | } expansion: { 109 | """ 110 | func d(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Bool) -> Void) -> Void { 111 | completionBlock(true) 112 | } 113 | 114 | func d(a: Int, for b: String, _ value: Double) async -> Bool { 115 | await withCheckedContinuation { continuation in 116 | d(a: a, for: b, value) { returnValue in 117 | 118 | continuation.resume(returning: returnValue) 119 | } 120 | } 121 | } 122 | """ 123 | } 124 | #endif 125 | } 126 | 127 | func testExpansionOnStoredPropertyEmitsError() { 128 | assertMacro { 129 | """ 130 | struct Test { 131 | @AddAsync 132 | var name: String 133 | } 134 | """ 135 | } diagnostics: { 136 | """ 137 | struct Test { 138 | @AddAsync 139 | ┬──────── 140 | ╰─ 🛑 @addAsync only works on functions 141 | var name: String 142 | } 143 | """ 144 | } 145 | } 146 | 147 | func testExpansionOnAsyncFunctionEmitsError() { 148 | assertMacro { 149 | """ 150 | struct Test { 151 | @AddAsync 152 | async func sayHello() { 153 | } 154 | } 155 | """ 156 | } diagnostics: { 157 | """ 158 | struct Test { 159 | @AddAsync 160 | ┬──────── 161 | ╰─ 🛑 @addAsync requires an function that returns void 162 | async func sayHello() { 163 | } 164 | } 165 | """ 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /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 | } expansion: { 17 | """ 18 | x * y - z 19 | """ 20 | } diagnostics: { 21 | """ 22 | #addBlocker(x * y + z) 23 | ───── ┬ ─ 24 | ╰─ ⚠️ blocked an add; did you mean to subtract? 25 | ✏️ use '-' 26 | """ 27 | } fixes: { 28 | """ 29 | #addBlocker(x * y + z) 30 | ───── ┬ ─ 31 | ╰─ ⚠️ blocked an add; did you mean to subtract? 32 | 33 | ✏️ use '-' 34 | #addBlocker(x * y - z) 35 | """ 36 | } 37 | } 38 | 39 | func testExpansionPreservesSubtraction() { 40 | assertMacro { 41 | """ 42 | #addBlocker(x * y - z) 43 | """ 44 | } expansion: { 45 | """ 46 | x * y - z 47 | """ 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /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 | #if canImport(SwiftSyntax600) 13 | assertMacro { 14 | """ 15 | @AddCompletionHandler 16 | func f(a: Int, for b: String, _ value: Double) async -> String { 17 | return b 18 | } 19 | """ 20 | } expansion: { 21 | """ 22 | func f(a: Int, for b: String, _ value: Double) async -> String { 23 | return b 24 | } 25 | 26 | func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { 27 | Task { 28 | completionHandler(await f(a: a, for: b, value)) 29 | } 30 | 31 | } 32 | """ 33 | } 34 | #else 35 | assertMacro { 36 | """ 37 | @AddCompletionHandler 38 | func f(a: Int, for b: String, _ value: Double) async -> String { 39 | return b 40 | } 41 | """ 42 | } expansion: { 43 | """ 44 | func f(a: Int, for b: String, _ value: Double) async -> String { 45 | return b 46 | } 47 | 48 | func f(a: Int, for b: String, _ value: Double, completionHandler: @escaping (String) -> Void) { 49 | Task { 50 | completionHandler(await f(a: a, for: b, value)) 51 | } 52 | } 53 | """ 54 | } 55 | #endif 56 | } 57 | 58 | func testExpansionOnStoredPropertyEmitsError() { 59 | assertMacro { 60 | """ 61 | struct Test { 62 | @AddCompletionHandler 63 | var value: Int 64 | } 65 | """ 66 | } diagnostics: { 67 | """ 68 | struct Test { 69 | @AddCompletionHandler 70 | ┬──────────────────── 71 | ╰─ 🛑 @addCompletionHandler only works on functions 72 | var value: Int 73 | } 74 | """ 75 | } 76 | } 77 | 78 | func testExpansionOnNonAsyncFunctionEmitsErrorWithFixItSuggestion() { 79 | assertMacro { 80 | """ 81 | struct Test { 82 | @AddCompletionHandler 83 | func fetchData() -> String { 84 | return "Hello, World!" 85 | } 86 | } 87 | """ 88 | } diagnostics: { 89 | """ 90 | struct Test { 91 | @AddCompletionHandler 92 | func fetchData() -> String { 93 | ┬─── 94 | ╰─ 🛑 can only add a completion-handler variant to an 'async' function 95 | ✏️ add 'async' 96 | return "Hello, World!" 97 | } 98 | } 99 | """ 100 | } fixes: { 101 | """ 102 | func fetchData() -> String { 103 | ┬─── 104 | ╰─ 🛑 can only add a completion-handler variant to an 'async' function 105 | 106 | ✏️ add 'async' 107 | struct Test { 108 | @AddCompletionHandler 109 | func fetchData() async-> String { 110 | return "Hello, World!" 111 | } 112 | } 113 | """ 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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/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 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /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 | } fixes: { 34 | """ 35 | @DiagnosticsAndFixitsEmitter 36 | ┬────────────────────────── 37 | ╰─ ⚠️ This is the first diagnostic. 38 | 39 | ✏️ This is the first fix-it. 40 | @DiagnosticsAndFixitsEmitter 41 | struct FooBar { 42 | let foo: Foo 43 | let bar: Bar 44 | } 45 | 46 | ✏️ This is the second fix-it. 47 | @DiagnosticsAndFixitsEmitter 48 | struct FooBar { 49 | let foo: Foo 50 | let bar: Bar 51 | } 52 | """ 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /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 | #if canImport(SwiftSyntax510) 18 | assertMacro { 19 | """ 20 | @DictionaryStorage 21 | struct Point { 22 | var x: Int = 1 23 | var y: Int = 2 24 | } 25 | """ 26 | } expansion: { 27 | """ 28 | struct Point { 29 | var x: Int { 30 | get { 31 | _storage["x", default: 1] as! Int 32 | } 33 | set { 34 | _storage["x"] = newValue 35 | } 36 | } 37 | var y: Int { 38 | get { 39 | _storage["y", default: 2] as! Int 40 | } 41 | set { 42 | _storage["y"] = newValue 43 | } 44 | } 45 | 46 | var _storage: [String: Any] = [:] 47 | } 48 | """ 49 | } 50 | #elseif canImport(SwiftSyntax509) 51 | assertMacro { 52 | """ 53 | @DictionaryStorage 54 | struct Point { 55 | var x: Int = 1 56 | var y: Int = 2 57 | } 58 | """ 59 | } expansion: { 60 | """ 61 | struct Point { 62 | var x: Int = 1 { 63 | get { 64 | _storage["x", default: 1] as! Int 65 | } 66 | set { 67 | _storage["x"] = newValue 68 | } 69 | } 70 | var y: Int = 2 { 71 | get { 72 | _storage["y", default: 2] as! Int 73 | } 74 | set { 75 | _storage["y"] = newValue 76 | } 77 | } 78 | 79 | var _storage: [String: Any] = [:] 80 | } 81 | """ 82 | } 83 | #endif 84 | } 85 | 86 | func testExpansionWithoutInitializersEmitsError() { 87 | assertMacro { 88 | """ 89 | @DictionaryStorage 90 | class Point { 91 | let x: Int 92 | let y: Int 93 | } 94 | """ 95 | } expansion: { 96 | """ 97 | class Point { 98 | let x: Int 99 | let y: Int 100 | 101 | var _storage: [String: Any] = [:] 102 | } 103 | """ 104 | } diagnostics: { 105 | """ 106 | @DictionaryStorage 107 | class Point { 108 | let x: Int 109 | ╰─ 🛑 stored property must have an initializer 110 | let y: Int 111 | ╰─ 🛑 stored property must have an initializer 112 | } 113 | """ 114 | } 115 | } 116 | 117 | func testExpansionIgnoresComputedProperties() { 118 | assertMacro { 119 | """ 120 | @DictionaryStorage 121 | struct Test { 122 | var value: Int { 123 | get { return 0 } 124 | set {} 125 | } 126 | } 127 | """ 128 | } expansion: { 129 | """ 130 | struct Test { 131 | var value: Int { 132 | get { return 0 } 133 | set {} 134 | } 135 | 136 | var _storage: [String: Any] = [:] 137 | } 138 | """ 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Tests/MacroTestingTests/EntryMacroTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftSyntax600) 2 | import MacroTesting 3 | import XCTest 4 | 5 | final class EntryMacroTests: BaseTestCase { 6 | override func invokeTest() { 7 | withMacroTesting( 8 | macros: [ 9 | EntryMacro.self 10 | ] 11 | ) { 12 | super.invokeTest() 13 | } 14 | } 15 | 16 | func testWithinEnvironmentValues() { 17 | assertMacro { 18 | """ 19 | extension EnvironmentValues { 20 | @Entry var x: String = "" 21 | } 22 | """ 23 | } expansion: { 24 | """ 25 | extension EnvironmentValues { 26 | var x: String { 27 | get { 28 | fatalError() 29 | } 30 | } 31 | } 32 | """ 33 | } 34 | } 35 | 36 | func testNotWithinEnvironmentValues() { 37 | assertMacro { 38 | """ 39 | extension String { 40 | @Entry var x: String = "" 41 | } 42 | """ 43 | } diagnostics: { 44 | """ 45 | extension String { 46 | @Entry var x: String = "" 47 | ┬───── 48 | ╰─ 🛑 '@Entry' macro can only attach to var declarations inside extensions of EnvironmentValues 49 | } 50 | """ 51 | } 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /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/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 | ┬───────────────── 81 | ╰─ ⚠️ First member needs to be replaced 82 | 83 | ✏️ Replace the first member 84 | @ReplaceFirstMember 85 | struct FooBar { 86 | let oye: Oye 87 | let bar: Bar 88 | let baz: Baz 89 | } 90 | """ 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | else { 58 | throw CustomError.message( 59 | "@addAsync requires an function that has a completion handler as last parameter" 60 | ) 61 | } 62 | 63 | // Completion handler needs to return Void 64 | if completionHandlerParameter.returnClause.type.as(IdentifierTypeSyntax.self)?.name.text 65 | != "Void" 66 | { 67 | throw CustomError.message( 68 | "@addAsync requires an function that has a completion handler that returns Void" 69 | ) 70 | } 71 | 72 | let returnType = completionHandlerParameter.parameters.first?.type 73 | 74 | let isResultReturn = returnType?.children(viewMode: .all).first?.description == "Result" 75 | let successReturnType = 76 | isResultReturn 77 | ? returnType!.as(IdentifierTypeSyntax.self)!.genericArgumentClause?.arguments.first!.argument 78 | : returnType 79 | 80 | // Remove completionHandler and comma from the previous parameter 81 | var newParameterList = funcDecl.signature.parameterClause.parameters 82 | newParameterList.removeLast() 83 | var newParameterListLastParameter = newParameterList.last! 84 | newParameterList.removeLast() 85 | newParameterListLastParameter.trailingTrivia = [] 86 | newParameterListLastParameter.trailingComma = nil 87 | newParameterList.append(newParameterListLastParameter) 88 | 89 | // Drop the @addAsync attribute from the new declaration. 90 | let newAttributeList = funcDecl.attributes.filter { 91 | guard case let .attribute(attribute) = $0, 92 | let attributeType = attribute.attributeName.as(IdentifierTypeSyntax.self), 93 | let nodeType = node.attributeName.as(IdentifierTypeSyntax.self) 94 | else { 95 | return true 96 | } 97 | 98 | return attributeType.name.text != nodeType.name.text 99 | } 100 | 101 | let callArguments: [String] = newParameterList.map { param in 102 | let argName = param.secondName ?? param.firstName 103 | 104 | let paramName = param.firstName 105 | if paramName.text != "_" { 106 | return "\(paramName.text): \(argName.text)" 107 | } 108 | 109 | return "\(argName.text)" 110 | } 111 | 112 | let switchBody: ExprSyntax = 113 | """ 114 | switch returnValue { 115 | case .success(let value): 116 | continuation.resume(returning: value) 117 | case .failure(let error): 118 | continuation.resume(throwing: error) 119 | } 120 | """ 121 | 122 | let newBody: ExprSyntax = 123 | """ 124 | 125 | \(raw: isResultReturn ? "try await withCheckedThrowingContinuation { continuation in" : "await withCheckedContinuation { continuation in") 126 | \(raw: funcDecl.name)(\(raw: callArguments.joined(separator: ", "))) { \(raw: returnType != nil ? "returnValue in" : "") 127 | 128 | \(raw: isResultReturn ? switchBody : "continuation.resume(returning: \(raw: returnType != nil ? "returnValue" : "()"))") 129 | } 130 | } 131 | 132 | """ 133 | 134 | // add async 135 | funcDecl.signature.effectSpecifiers = FunctionEffectSpecifiersSyntax( 136 | leadingTrivia: .space, 137 | asyncSpecifier: .keyword(.async), 138 | throwsSpecifier: isResultReturn ? .keyword(.throws) : nil 139 | ) 140 | 141 | // add result type 142 | if let successReturnType { 143 | funcDecl.signature.returnClause = ReturnClauseSyntax( 144 | leadingTrivia: .space, type: successReturnType.with(\.leadingTrivia, .space)) 145 | } else { 146 | funcDecl.signature.returnClause = nil 147 | } 148 | 149 | // drop completion handler 150 | funcDecl.signature.parameterClause.parameters = newParameterList 151 | funcDecl.signature.parameterClause.trailingTrivia = [] 152 | 153 | funcDecl.body = CodeBlockSyntax( 154 | leftBrace: .leftBraceToken(leadingTrivia: .space), 155 | statements: CodeBlockItemListSyntax( 156 | [CodeBlockItemSyntax(item: .expr(newBody))] 157 | ), 158 | rightBrace: .rightBraceToken(leadingTrivia: .newline) 159 | ) 160 | 161 | funcDecl.attributes = newAttributeList 162 | 163 | funcDecl.leadingTrivia = .newlines(2) 164 | 165 | return [DeclSyntax(funcDecl)] 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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 | -------------------------------------------------------------------------------- /Tests/MacroTestingTests/MacroExamples/EntryMacro.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftSyntax600) 2 | import SwiftSyntax 3 | import SwiftSyntaxMacros 4 | 5 | // Not complete, just enough to unit test lexical context. 6 | public struct EntryMacro: AccessorMacro { 7 | public static func expansion( 8 | of node: AttributeSyntax, 9 | providingAccessorsOf declaration: some DeclSyntaxProtocol, 10 | in context: some MacroExpansionContext 11 | ) throws -> [AccessorDeclSyntax] { 12 | let isInEnvironmentValues = context.lexicalContext.contains { lexicalContext in 13 | lexicalContext.as(ExtensionDeclSyntax.self)?.extendedType.trimmedDescription 14 | == "EnvironmentValues" 15 | } 16 | 17 | guard isInEnvironmentValues else { 18 | throw MacroExpansionErrorMessage( 19 | "'@Entry' macro can only attach to var declarations inside extensions of EnvironmentValues" 20 | ) 21 | } 22 | 23 | return [ 24 | AccessorDeclSyntax(accessorSpecifier: .keyword(.get)) { 25 | "fatalError()" 26 | } 27 | ] 28 | } 29 | } 30 | #endif 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Tests/MacroTestingTests/MacroExamples/FontLiteralMacro.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 | /// Implementation of the `#fontLiteral` macro, which is similar in spirit 17 | /// to the built-in expressions `#colorLiteral`, `#imageLiteral`, etc., but in 18 | /// a small macro. 19 | public enum FontLiteralMacro: ExpressionMacro { 20 | public static func expansion( 21 | of node: some FreestandingMacroExpansionSyntax, 22 | in context: some MacroExpansionContext 23 | ) throws -> 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/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/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/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/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/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 | -------------------------------------------------------------------------------- /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 | return nil 98 | } 99 | 100 | optionsEnumName = optionsEnumNameString.content.text 101 | } else { 102 | optionsEnumName = defaultOptionsEnumName 103 | } 104 | 105 | // Only apply to structs. 106 | guard let structDecl = decl.as(StructDeclSyntax.self) else { 107 | context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl)) 108 | return nil 109 | } 110 | 111 | // Find the option enum within the struct. 112 | guard 113 | let optionsEnum = decl.memberBlock.members.compactMap({ member in 114 | if let enumDecl = member.decl.as(EnumDeclSyntax.self), 115 | enumDecl.name.text == optionsEnumName 116 | { 117 | return enumDecl 118 | } 119 | 120 | return nil 121 | }).first 122 | else { 123 | context.diagnose( 124 | OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) 125 | return nil 126 | } 127 | 128 | // Retrieve the raw type from the attribute. 129 | guard 130 | let genericArgs = attribute.attributeName.as(IdentifierTypeSyntax.self)? 131 | .genericArgumentClause, 132 | let rawType = genericArgs.arguments.first?.argument 133 | else { 134 | context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) 135 | return nil 136 | } 137 | 138 | return (structDecl, optionsEnum, rawType) 139 | } 140 | } 141 | 142 | extension OptionSetMacro: ExtensionMacro { 143 | public static func expansion( 144 | of node: AttributeSyntax, 145 | attachedTo declaration: some DeclGroupSyntax, 146 | providingExtensionsOf type: some TypeSyntaxProtocol, 147 | conformingTo protocols: [TypeSyntax], 148 | in context: some MacroExpansionContext 149 | ) throws -> [ExtensionDeclSyntax] { 150 | // Decode the expansion arguments. 151 | guard let (structDecl, _, _) = decodeExpansion(of: node, attachedTo: declaration, in: context) 152 | else { 153 | return [] 154 | } 155 | 156 | // If there is an explicit conformance to OptionSet already, don't add one. 157 | if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypes, 158 | inheritedTypes.contains(where: { inherited in inherited.type.trimmedDescription == "OptionSet" 159 | }) 160 | { 161 | return [] 162 | } 163 | 164 | return [try ExtensionDeclSyntax("extension \(type): OptionSet {}")] 165 | } 166 | } 167 | 168 | extension OptionSetMacro: MemberMacro { 169 | public static func expansion( 170 | of attribute: AttributeSyntax, 171 | providingMembersOf decl: some DeclGroupSyntax, 172 | in context: some MacroExpansionContext 173 | ) throws -> [DeclSyntax] { 174 | // Decode the expansion arguments. 175 | guard 176 | let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) 177 | else { 178 | return [] 179 | } 180 | 181 | // Find all of the case elements. 182 | let caseElements = optionsEnum.memberBlock.members.flatMap { member in 183 | guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { 184 | return [EnumCaseElementSyntax]() 185 | } 186 | 187 | return Array(caseDecl.elements) 188 | } 189 | 190 | // Dig out the access control keyword we need. 191 | let access = decl.modifiers.first(where: \.isNeededAccessLevelModifier) 192 | 193 | let staticVars = caseElements.map { (element) -> DeclSyntax in 194 | """ 195 | \(access) static let \(element.name): Self = 196 | Self(rawValue: 1 << \(optionsEnum.name).\(element.name).rawValue) 197 | """ 198 | } 199 | 200 | return [ 201 | "\(access)typealias RawValue = \(rawType)", 202 | "\(access)var rawValue: RawValue", 203 | "\(access)init() { self.rawValue = 0 }", 204 | "\(access)init(rawValue: RawValue) { self.rawValue = rawValue }", 205 | ] + staticVars 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /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/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/SwiftSyntaxCompatibilityExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | 4 | #if !canImport(SwiftSyntax510) && canImport(SwiftSyntax509) 5 | extension FreestandingMacroExpansionSyntax { 6 | var arguments: LabeledExprListSyntax { 7 | get { self.argumentList } 8 | set { self.argumentList = newValue } 9 | } 10 | } 11 | #endif 12 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | #if !canImport(SwiftSyntax510) 18 | assertMacro { 19 | """ 20 | @Observable 21 | final class Dog { 22 | var name: String? 23 | var treat: Treat? 24 | 25 | var isHappy: Bool = true 26 | 27 | init() {} 28 | 29 | func bark() { 30 | print("bork bork") 31 | } 32 | } 33 | """ 34 | } expansion: { 35 | #""" 36 | final class Dog { 37 | var name: String? { 38 | get { 39 | _registrar.beginAccess(\.name) 40 | defer { 41 | _registrar.endAccess() 42 | } 43 | return _storage.name 44 | } 45 | set { 46 | _registrar.beginAccess(\.name) 47 | _registrar.register(observable: self, willSet: \.name, to: newValue) 48 | defer { 49 | _registrar.register(observable: self, didSet: \.name) 50 | _registrar.endAccess() 51 | } 52 | _storage.name = newValue 53 | } 54 | } 55 | var treat: Treat? { 56 | get { 57 | _registrar.beginAccess(\.treat) 58 | defer { 59 | _registrar.endAccess() 60 | } 61 | return _storage.treat 62 | } 63 | set { 64 | _registrar.beginAccess(\.treat) 65 | _registrar.register(observable: self, willSet: \.treat, to: newValue) 66 | defer { 67 | _registrar.register(observable: self, didSet: \.treat) 68 | _registrar.endAccess() 69 | } 70 | _storage.treat = newValue 71 | } 72 | } 73 | 74 | var isHappy: Bool = true { 75 | get { 76 | _registrar.beginAccess(\.isHappy) 77 | defer { 78 | _registrar.endAccess() 79 | } 80 | return _storage.isHappy 81 | } 82 | set { 83 | _registrar.beginAccess(\.isHappy) 84 | _registrar.register(observable: self, willSet: \.isHappy, to: newValue) 85 | defer { 86 | _registrar.register(observable: self, didSet: \.isHappy) 87 | _registrar.endAccess() 88 | } 89 | _storage.isHappy = newValue 90 | } 91 | } 92 | 93 | init() {} 94 | 95 | func bark() { 96 | print("bork bork") 97 | } 98 | 99 | let _registrar = ObservationRegistrar() 100 | 101 | public nonisolated func addObserver(_ observer: some Observer) { 102 | _registrar.addObserver(observer) 103 | } 104 | 105 | public nonisolated func removeObserver(_ observer: some Observer) { 106 | _registrar.removeObserver(observer) 107 | } 108 | 109 | private func withTransaction(_ apply: () throws -> T) rethrows -> T { 110 | _registrar.beginAccess() 111 | defer { 112 | _registrar.endAccess() 113 | } 114 | return try apply() 115 | } 116 | 117 | private struct Storage { 118 | 119 | var name: String? 120 | var treat: Treat? 121 | 122 | var isHappy: Bool = true 123 | } 124 | 125 | private var _storage = Storage() 126 | } 127 | 128 | extension Dog: Observable { 129 | } 130 | """# 131 | } 132 | #else 133 | assertMacro { 134 | """ 135 | @Observable 136 | final class Dog { 137 | var name: String? 138 | var treat: Treat? 139 | 140 | var isHappy: Bool = true 141 | 142 | init() {} 143 | 144 | func bark() { 145 | print("bork bork") 146 | } 147 | } 148 | """ 149 | } expansion: { 150 | #""" 151 | final class Dog { 152 | var name: String? { 153 | get { 154 | _registrar.beginAccess(\.name) 155 | defer { 156 | _registrar.endAccess() 157 | } 158 | return _storage.name 159 | } 160 | set { 161 | _registrar.beginAccess(\.name) 162 | _registrar.register(observable: self, willSet: \.name, to: newValue) 163 | defer { 164 | _registrar.register(observable: self, didSet: \.name) 165 | _registrar.endAccess() 166 | } 167 | _storage.name = newValue 168 | } 169 | } 170 | var treat: Treat? { 171 | get { 172 | _registrar.beginAccess(\.treat) 173 | defer { 174 | _registrar.endAccess() 175 | } 176 | return _storage.treat 177 | } 178 | set { 179 | _registrar.beginAccess(\.treat) 180 | _registrar.register(observable: self, willSet: \.treat, to: newValue) 181 | defer { 182 | _registrar.register(observable: self, didSet: \.treat) 183 | _registrar.endAccess() 184 | } 185 | _storage.treat = newValue 186 | } 187 | } 188 | 189 | var isHappy: Bool { 190 | get { 191 | _registrar.beginAccess(\.isHappy) 192 | defer { 193 | _registrar.endAccess() 194 | } 195 | return _storage.isHappy 196 | } 197 | set { 198 | _registrar.beginAccess(\.isHappy) 199 | _registrar.register(observable: self, willSet: \.isHappy, to: newValue) 200 | defer { 201 | _registrar.register(observable: self, didSet: \.isHappy) 202 | _registrar.endAccess() 203 | } 204 | _storage.isHappy = newValue 205 | } 206 | } 207 | 208 | init() {} 209 | 210 | func bark() { 211 | print("bork bork") 212 | } 213 | 214 | let _registrar = ObservationRegistrar() 215 | 216 | public nonisolated func addObserver(_ observer: some Observer) { 217 | _registrar.addObserver(observer) 218 | } 219 | 220 | public nonisolated func removeObserver(_ observer: some Observer) { 221 | _registrar.removeObserver(observer) 222 | } 223 | 224 | private func withTransaction(_ apply: () throws -> T) rethrows -> T { 225 | _registrar.beginAccess() 226 | defer { 227 | _registrar.endAccess() 228 | } 229 | return try apply() 230 | } 231 | 232 | private struct Storage { 233 | 234 | var name: String? 235 | var treat: Treat? 236 | 237 | var isHappy: Bool = true 238 | } 239 | 240 | private var _storage = Storage() 241 | } 242 | 243 | extension Dog: Observable { 244 | } 245 | """# 246 | } 247 | #endif 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /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 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Tests/MacroTestingTests/SwiftTestingTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(Testing) 2 | @_spi(Experimental) import MacroTesting 3 | import Testing 4 | 5 | @Suite( 6 | .macros( 7 | //record: .failed, 8 | macros: ["URL": URLMacro.self] 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/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 | -------------------------------------------------------------------------------- /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 | } expansion: { 17 | """ 18 | () 19 | """ 20 | } diagnostics: { 21 | """ 22 | #myWarning("This is a warning") 23 | ┬────────────────────────────── 24 | ╰─ ⚠️ This is a warning 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/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 | } 59 | } 60 | 61 | func testExpansionWithInvalidAttributeEmitsError() { 62 | assertMacro { 63 | """ 64 | @wrapStoredProperties(12) 65 | struct Test { 66 | var value: Int 67 | } 68 | """ 69 | } diagnostics: { 70 | """ 71 | @wrapStoredProperties(12) 72 | ┬──────────────────────── 73 | ╰─ 🛑 macro requires a string literal containing the name of an attribute 74 | struct Test { 75 | var value: Int 76 | } 77 | """ 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/MemberwiseInitTests/CustomInitRawTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import MemberwiseInitMacros 3 | import SwiftSyntaxMacros 4 | import XCTest 5 | 6 | final class CustomInitRawTests: XCTestCase { 7 | override func invokeTest() { 8 | withMacroTesting( 9 | indentationWidth: .spaces(2), 10 | macros: [ 11 | "MemberwiseInit": MemberwiseInitMacro.self, 12 | "InitRaw": InitMacro.self, 13 | ] 14 | ) { 15 | super.invokeTest() 16 | } 17 | } 18 | 19 | func testInitRaw() { 20 | assertMacro { 21 | """ 22 | @MemberwiseInit 23 | struct S { 24 | @InitRaw var v: T 25 | } 26 | """ 27 | } expansion: { 28 | """ 29 | struct S { 30 | var v: T 31 | 32 | internal init( 33 | v: T 34 | ) { 35 | self.v = v 36 | } 37 | } 38 | """ 39 | } 40 | } 41 | 42 | func testDefault() { 43 | assertMacro { 44 | """ 45 | @MemberwiseInit 46 | public struct S { 47 | @InitRaw(default: 0) let number: T 48 | } 49 | """ 50 | } expansion: { 51 | """ 52 | public struct S { 53 | let number: T 54 | 55 | internal init( 56 | number: T = 0 57 | ) { 58 | self.number = number 59 | } 60 | } 61 | """ 62 | } 63 | } 64 | 65 | func testDefaultOnInitializedLet() { 66 | assertMacro { 67 | """ 68 | @MemberwiseInit 69 | struct S { 70 | @InitRaw(default: 42) let number = 0 71 | } 72 | """ 73 | } expansion: { 74 | """ 75 | struct S { 76 | let number = 0 77 | 78 | internal init() { 79 | } 80 | } 81 | """ 82 | } diagnostics: { 83 | """ 84 | @MemberwiseInit 85 | struct S { 86 | @InitRaw(default: 42) let number = 0 87 | ┬────────── 88 | ╰─ 🛑 @InitRaw can't be applied to already initialized constant 89 | ✏️ Remove '@InitRaw(default: 42)' 90 | ✏️ Remove '= 0' 91 | } 92 | """ 93 | } fixes: { 94 | """ 95 | @InitRaw(default: 42) let number = 0 96 | ┬────────── 97 | ╰─ 🛑 @InitRaw can't be applied to already initialized constant 98 | 99 | ✏️ Remove '@InitRaw(default: 42)' 100 | @MemberwiseInit 101 | struct S { 102 | let number = 0 103 | } 104 | 105 | ✏️ Remove '= 0' 106 | @MemberwiseInit 107 | struct S { 108 | @InitRaw(default: 42) let number: Int 109 | } 110 | """ 111 | } 112 | } 113 | 114 | func testType() { 115 | assertMacro { 116 | """ 117 | @MemberwiseInit 118 | struct S { 119 | @InitRaw(type: Q.self) var v: T 120 | } 121 | """ 122 | } expansion: { 123 | """ 124 | struct S { 125 | var v: T 126 | 127 | internal init( 128 | v: Q 129 | ) { 130 | self.v = v 131 | } 132 | } 133 | """ 134 | } 135 | } 136 | 137 | func testTypeAsGenericExpression() { 138 | assertMacro { 139 | """ 140 | @MemberwiseInit 141 | struct S { 142 | @InitRaw(type: Q.self) var v: T 143 | } 144 | """ 145 | } expansion: { 146 | """ 147 | struct S { 148 | var v: T 149 | 150 | internal init( 151 | v: Q 152 | ) { 153 | self.v = v 154 | } 155 | } 156 | """ 157 | } 158 | } 159 | 160 | func testEverything() { 161 | assertMacro { 162 | """ 163 | @MemberwiseInit(.public) 164 | public struct S { 165 | @InitRaw(.public, assignee: "self.foo", default: nil, escaping: true, label: "_", type: Q?.self) 166 | var initRaw: T 167 | } 168 | """ 169 | } expansion: { 170 | """ 171 | public struct S { 172 | var initRaw: T 173 | 174 | public init( 175 | _ initRaw: @escaping Q? = nil 176 | ) { 177 | self.foo = initRaw 178 | } 179 | } 180 | """ 181 | } 182 | } 183 | 184 | // NB: In Swift 5.9, you could use `type: Q` without `.self` 185 | // In Swift 6, you must use `type: Q.self` when referencing types as values 186 | // 187 | // @MemberwiseInit doesn't produce warnings/fix-its for the Swift 5.9 syntax because: 188 | // 1. On Swift 6, the compiler already produces errors with fix-its 189 | // 2. Adding our own diagnostics would create redundant, noisy warnings alongside compiler errors 190 | // 3. Both syntax forms produce the correct output with proper parameter types 191 | func testTypeReferenceCompatibility() { 192 | assertMacro { 193 | """ 194 | @MemberwiseInit 195 | struct S { 196 | @Init(type: Q) var v: T 197 | } 198 | """ 199 | } expansion: { 200 | """ 201 | struct S { 202 | @Init(type: Q) var v: T 203 | 204 | internal init( 205 | v: Q 206 | ) { 207 | self.v = v 208 | } 209 | } 210 | """ 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Tests/MemberwiseInitTests/CustomInitTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import MemberwiseInitMacros 3 | import XCTest 4 | 5 | final class CustomInitTests: XCTestCase { 6 | override func invokeTest() { 7 | // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 8 | withMacroTesting( 9 | //indentationWidth: .spaces(2), 10 | macros: [ 11 | "MemberwiseInit": MemberwiseInitMacro.self, 12 | "InitRaw": InitMacro.self, 13 | ] 14 | ) { 15 | super.invokeTest() 16 | } 17 | } 18 | 19 | // TODO: For 1.0, diagnostic error on nonsensical @Init 20 | func testInitializedLet() { 21 | assertMacro { 22 | """ 23 | @MemberwiseInit 24 | struct S { 25 | @Init let number = 42 26 | } 27 | """ 28 | } expansion: { 29 | """ 30 | struct S { 31 | @Init let number = 42 32 | 33 | internal init() { 34 | } 35 | } 36 | """ 37 | } diagnostics: { 38 | """ 39 | @MemberwiseInit 40 | struct S { 41 | @Init let number = 42 42 | ┬──── 43 | ╰─ ⚠️ @Init can't be applied to already initialized constant 44 | ✏️ Remove '@Init' 45 | ✏️ Remove '= 42' 46 | } 47 | """ 48 | } fixes: { 49 | """ 50 | @Init let number = 42 51 | ┬──── 52 | ╰─ ⚠️ @Init can't be applied to already initialized constant 53 | 54 | ✏️ Remove '@Init' 55 | @MemberwiseInit 56 | struct S { 57 | let number = 42 58 | } 59 | 60 | ✏️ Remove '= 42' 61 | @MemberwiseInit 62 | struct S { 63 | @Init let number: Int 64 | } 65 | """ 66 | } 67 | } 68 | 69 | // TODO: For 1.0, diagnostic error on nonsensical @Init. While getter-only computed properties are 70 | // nonsensical, setter computed properties could be allowed, and perhaps also computed properties with init accessor? 71 | // TODO: For 0.3.0, diagnostic warning on nonsensical @Init? 72 | func testComputedProperty() { 73 | assertMacro { 74 | """ 75 | @MemberwiseInit 76 | struct S { 77 | var number: Int 78 | @Init var computed: Int { number * 2 } 79 | } 80 | """ 81 | } expansion: { 82 | """ 83 | struct S { 84 | var number: Int 85 | @Init var computed: Int { number * 2 } 86 | 87 | internal init( 88 | number: Int 89 | ) { 90 | self.number = number 91 | } 92 | } 93 | """ 94 | } 95 | } 96 | 97 | // TODO: For 1.0, diagnostic error on nonsensical @Init 98 | func testStaticProperty() { 99 | assertMacro { 100 | """ 101 | @MemberwiseInit 102 | struct S { 103 | @Init static var staticNumber: Int 104 | } 105 | """ 106 | } expansion: { 107 | """ 108 | struct S { 109 | @Init static var staticNumber: Int 110 | 111 | internal init() { 112 | } 113 | } 114 | """ 115 | } diagnostics: { 116 | """ 117 | @MemberwiseInit 118 | struct S { 119 | @Init static var staticNumber: Int 120 | ┬───── 121 | ╰─ ⚠️ @Init can't be applied to 'static' members 122 | ✏️ Remove '@Init' 123 | } 124 | """ 125 | } fixes: { 126 | """ 127 | @Init static var staticNumber: Int 128 | ┬───── 129 | ╰─ ⚠️ @Init can't be applied to 'static' members 130 | 131 | ✏️ Remove '@Init' 132 | @MemberwiseInit 133 | struct S { 134 | static var staticNumber: Int 135 | } 136 | """ 137 | } 138 | } 139 | 140 | // TODO: For 1.0, diagnostic error on nonsensical @Init 141 | func testLazyProperty() { 142 | assertMacro { 143 | """ 144 | @MemberwiseInit 145 | struct S { 146 | @Init(default: 0) lazy var lazyNumber: Int = { 147 | return 2 * 2 148 | }() 149 | } 150 | """ 151 | } expansion: { 152 | """ 153 | struct S { 154 | @Init(default: 0) lazy var lazyNumber: Int = { 155 | return 2 * 2 156 | }() 157 | 158 | internal init() { 159 | } 160 | } 161 | """ 162 | } diagnostics: { 163 | """ 164 | @MemberwiseInit 165 | struct S { 166 | @Init(default: 0) lazy var lazyNumber: Int = { 167 | ┬─── 168 | ╰─ ⚠️ @Init can't be applied to 'lazy' members 169 | ✏️ Remove '@Init(default: 0)' 170 | return 2 * 2 171 | }() 172 | } 173 | """ 174 | } fixes: { 175 | """ 176 | @Init(default: 0) lazy var lazyNumber: Int = { 177 | ┬─── 178 | ╰─ ⚠️ @Init can't be applied to 'lazy' members 179 | 180 | ✏️ Remove '@Init(default: 0)' 181 | @MemberwiseInit 182 | struct S { 183 | lazy var lazyNumber: Int = { 184 | return 2 * 2 185 | }() 186 | } 187 | """ 188 | } 189 | } 190 | 191 | // NB: 'lazy static' is redundant and a compiler error: "'lazy' cannot be used on an already-lazy 192 | // global". Since the fix is to "Remove 'lazy '", @MemberwiseInit emits its diagnostic on 193 | // 'static' which is still a mistake to apply @Init to. 194 | func testLazyStaticProperty() { 195 | assertMacro { 196 | """ 197 | @MemberwiseInit 198 | struct B { 199 | @Init lazy static var value = 0 200 | } 201 | """ 202 | } expansion: { 203 | """ 204 | struct B { 205 | @Init lazy static var value = 0 206 | 207 | internal init() { 208 | } 209 | } 210 | """ 211 | } diagnostics: { 212 | """ 213 | @MemberwiseInit 214 | struct B { 215 | @Init lazy static var value = 0 216 | ┬───── 217 | ╰─ ⚠️ @Init can't be applied to 'static' members 218 | ✏️ Remove '@Init' 219 | } 220 | """ 221 | } fixes: { 222 | """ 223 | @Init lazy static var value = 0 224 | ┬───── 225 | ╰─ ⚠️ @Init can't be applied to 'static' members 226 | 227 | ✏️ Remove '@Init' 228 | @MemberwiseInit 229 | struct B { 230 | lazy static var value = 0 231 | } 232 | """ 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /Tests/MemberwiseInitTests/CustomInitWrapperTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import MemberwiseInitMacros 3 | import SwiftSyntaxMacros 4 | import XCTest 5 | 6 | final class CustomInitWrapperTests: XCTestCase { 7 | override func invokeTest() { 8 | withMacroTesting( 9 | indentationWidth: .spaces(2), 10 | macros: [ 11 | "MemberwiseInit": MemberwiseInitMacro.self, 12 | "Init": InitMacro.self, 13 | "InitWrapper": InitMacro.self, 14 | ] 15 | ) { 16 | super.invokeTest() 17 | } 18 | } 19 | 20 | func testType() { 21 | assertMacro { 22 | """ 23 | @MemberwiseInit 24 | struct S { 25 | @InitWrapper(type: Q.self) 26 | var v: T 27 | } 28 | """ 29 | } expansion: { 30 | """ 31 | struct S { 32 | var v: T 33 | 34 | internal init( 35 | v: Q 36 | ) { 37 | self._v = v 38 | } 39 | } 40 | """ 41 | } 42 | } 43 | 44 | func testEscaping() { 45 | assertMacro { 46 | """ 47 | @MemberwiseInit 48 | struct S { 49 | @InitWrapper(escaping: true, type: Q.self) 50 | var v: T 51 | } 52 | """ 53 | } expansion: { 54 | """ 55 | struct S { 56 | var v: T 57 | 58 | internal init( 59 | v: @escaping Q 60 | ) { 61 | self._v = v 62 | } 63 | } 64 | """ 65 | } 66 | } 67 | 68 | func testLabel() { 69 | assertMacro { 70 | """ 71 | @MemberwiseInit 72 | struct S { 73 | @InitWrapper(label: "_", type: Q.self) 74 | var v: T 75 | } 76 | """ 77 | } expansion: { 78 | """ 79 | struct S { 80 | var v: T 81 | 82 | internal init( 83 | _ v: Q 84 | ) { 85 | self._v = v 86 | } 87 | } 88 | """ 89 | } 90 | } 91 | 92 | func testEverything() { 93 | assertMacro { 94 | """ 95 | @MemberwiseInit(.public) 96 | public struct S { 97 | @InitWrapper(.public, default: Q(), escaping: true, label: "_", type: Q.self) 98 | var v: T 99 | } 100 | """ 101 | } expansion: { 102 | """ 103 | public struct S { 104 | var v: T 105 | 106 | public init( 107 | _ v: @escaping Q = Q() 108 | ) { 109 | self._v = v 110 | } 111 | } 112 | """ 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tests/MemberwiseInitTests/InvalidSyntaxTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import MemberwiseInitMacros 3 | import XCTest 4 | 5 | final class InvalidSyntaxTests: XCTestCase { 6 | override func invokeTest() { 7 | // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 8 | withMacroTesting( 9 | //indentationWidth: .spaces(2), 10 | macros: [ 11 | "MemberwiseInit": MemberwiseInitMacro.self, 12 | "InitRaw": InitMacro.self, 13 | ] 14 | ) { 15 | super.invokeTest() 16 | } 17 | } 18 | 19 | func testInvalidRedeclaration_SucceedsWithInvalidCode() { 20 | assertMacro { 21 | """ 22 | @MemberwiseInit 23 | struct S { 24 | let x: T 25 | let x: T 26 | } 27 | """ 28 | } expansion: { 29 | """ 30 | struct S { 31 | let x: T 32 | let x: T 33 | 34 | internal init( 35 | x: T, 36 | x: T 37 | ) { 38 | self.x = x 39 | self.x = x 40 | } 41 | } 42 | """ 43 | } 44 | } 45 | 46 | func testInvalidRedeclaration2_SucceedsWithInvalidCode() { 47 | assertMacro { 48 | """ 49 | @MemberwiseInit 50 | struct S { 51 | let x, x: T 52 | } 53 | """ 54 | } expansion: { 55 | """ 56 | struct S { 57 | let x, x: T 58 | 59 | internal init( 60 | x: T, 61 | x: T 62 | ) { 63 | self.x = x 64 | self.x = x 65 | } 66 | } 67 | """ 68 | } 69 | } 70 | 71 | func testInvalidRedeclarationAndCustomLabelConflictsWithPropertyName_FailsWithDiagnostic() { 72 | assertMacro { 73 | """ 74 | @MemberwiseInit 75 | struct S { 76 | @Init(label: "x") let x: T 77 | let x: T 78 | } 79 | """ 80 | } expansion: { 81 | """ 82 | struct S { 83 | @Init(label: "x") let x: T 84 | let x: T 85 | 86 | internal init( 87 | x: T, 88 | x: T 89 | ) { 90 | self.x = x 91 | self.x = x 92 | } 93 | } 94 | """ 95 | } diagnostics: { 96 | """ 97 | @MemberwiseInit 98 | struct S { 99 | @Init(label: "x") let x: T 100 | ┬── 101 | ╰─ 🛑 Label 'x' conflicts with a property name 102 | let x: T 103 | } 104 | """ 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Tests/MemberwiseInitTests/MemberwiseInitDeprecationTests.swift: -------------------------------------------------------------------------------- 1 | import MacroTesting 2 | import MemberwiseInitMacros 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | 7 | final class MemberwiseInitDeprecationTests: XCTestCase { 8 | override func invokeTest() { 9 | // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 10 | withMacroTesting( 11 | //indentationWidth: .spaces(2), 12 | macros: [ 13 | "MemberwiseInit": MemberwiseInitMacro.self, 14 | "Init": InitMacro.self, 15 | ] 16 | ) { 17 | super.invokeTest() 18 | } 19 | } 20 | 21 | // Deprecated; remove in 1.0 22 | func testDotEscaping1() { 23 | assertMacro { 24 | """ 25 | @MemberwiseInit 26 | struct S { 27 | @Init(.escaping) var value: T 28 | } 29 | """ 30 | } expansion: { 31 | """ 32 | struct S { 33 | var value: T 34 | 35 | internal init( 36 | value: @escaping T 37 | ) { 38 | self.value = value 39 | } 40 | } 41 | """ 42 | } diagnostics: { 43 | """ 44 | @MemberwiseInit 45 | struct S { 46 | @Init(.escaping) var value: T 47 | ┬──────── 48 | ╰─ ⚠️ @Init(.escaping) is deprecated 49 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 50 | } 51 | """ 52 | } fixes: { 53 | """ 54 | @Init(.escaping) var value: T 55 | ┬──────── 56 | ╰─ ⚠️ @Init(.escaping) is deprecated 57 | 58 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 59 | @MemberwiseInit 60 | struct S { 61 | @Init(escaping: true) var value: T 62 | } 63 | """ 64 | } 65 | } 66 | 67 | // Deprecated; remove in 1.0 68 | func testDotEscaping2() { 69 | assertMacro { 70 | """ 71 | @MemberwiseInit 72 | struct S { 73 | @Init(.public, .escaping) var value: T 74 | } 75 | """ 76 | } expansion: { 77 | """ 78 | struct S { 79 | var value: T 80 | 81 | internal init( 82 | value: @escaping T 83 | ) { 84 | self.value = value 85 | } 86 | } 87 | """ 88 | } diagnostics: { 89 | """ 90 | @MemberwiseInit 91 | struct S { 92 | @Init(.public, .escaping) var value: T 93 | ┬───────────────── 94 | ╰─ ⚠️ @Init(.escaping) is deprecated 95 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 96 | } 97 | """ 98 | } fixes: { 99 | """ 100 | @Init(.public, .escaping) var value: T 101 | ┬───────────────── 102 | ╰─ ⚠️ @Init(.escaping) is deprecated 103 | 104 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 105 | @MemberwiseInit 106 | struct S { 107 | @Init(.public, escaping: true) var value: T 108 | } 109 | """ 110 | } 111 | } 112 | 113 | // Deprecated; remove in 1.0 114 | func testDotEscaping3() { 115 | assertMacro { 116 | """ 117 | @MemberwiseInit 118 | struct S { 119 | @Init(.escaping, label: "_") var value: T 120 | } 121 | """ 122 | } expansion: { 123 | """ 124 | struct S { 125 | var value: T 126 | 127 | internal init( 128 | _ value: @escaping T 129 | ) { 130 | self.value = value 131 | } 132 | } 133 | """ 134 | } diagnostics: { 135 | """ 136 | @MemberwiseInit 137 | struct S { 138 | @Init(.escaping, label: "_") var value: T 139 | ┬──────────────────── 140 | ╰─ ⚠️ @Init(.escaping) is deprecated 141 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 142 | } 143 | """ 144 | } fixes: { 145 | """ 146 | @Init(.escaping, label: "_") var value: T 147 | ┬──────────────────── 148 | ╰─ ⚠️ @Init(.escaping) is deprecated 149 | 150 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 151 | @MemberwiseInit 152 | struct S { 153 | @Init(escaping: true, label: "_") var value: T 154 | } 155 | """ 156 | } 157 | } 158 | 159 | // Deprecated; remove in 1.0 160 | func testDotEscaping4() { 161 | assertMacro { 162 | """ 163 | @MemberwiseInit 164 | struct S { 165 | @Init(.public, .escaping, label: "_") var value: T 166 | } 167 | """ 168 | } expansion: { 169 | """ 170 | struct S { 171 | var value: T 172 | 173 | internal init( 174 | _ value: @escaping T 175 | ) { 176 | self.value = value 177 | } 178 | } 179 | """ 180 | } diagnostics: { 181 | """ 182 | @MemberwiseInit 183 | struct S { 184 | @Init(.public, .escaping, label: "_") var value: T 185 | ┬───────────────────────────── 186 | ╰─ ⚠️ @Init(.escaping) is deprecated 187 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 188 | } 189 | """ 190 | } fixes: { 191 | """ 192 | @Init(.public, .escaping, label: "_") var value: T 193 | ┬───────────────────────────── 194 | ╰─ ⚠️ @Init(.escaping) is deprecated 195 | 196 | ✏️ Replace '@Init(.escaping)' with '@Init(escaping: true)' 197 | @MemberwiseInit 198 | struct S { 199 | @Init(.public, escaping: true, label: "_") var value: T 200 | } 201 | """ 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /bin/generate_access_level_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Define the output file location 4 | output_file="Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift" 5 | 6 | # Check for dirty changes in the target file 7 | if git diff --name-only | grep -q "$output_file"; then 8 | echo "Error: $output_file has uncommitted changes. Commit or stash them before running this script." 9 | exit 1 10 | fi 11 | 12 | # Extract the class name from the output file name 13 | class_name=$(basename "$output_file" .swift) 14 | 15 | # Define the possible values for each variable 16 | access_levels=("private" "" "public") 17 | 18 | # Initialize init_modifiers with an empty string and access levels 19 | init_modifiers=("") 20 | for level in "${access_levels[@]}"; do 21 | if [ -n "$level" ]; then 22 | init_modifiers+=("@Init(.$level)") 23 | else 24 | # For the empty/default case, use "@Init(.internal)" 25 | init_modifiers+=("@Init(.internal)") 26 | fi 27 | done 28 | 29 | # NB: '[none]' represents case where a struct has no properties 30 | property_access_levels=("private" "" "public" "[none]") 31 | 32 | capitalize() { 33 | echo "$1" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1' 34 | } 35 | 36 | # Start writing to the output file 37 | { 38 | echo "// This file is automatically generated by '../../bin/generate_access_level_tests.sh'." 39 | echo "// Do not edit this file directly." 40 | echo "" 41 | echo "import MacroTesting" 42 | echo "import MemberwiseInitMacros" 43 | echo "import SwiftSyntaxMacros" 44 | echo "import XCTest" 45 | echo "" 46 | echo "final class $class_name: XCTestCase {" 47 | echo " override func invokeTest() {" 48 | echo " // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8" 49 | echo " withMacroTesting(" 50 | echo " //indentationWidth: .spaces(2)," 51 | echo " //isRecording: true," 52 | echo " macros: [" 53 | echo " \"MemberwiseInit\": MemberwiseInitMacro.self," 54 | echo " \"Init\": InitMacro.self," 55 | echo " ]" 56 | echo " ) {" 57 | echo " super.invokeTest()" 58 | echo " }" 59 | echo " }" 60 | echo "" 61 | 62 | # Generate test cases for each combination of values 63 | for memberwise_access in "${access_levels[@]}"; do 64 | for struct_access in "${access_levels[@]}"; do 65 | for property_access in "${property_access_levels[@]}"; do 66 | 67 | # If property is [none], we don't want to iterate over different init_modifiers 68 | if [ "$property_access" == "[none]" ]; then 69 | init_modifiers_temp=("") 70 | else 71 | init_modifiers_temp=("${init_modifiers[@]}") 72 | fi 73 | 74 | for init_modifier in "${init_modifiers_temp[@]}"; do 75 | 76 | memberwise_str=$([ -z "$memberwise_access" ] && echo "" || echo ".$memberwise_access") 77 | struct_str=$([ -z "$struct_access" ] && echo "" || echo "$struct_access") 78 | init_str=$([ -z "$init_modifier" ] && echo "" || echo "$init_modifier") 79 | property_str=$([ -z "$property_access" ] && echo "" || echo "$property_access") 80 | 81 | init_name=$(echo "$init_modifier" | sed -E 's/@Init\((.*)\)/\1/' | tr -d '.' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1') 82 | 83 | # Adjusting the test name for the "[none]" property access case 84 | test_property_name=$([ "$property_access" == "[none]" ] && echo "No" || echo "$(capitalize ${property_access:-default})") 85 | 86 | test_name="testMemberwiseInit$(capitalize ${memberwise_access:-default})_$(capitalize ${struct_access:-default})Struct_${init_name:+Init}${init_name}_${test_property_name}Property" 87 | test_name="${test_name//,/_}" 88 | test_name="${test_name// /}" 89 | test_name="${test_name//__/_}" 90 | 91 | echo " func $test_name() {" 92 | echo " assertMacro(applyFixIts: false) {" 93 | echo " \"\"\"" 94 | echo " @MemberwiseInit${memberwise_str:+($memberwise_str)}" 95 | echo " ${struct_str}${struct_str:+ }struct S {" 96 | 97 | # Only add the property declaration if property_access is not '[none]' 98 | if [ "$property_access" != "[none]" ]; then 99 | echo " ${init_str}${init_str:+ }${property_str}${property_str:+ }let v: T" 100 | fi 101 | 102 | echo " }" 103 | echo " \"\"\"" 104 | echo " }" 105 | echo " }" 106 | echo "" 107 | done 108 | done 109 | done 110 | done 111 | 112 | echo "}" 113 | 114 | } > "$output_file" 115 | 116 | echo "Test file generated at $output_file" 117 | --------------------------------------------------------------------------------