From f6a334cc88632dff43155317948f23b85f815076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Thu, 31 Aug 2023 13:11:44 -0700 Subject: [PATCH] =?UTF-8?q?MemberwiseInit=20=F0=9F=8E=89=20(wip)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/CODE_OF_CONDUCT.md | 132 + .github/ISSUE_TEMPLATE/bug_report.yml | 71 + .github/ISSUE_TEMPLATE/config.yml | 9 + .github/workflows/ci.yml | 65 + .github/workflows/format.yml | 29 + .gitignore | 7 + LICENSE | 21 + Makefile | 8 + Package.resolved | 32 + Package.swift | 54 + README.md | 574 ++++ Sources/MemberwiseInit/MemberwiseInit.swift | 73 + Sources/MemberwiseInitClient/main.swift | 169 ++ .../MemberwiseInitMacros/MacroPlugin.swift | 10 + .../Macros/MemberwiseInitMacro.swift | 334 +++ .../Macros/Support/AccesssLevelSyntax.swift | 120 + .../Macros/Support/Diagnostics.swift | 67 + .../Macros/Support/SyntaxHelpers.swift | 61 + .../MemberwiseInitAccessLevelTests.swift | 2668 +++++++++++++++++ .../MemberwiseInitTests.swift | 1764 +++++++++++ bin/generate_access_level_tests.sh | 115 + 21 files changed, 6383 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/format.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/MemberwiseInit/MemberwiseInit.swift create mode 100644 Sources/MemberwiseInitClient/main.swift create mode 100644 Sources/MemberwiseInitMacros/MacroPlugin.swift create mode 100644 Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift create mode 100644 Sources/MemberwiseInitMacros/Macros/Support/AccesssLevelSyntax.swift create mode 100644 Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift create mode 100644 Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift create mode 100644 Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift create mode 100644 Tests/MemberwiseInitTests/MemberwiseInitTests.swift create mode 100755 bin/generate_access_level_tests.sh diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..12d11ca --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0baf61d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,71 @@ +name: Bug Report +description: Something isn't working as expected +labels: [bug] +body: +- type: markdown + attributes: + value: | + Thank you for contributing to swift-memberwise-init-macro! + + 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 +- type: textarea + attributes: + label: Description + description: | + A short description of the incorrect behavior. + + 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. + validations: + required: true +- type: checkboxes + attributes: + label: Checklist + options: + - label: If possible, I've reproduced the issue using the `main` branch of this package. + required: false + - 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). + required: true +- type: textarea + attributes: + label: Expected behavior + description: Describe what you expected to happen. + validations: + required: false +- type: textarea + attributes: + label: Actual behavior + description: Describe or copy/paste the behavior you observe. + validations: + required: false +- type: textarea + attributes: + label: Steps to reproduce + description: | + Explanation of how to reproduce the incorrect behavior. + + This could include an attached project or link to code that is exhibiting the issue, and/or a screen recording. + placeholder: | + 1. ... + validations: + required: false +- type: input + attributes: + label: swift-memberwise-init-macro version information + description: The version of swift-memberwise-init-macro used to reproduce this issue. + placeholder: "'0.1.0' for example, or a commit hash" +- type: input + attributes: + label: Destination operating system + description: The OS running swift-memberwise-init-macro. + placeholder: "'macOS 14' for example" +- type: input + attributes: + label: Xcode version information + description: The version of Xcode used to reproduce this issue. + placeholder: "The version displayed from 'Xcode 〉About Xcode'" +- type: textarea + attributes: + label: Swift Compiler version information + description: The version of Swift used to reproduce this issue. + placeholder: Output from 'xcrun swiftc --version' + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e49a6be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,9 @@ +blank_issues_enabled: false + +contact_links: + - name: Project Discussion + url: https://github.com/gohanlon/swift-memberwise-init-macro/discussions + about: Library Q&A, ideas, and more + - name: README + url: https://github.com/gohanlon/swift-memberwise-init-macro + about: Read the README diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5dfc7cd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,65 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + macos: + name: macOS + runs-on: macos-13 + steps: + - uses: actions/checkout@v4 + - name: Select Xcode 15 + run: sudo xcode-select -s /Applications/Xcode_15.0.app + - name: Run tests + run: swift test + + linux: + name: Linux + runs-on: ubuntu-latest + steps: + - name: Install Swift + uses: slashmo/install-swift@v0.4.0 + with: + version: 5.9 + - uses: actions/checkout@v4 + - name: Run tests + run: swift test + + # NB: 5.9 snapshot unavailable, wait for release + # wasm: + # name: Wasm + # runs-on: ubuntu-latest + # strategy: + # matrix: + # include: + # - { toolchain: wasm-5.9-RELEASE } + # steps: + # - uses: actions/checkout@v4 + # - run: echo "${{ matrix.toolchain }}" > .swift-version + # - uses: swiftwasm/swiftwasm-action@v5.9 + # with: + # shell-action: carton test --environment node + + # NB: 5.9 snapshot outdated, wait for release + # windows: + # name: Windows + # runs-on: windows-latest + # steps: + # - uses: compnerd/gha-setup-swift@main + # with: + # branch: swift-5.9-release + # tag: 5.9-DEVELOPMENT-SNAPSHOT-2023-09-16-a + # - uses: actions/checkout@v4 + # - name: Run tests + # run: swift test diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..84b3245 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,29 @@ +name: Format + +on: + push: + branches: + - main + +concurrency: + group: format-${{ github.ref }} + cancel-in-progress: true + +jobs: + swift_format: + name: swift-format + runs-on: macos-13 + steps: + - uses: actions/checkout@v3 + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_14.3.1.app + - name: Tap + run: brew install swift-format + - name: Format + run: make format + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Run swift-format + branch: 'main' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6148700 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/.swiftpm +/Packages +/*.swiftinterface +/*.xcodeproj +xcuserdata/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b1b5908 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Galen O’Hanlon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..30362cf --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +format: + swift format \ + --ignore-unparsable-files \ + --in-place \ + --recursive \ + ./Package.swift ./Sources ./Tests + +.PHONY: format \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..269ef50 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,32 @@ +{ + "pins" : [ + { + "identity" : "swift-macro-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/gohanlon/swift-macro-testing", + "state" : { + "branch" : "explicit-indentation-width", + "revision" : "76d525003f044c7b97d3d577ee03f35d07697c8c" + } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing", + "state" : { + "revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4", + "version" : "1.14.2" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax", + "state" : { + "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", + "version" : "509.0.0" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..bb9dd97 --- /dev/null +++ b/Package.swift @@ -0,0 +1,54 @@ +// swift-tools-version: 5.9 + +import CompilerPluginSupport +import PackageDescription + +let package = Package( + name: "MemberwiseInit", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "MemberwiseInit", + targets: ["MemberwiseInit"] + ), + .executable( + name: "MemberwiseInitClient", + targets: ["MemberwiseInitClient"] + ), + ], + dependencies: [ + .package( + url: "https://github.com/gohanlon/swift-macro-testing", + branch: "explicit-indentation-width" + ), // TODO: w/f https://github.com/pointfreeco/swift-macro-testing/pull/8 + // .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.2.1"), + .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), + // .package(url: "https://github.com/apple/swift-syntax", branch: "main"), + ], + targets: [ + .macro( + name: "MemberwiseInitMacros", + dependencies: [ + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), + .product(name: "SwiftOperators", package: "swift-syntax"), + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + ] + ), + .target(name: "MemberwiseInit", dependencies: ["MemberwiseInitMacros"]), + .executableTarget(name: "MemberwiseInitClient", dependencies: ["MemberwiseInit"]), + .testTarget( + name: "MemberwiseInitTests", + dependencies: [ + "MemberwiseInitMacros", + .product(name: "MacroTesting", package: "swift-macro-testing"), + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfc65e7 --- /dev/null +++ b/README.md @@ -0,0 +1,574 @@ +# @MemberwiseInit + +A Swift Macro to generate seamless, safe, and explicit memberwise inits. + +> :warning: :warning: :warning: **Pre-release**
+> I’m on the cusp of the official release and excited to share this with you! Dive in, experiment, and any feedback you provide will be greatly appreciated. + +> :warning: **Important**
+> `@MemberwiseInit` is a Swift Macro requiring **swift-tools-version: 5.9** or later (**Xcode 15** onwards). + +* [Introduction](#introduction) +* [Quick start](#quick-start) +* [Quick reference](#quick-reference) +* [Advanced usage and limitations](#advanced-usage-and-limitations) + * [Underscore-prefixed properties](#underscore-prefixed-properties) + * [Attributed properties are ignored by default, but includable](#attributed-properties-are-ignored-by-default-but-includable) + * [Explicitly ignore properties](#explicitly-ignore-properties) + * [Automatic `@escaping` for closure types (usually)](#automatic-escaping-for-closure-types-usually) + * [Tuple destructuring in property declarations isn’t supported (yet)](#tuple-destructuring-in-property-declarations-isnt-supported-yet) +* [Motivation](#motivation) +* [Installation](#installation) +* [License](#license) + +## Introduction + +Swift provides memberwise initializers for structs, streamlining instantiation. However, this convenience doesn’t extend to classes or actors, and structs face constraints: limited init visibility, an inability to coexist with custom initializers, and lack of fine-grained control. + +**MemberwiseInit** addresses these constraints: + +- **Familiar** +
MemberwiseInit closely mirrors Swift’s native memberwise initializer, making it feel at home alongside types relying on the built-in init. Furthermore, it’s additive in nature, offering similar yet incrementally enhanced automatic initializer generation. + +- **Control** +
Adjust your init using attributes. + +- **Safety** +
MemberwiseInit generates safe public initializers without exposing private properties by default. Override property visibility when necessary. + +- **Enhanced utility** +
Seamlessly incorporate a generated memberwise init even when you have custom initializers or need to manually conform to popular protocols which require an `init`, such as `Decodable` and `RawRepresentable`.[^1] + +- **Broadened scope** +
While Swift rightly avoids auto-generating memberwise initializers for classes and actors due to possible complexities with inheritance and safety, MemberwiseInit empowers you to create one when you deem it appropriate. + +- **Reduced boilerplate** +
Write and maintain fewer manual inits. + +## Quick start + +To use MemberwiseInit: + +1. **Installation** +
Add MemberwiseInit via Swift Package Manager by providing the package URL to your Xcode project: “https://github.com/gohanlon/swift-memberwise-init-macro”. + + Or, for SPM-based projects, add it to your package dependencies and target dependency: + + ```swift + .package(url: "https://github.com/gohanlon/swift-memberwise-init-macro", from: "0.1.0"), + ``` + + ```swift + dependencies: [ + .product(name: "MemberwiseInit", package: "swift-memberwise-init-macro"), + ] + ``` + +2. **Import & basic usage** +
After importing MemberwiseInit, simply add `@MemberwiseInit` before your type definition. This will mirror Swift’s behavior: it generates an initializer with up to internal access, but scales down if any properties are more restrictive. Here, `age` being private makes the initializer private too. + + ```swift + import MemberwiseInit + + @MemberwiseInit + struct Person { + let name: String + private var age: Int? = nil + } + ``` + +3. **Customize visibility** +
Make the struct public and use `@MemberwiseInit(.public)` to enable up to a public initializer. At this point, the init will still be private because `age` is private. + + ```swift + @MemberwiseInit(.public) + public struct Person { + let name: String + private var age: Int? = nil + } + ``` + + Make `name` public instead of internal (alternatively, you could mark it with`@Init(.public)`), and tell MemberwiseInit to ignore `age` with `@Init(.ignore)`: + + ```swift + @MemberwiseInit(.public) + public struct Person { + public let name: String + @Init(.ignore) private var age: Int? = nil + } + ``` + +That’s it! Now access your new public memberwise initializer: + +```swift +let person = Person(name: "Blob") +``` + +## Quick reference + +MemberwiseInit includes two autocomplete-friendly macros: + +### `@MemberwiseInit` +Attach to struct, actor, or class. + +* `@MemberwiseInit` +
Generate up to an internal memberwise init, closely mimicking Swift’s built-in memberwise init. + +* `@MemberwiseInit(.public)` +
Generate a memberwise init with up to the provided access level. Valid access levels: `.private`, `.fileprivate`, `.internal`, `.public`, `.open`. + +### `@Init` +Attach to member property declarations of a struct, actor, or class that `@MemberwiseInit` is generating an init for. + +* `@Init` +
Include a property that would otherwise be ignored, e.g., attributed properties such as SwiftUI’s `@State` properties. + +* `@Init(.ignore)` +
Ignore that member property. The access level of an ignored property won’t affect that of the generated init, and the property won’t be included in the init. *Note: Ignored properties must be initialized elsewhere.* + +* `@Init(.public)` +
For calculating the generated init’s access level, consider the property as having a different access level than its declared access level. Valid access levels: `.private`, `.fileprivate`, `.internal`, `.public`, `.open`. + +* `@Init(.escaping`) +
To avoid compiler errors when a property’s init argument can’t automatically be `@escaped`, e.g. when a property’s type uses a typealias that represents a closure. + +* `@Init(.public, .escaping`) +
Access level and escaping behaviors can be used together. + +## Advanced usage and limitations + +### Underscore-prefixed properties +When using MemberwiseInit on underscore-prefixed properties, the initializer will present a non-prefixed version as the external parameter. + +#### Explanation +In Swift, properties prefixed with an underscore are conventionally used as internal storage or backing properties. MemberwiseInit respects this convention, creating public-facing initializers without the underscore: + +```swift +@MemberwiseInit(.public) +public struct Review { + @Init(.public) private let _rating: Int + + var rating: String { + return String(repeating: "⭐️", count: self._rating) + } +} +``` + +This yields: + +```swift +public init( + rating: Int // 👈 Non-underscored parameter +) { + self._rating = rating +} +``` + +### Attributed properties are ignored by default, but includable + +If MemberwiseInit ignores an attributed property and causes a compiler error, you have two immediate remedies: + +1. Assign a default value to the property. +2. Explicitly include the property in the initializer using the `@Init` annotation. + +#### Explanation +Unlike the compiler’s default behavior, MemberwiseInit takes a more cautious approach when dealing with member properties that have attributes attached. + +For a SwiftUI-based illustration, let’s look at a view without MemberwiseInit: + +```swift +import SwiftUI +struct MyView: View { + @State var isOn: Bool + + var body: some View { … } +} +``` + +The compiler provides the following default memberwise initializer: + +```swift +internal init( + isOn: Bool +) { + self.isOn = isOn +} +``` + +However, initializing `@State` properties in this manner is a common pitfall in SwiftUI. The `isOn` state is only assigned upon the initial rendering of the view, and this assignment doesn’t occur on subsequent renders. To safeguard against this, MemberwiseInit defaults to ignoring such attributed properties: + +```swift +import SwiftUI +@MemberwiseInit // 👈 +struct MyView: View { + @State var isOn: Bool + + var body: some View { … } +} +``` + +This leads MemberwiseInit to generate the following initializer: + +```swift +internal init() { +} // 🛑 Compiler error:↵ +// Return from initializer without initializing all stored properties +``` + +From here, you have two alternatives: + +1. **Assign a default value** +
Defaulting the property to a value makes the generated MemberwiseInit valid, as the generated init no longer needs to initialize the property. + + ```swift + import SwiftUI + @MemberwiseInit + struct MyView: View { + @State var isOn: Bool = false // 👈 Default value provided + + var body: some View { … } + } + ``` + + The resulting initializer is: + + ```swift + internal init() { + } // 🎉 No error, all stored properties are initialized + ``` + +2. **Use `@Init` annotation** +
If you understand the behavior the attribute imparts, you can explicitly mark the property with `@Init` to include it in the initializer. + + ```swift + import SwiftUI + @MemberwiseInit + struct MyView: View { + @Init @State var isOn: Bool // 👈 `@Init` + + var body: some View { … } + } + ``` + + This yields: + + ```swift + internal init( + isOn: Bool + ) { + self.isOn = isOn + } + ``` + +### Explicitly ignore properties +To have MemberwiseInit both ignore and exclude specific properties from its generated initializer, mark them with `@Init(.ignore)`. Ensure these properties are otherwise initialized to avoid compiler errors. + +### Explanation +The `@Init(.ignore)` attribute excludes properties from the initializer, potentially allowing MemberwiseInit to produce a more accessible initializer for the remaining properties. + +For example: + +```swift +@MemberwiseInit(.public) +public struct Person { + public let name: String + @Init(.ignore) private var age: Int? = nil // 👈 Ignored and given a default value +} +``` + +By marking `age` as ignored, MemberwiseInit creates a public initializer without the `age` parameter: + +```swift +public init( + name: String +) { + self.name = name +} +``` + +If `age` weren't marked as ignored, the initializer would be private and would include the `age` property. + +> **Note** +> In line with Swift’s built-in init, MemberwiseInit automatically ignores `let` properties with assigned default values, as reassigning such properties within the initializer would be invalid. + +### Automatic `@escaping` for closure types (usually) +If you’re using MemberwiseInit with closure types, it’ll handle `@escaping` automatically for directly declared closures. However, if you use a typealias that represents a closure, ensure to explicitly mark the property with `@Init(.escaping)` to avoid compiler errors. + +#### Explanation +Swift Macros operate at the syntax level and don’t inherently understand type information. MemberwiseInit will automatically attribute init parameters with `@escaping`, provided that the closure type is directly declared as part of the property. Fortunately, this is the typical scenario. + +In contrast, the compiler’s built-in memberwise init has the advantage of working with type information. This allows it to recognize and add `@escaping` even when the closure type is “obscured” within a typealias. + +Consider the following struct: + +```swift +public struct TaskRunner { + public let onCompletion: () -> Void +} +``` + +Through observation (or by delving into the compiler’s source code), we can see that the built-in memberwise init generates the following internal init: + +```swift +internal init( + onCompletion: @escaping () -> Void // 🎉 `@escaping` automatically +) { + self.onCompletion = onCompletion +} +``` + +Now, with MemberwiseInit: + +```swift +@MemberwiseInit // 👈 +public struct TaskRunner { + public let onCompletion: () -> Void +} +``` + +we get the same init, which we can inspect using Xcode’s “Expand Macro” command: + +```swift +internal init( + onCompletion: @escaping () -> Void // 🎉 `@escaping` automatically +) { + self.onCompletion = onCompletion +} +``` + +And we can have MemberwiseInit generate a public init: + +```swift +@MemberwiseInit(.public) // 👈 `.public` +public struct TaskRunner { + public let onCompletion: () -> Void +} +``` + +This yields: + +```swift +public init( // 🎉 `public` + onCompletion: @escaping () -> Void +) { + self.onCompletion = onCompletion +} +``` + +Now, suppose the type of `onCompletion` got more complex and we decided to extract a typealias: + +```swift +public typealias CompletionHandler = @Sendable () -> Void + +@MemberwiseInit(.public) +public struct TaskRunner: Sendable { + public let onCompletion: CompletionHandler +} +``` + +When using type aliases that represent closure types, there’s a caveat: Remember, Swift Macros operate at the syntax level and don’t inherently understand type information. MemberwiseInit cannot “see” that `CompletionHandler` represents a closure type that needs to be marked `@escaping`, leading to a compiler error: + +```swift +public init( + onCompletion: CompletionHandler // 👈 Missing `@escaping`! +) { + self.onCompletion = onCompletion // 🛑 Compiler error:↵ + // Assigning non-escaping parameter 'onCompletion' to an @escaping closure +} +``` + +To address this, when using a typealias for closures, you must explicitly mark the property with `@Init(.escaping)`: + +```swift +public typealias CompletionHandler = @Sendable () -> Void + +@MemberwiseInit(.public) +public struct TaskRunner: Sendable { + @Init(.escaping) public let onCompletion: CompletionHandler // 👈 +} +``` + +which results in the following valid and inspectable public init: + +```swift +public init( + onCompletion: @escaping CompletionHandler // 🎉 Correctly `@escaping` +) { + self.onCompletion = onCompletion +} +``` + +### Tuple destructuring in property declarations isn’t supported (yet) + +Using tuple syntax in property declarations isn’t supported: + +```swift +@MemberwiseInit +struct Point2D { + let (x, y): (Int, Int) +//┬───────────────────── +//╰─ 🛑 @MemberwiseInit does not support tuple destructuring for +// property declarations. Use multiple declartions instead. +} +``` + +## Motivation + +The Swift compiler’s built-in memberwise init behavior is safe by design. It won’t assume that a public type should be constructible from external modules, so it never generates an init having an access level greater than “internal.” + +To safely add a public init to a type requires an explicit developer intent. Traditionally, that means manually declaring an init, or using Xcode to generate a boilerplate init. Take this simple example: + +```swift +public struct Person { + public let name: String +} +``` + +Swift transparently adds the following, familiar init: + +```swift +internal init( + name: String +) { + self.name = name +} +``` + +MemberwiseInit can generate the exact same init: + +```swift +@MemberwiseInit // 👈 +public struct Person { + public let name: String +} +``` + +Unlike Swift’s built-in init, you can inspect MemberwiseInit’s generated init using Xcode by right clicking on `@MemberwiseInit` and the selecting “Expand Macro”. + +> **Note** +> Introducing an explicit init suppresses the addition of Swift’s built-in memberwise initializer. MemberwiseInit’s initializer is always added and can coexist with your other inits. + +In contrast to Swift’s native memberwise initializer, MemberwiseInit can generate initializers up to any access level, including public. You explicitly allow it to generate a public init by marking `Person` with `@MemberwiseInit(.public)`: + +```swift +@MemberwiseInit(.public) // 👈 `.public` +public struct Person { + public let name: String +} +``` + +With this adjustment, expanding the macro yields: + +```swift +public init( // 🎉 `public` + name: String +) { + self.name = name +} +``` + +Suppose you then added a private member to `Person`: + +```swift +@MemberwiseInit(.public) +public struct Person { + public let name: String + private var age: Int? // 👈 `private` +} +``` + +Now MemberwiseInit, as the compiler’s built-in init would, generates a private init: + +```swift +private init( // 👈 `private` + name: String, + age: Int? +) { + self.name = name + self.age = age +} +``` + +The reason this init is private is foundational to understanding both MemberwiseInit and the compiler’s built-in memberwise init. By default, they both generate an init that acts as a safeguard, ensuring there’s no unintentional leaked access to more restricted properties. + +To publicly expose `age` via MemberwiseInit’s generated init, mark it with `@Init(.public)`: + +```swift +@MemberwiseInit(.public) +public struct Person { + public let name: String + @Init(.public) private var age: Int? // 👈 `@Init(.public)` +} +``` + +and now MemberwiseInit generates a public init that exposes the private `age` property: + +```swift +public init( // 👈 `public` + name: String, + age: Int? // 👈 Exposed deliberately +) { + self.name = name + self.age = age +} +``` + +Let’s give `age` a default value: + +```swift +@MemberwiseInit(.public) +public struct Person { + public let name: String + @Init(.public) private var age: Int? = nil // 👈 Default value +} +``` + +and now MemberwiseInit’s init parameter includes the default `age` value: + +```swift +public init( + name: String, + age: Int? = nil // 👈 Default value +) { + self.name = name + self.age = age +} +``` + +Suppose we don’t want to expose `age` publicly via the init. As long as `age` is initialized in another way (e.g. declared with a default value), we can explicitly tell MemberwiseInit to ignore it using `@Init(.ignore)`: + +```swift +@MemberwiseInit(.public) +public struct Person { + public let name: String + @Init(.ignore) private var age: Int? = nil // 👈 `.ignore` +} +``` + +Now MemberwiseInit ignores the private `age` property and generates a public init: + +```swift +public init( // 👈 `public`, ignoring `age` property + name: String +) { + self.name = name +} +``` + +## Installation + +To integrate MemberwiseInit into your Xcode project: + +1. Open your project in Xcode. +2. Navigate to the menu `File > Add Package Dependencies…`. +3. Enter the package repository URL in the “Search or Enter Package URL” field: “https://github.com/gohanlon/swift-memberwise-init-macro”. +4. Select the latest version or specify a version range. +5. Click ‘Next’ and then ‘Finish’ to complete the integration. + +After integrating, import the macro using `import MemberwiseInit` in your Swift files. + +## License + +MemberwiseInit is available under the MIT license. See the [LICENSE](https://github.com/gohanlon/swift-memberwise-init-macro/blob/main/LICENSE) file for more info. + +[^1]: Swift omits its built-in memberwise init when any explicit init is present. You can do an [“extension dance”](https://gist.github.com/gohanlon/6aaeff970c955c9a39308c182c116f64) to retain Swift’s built-in memberwise init, but with imposed tradeoffs. diff --git a/Sources/MemberwiseInit/MemberwiseInit.swift b/Sources/MemberwiseInit/MemberwiseInit.swift new file mode 100644 index 0000000..40f2e30 --- /dev/null +++ b/Sources/MemberwiseInit/MemberwiseInit.swift @@ -0,0 +1,73 @@ +public enum AccessLevelConfig { + case `private` + case `fileprivate` + case `internal` + case `public` + case `open` +} + +// MARK: @MemberwiseInit macro + +@attached(member, names: named(init)) +public macro MemberwiseInit( + _ accessLevel: AccessLevelConfig = .internal, + _optionalsDefaultNil: Bool = false +) = + #externalMacro( + module: "MemberwiseInitMacros", + type: "MemberwiseInitMacro" + ) + +// MARK: @Init macro + +public enum EscapingConfig { + case escaping +} + +public enum IgnoreConfig { + case ignore +} + +@attached(peer) +public macro Init() = + #externalMacro( + module: "MemberwiseInitMacros", + type: "InitMacro" + ) + +@attached(peer) +public macro Init( + _ ignore: IgnoreConfig +) = + #externalMacro( + module: "MemberwiseInitMacros", + type: "InitMacro" + ) + +@attached(peer) +public macro Init( + _ accessLevel: AccessLevelConfig +) = + #externalMacro( + module: "MemberwiseInitMacros", + type: "InitMacro" + ) + +@attached(peer) +public macro Init( + _ escaping: EscapingConfig +) = + #externalMacro( + module: "MemberwiseInitMacros", + type: "InitMacro" + ) + +@attached(peer) +public macro Init( + _ accessLevel: AccessLevelConfig, + _ escaping: EscapingConfig +) = + #externalMacro( + module: "MemberwiseInitMacros", + type: "InitMacro" + ) diff --git a/Sources/MemberwiseInitClient/main.swift b/Sources/MemberwiseInitClient/main.swift new file mode 100644 index 0000000..93cc175 --- /dev/null +++ b/Sources/MemberwiseInitClient/main.swift @@ -0,0 +1,169 @@ +import MemberwiseInit + +@MemberwiseInit(.public) +public struct Person1 { + public let name: String + @Init(.public) private var age: Int? = nil +} +_ = Person1(name: "Blob") +_ = Person1(name: "Blob", age: 42) + +@MemberwiseInit(.public) +public struct Person2 { + public let name: String + @Init(.ignore) private var age: Int? = nil +} +_ = Person2(name: "Blob") +//_ = Person2(name: "Blob", age: 42) // 🛑 Incorrect argument label in call + +@MemberwiseInit(.public) +public struct Dependency: Sendable { + public let _get: @Sendable (_ key: String, _ type: Any.Type) -> (any Sendable)? + public let _set: @Sendable (_ value: (any Sendable)?, _ key: String) -> Void + public let _values: + @Sendable (_ key: String, _ value: Any.Type) -> AsyncStream<(any Sendable)?> +} +_ = Dependency( + get: { key, type in }, + set: { value, key in }, + values: { key, value in + return .init { continuation in + // … + } + } +) + +public typealias CompletionHandler = @Sendable () -> Void + +@MemberwiseInit(.public) +public struct TaskRunner: Sendable { + @Init(.escaping) public let onCompletion: CompletionHandler +} + +//@MemberwiseInit(.public) +//struct MyView: View { +// @State var isOn: Bool = false // 👈 initializer clause +// +// var body: some View { EmptyView() } +//} +//_ = MyView.init() +// +//@MemberwiseInit +//struct MyView2: View { +// @Init @State var isOn: Bool // 👈 @Init +// +// var body: some View { EmptyView() } +//} +//_ = MyView2.init(isOn: true) + +//@MemberwiseInit +//struct Point2D { +// let (x, y): (Int, Int) // 🛑 @MemberwiseInit does not support tuple destructuring for property declarations. Use multiple declarations instead. +//} + +@MemberwiseInit +struct Person10 { + let _name: String +} +_ = Person10(name: "Foo") +//_ = Person10(_name: "Foo") // 🛑 No exact matches in call to initializer + +// Bare: +struct Person11 { + let _name: String +} +_ = Person11.init(_name: "Foo") + +@MemberwiseInit +struct Person20 { + @Init(.internal) private let age: Int +} +_ = Person20(age: 42) + +// Bare: +//struct Person21 { +// private let age: Int +//} +//_ = Person21(age: 42) // Error: 'Person21' initializer is inaccessible due to 'private' protection level + +//@MemberwiseInit +//struct Person22 { +// private let age: Int +//} +//_ = Person22(age: 42) // Error: 'Person22' initializer is inaccessible due to 'private' protection level + +@MemberwiseInit +struct Person30: Decodable { + let name: String + + enum CodingKeys: CodingKey { + case name + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + } +} +_ = Person30.init(name: "Foo") + +// Bare: +//struct Person31: Decodable { +// let name: String +// +// enum CodingKeys: CodingKey { +// case name +// } +// +// init(from decoder: Decoder) throws { +// let container = try decoder.container(keyedBy: CodingKeys.self) +// self.name = try container.decode(String.self, forKey: .name) +// } +//} +//_ = Person31.init(name: "Foo") // 🛑 Incorrect argument label in call (have 'name:', expected 'from:') + +@MemberwiseInit +struct Person40: RawRepresentable { + let name: String + + var rawValue: String { + self.name + } + + init?(rawValue: String) { + self.name = rawValue + } +} +_ = Person40.init(name: "Foo") + +// Bare: +//struct Person41: RawRepresentable { +// let name: String +// +// var rawValue: String { +// self.name +// } +// +// init?(rawValue: String) { +// self.name = rawValue +// } +//} +//_ = Person41.init(name: "Foo") // 🛑 Incorrect argument label in call (have 'name:', expected 'rawValue:') + +// Bare: +//class Calculator { +// lazy var lastResult: Double // 🛑 `Error: Lazy properties must have an initializer` +//} + +// MemberwiseInit does not supports tuple destructuring for property declarations (yet): +//@MemberwiseInit +//struct Point2D { +// let (defaultX, defaultY): (Int, Int) // 🛑 @MemberwiseInit does not support tuple destructuring for property declarations. Use multiple declarations instead. +//} + +// Swift does support tuple destructuring for property declarations: +// Bare: +struct Point { + let (defaultX, defaultY): (Int, Int) +} +_ = Point.init(defaultX: 0, defaultY: 0) diff --git a/Sources/MemberwiseInitMacros/MacroPlugin.swift b/Sources/MemberwiseInitMacros/MacroPlugin.swift new file mode 100644 index 0000000..ad8ecdd --- /dev/null +++ b/Sources/MemberwiseInitMacros/MacroPlugin.swift @@ -0,0 +1,10 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct MemberwiseInitPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + InitMacro.self, + MemberwiseInitMacro.self, + ] +} diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift new file mode 100644 index 0000000..9b681b1 --- /dev/null +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -0,0 +1,334 @@ +import SwiftCompilerPlugin +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct InitMacro: PeerMacro { + public static func expansion( + of node: SwiftSyntax.AttributeSyntax, + providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, + in context: some SwiftSyntaxMacros.MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + return [] + } +} + +public struct MemberwiseInitMacro: MemberMacro { + struct PropertyBinding { + let accessLevel: AccessLevelModifier + let configuredAccessLevel: AccessLevelModifier? + let configuredForceEscaping: Bool + let effectiveTypeAnnotation: TypeAnnotationSyntax? + let initializer: ExprSyntax? + let isComputedProperty: Bool + let isTuplePattern: Bool + let keywordToken: TokenKind + let name: String? + private let _syntaxNode: Syntax + + init( + accessLevel: AccessLevelModifier, + adoptedType: TypeAnnotationSyntax?, + binding: PatternBindingSyntax, + configuredAccessLevel: AccessLevelModifier?, + configuredForceEscaping: Bool, + keywordToken: TokenKind + ) { + self.accessLevel = accessLevel + self.configuredAccessLevel = configuredAccessLevel + self.effectiveTypeAnnotation = binding.typeAnnotation ?? adoptedType + self.configuredForceEscaping = configuredForceEscaping + self.initializer = binding.initializer?.trimmed.value + self.isComputedProperty = binding.isComputedProperty + self.isTuplePattern = binding.pattern.isTuplePattern + self.keywordToken = keywordToken + self.name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text + self._syntaxNode = binding._syntaxNode + } + + var isPreinitializedVarWithoutType: Bool { + self.initializer != nil + && self.effectiveTypeAnnotation == nil + && self.keywordToken == .keyword(.var) + } + + var isPreinitializedLet: Bool { + self.initializer != nil && self.keywordToken == .keyword(.let) + } + + func diagnostic(message: MemberwiseInitMacroDiagnostic) -> Diagnostic { + Diagnostic(node: self._syntaxNode, message: message) + } + } + + struct MemberProperty { + let accessLevel: AccessLevelModifier + let configuredAccessLevel: AccessLevelModifier? + let configuredForceEscaping: Bool + let initializer: ExprSyntax? + let keywordToken: TokenKind + let name: String + let type: TypeSyntax + + init( + accessLevel: AccessLevelModifier, + configuredAccessLevel: AccessLevelModifier?, + configuredForceEscaping: Bool, + initializer: ExprSyntax?, + keywordToken: TokenKind, + name: String, + type: TypeSyntax + ) { + self.accessLevel = accessLevel + self.configuredAccessLevel = configuredAccessLevel + self.configuredForceEscaping = configuredForceEscaping + self.initializer = initializer + self.keywordToken = keywordToken + self.name = name + self.type = type + } + + var externalName: String { + self.name.hasPrefix("_") ? String(self.name.dropFirst()) : self.name + } + } + + public static func expansion( + of node: AttributeSyntax, + providingMembersOf decl: D, + in context: C + ) throws -> [SwiftSyntax.DeclSyntax] + where D: DeclGroupSyntax, C: MacroExpansionContext { + guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(decl.kind) else { + throw MemberwiseInitMacroDiagnostic.invalidDeclarationKind(decl) + } + let configuredAccessLevel: AccessLevelModifier? = extractConfiguredAccessLevel(from: node) + let optionalsDefaultNil: Bool = extractOptionalsDefaultNil(from: node) ?? false + + let (properties, diagnostics) = try collectMemberProperties(from: decl.memberBlock.members) + diagnostics.forEach { context.diagnose($0) } + + let accessLevel = NonEmptyArray( + head: configuredAccessLevel ?? .internal, + tail: properties.compactMap { $0.configuredAccessLevel ?? $0.accessLevel } + ).min() + + func formatParameters(from properties: [MemberProperty]) -> String { + guard !properties.isEmpty else { return "" } + + return "\n" + + properties + .map { property in + formatParameter( + from: property, + optionalsDefaultNil: optionalsDefaultNil + ) + } + .joined(separator: ",\n") + + "\n" + } + + let formattedInitSignature = "\n\(accessLevel) init(\(formatParameters(from: properties)))" + return [ + DeclSyntax( + try InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) { + CodeBlockItemListSyntax( + properties + .map(formatInitializerAssignmentStatement(from:)) + .map(CodeBlockItemSyntax.init(stringLiteral:)) + ) + } + ) + ] + } + + private static func extractConfiguredAccessLevel( + from node: AttributeSyntax + ) -> AccessLevelModifier? { + guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) + else { return nil } + + // NB: Search for the first argument who's name matches an access level name + return arguments.compactMap { labeledExprSyntax -> AccessLevelModifier? in + guard + let identifier = labeledExprSyntax.expression.as(MemberAccessExprSyntax.self)?.declName, + let accessLevel = AccessLevelModifier(rawValue: identifier.baseName.trimmedDescription) + else { return nil } + + return accessLevel + } + .first + } + + private static func extractOptionalsDefaultNil( + from node: AttributeSyntax + ) -> Bool? { + guard let arguments = node.arguments?.as(LabeledExprListSyntax.self) + else { return nil } + + return arguments.contains { labeledExprSyntax -> Bool in + guard + labeledExprSyntax.label?.text == "_optionalsDefaultNil", + let booleanLiteral = labeledExprSyntax.expression.as(BooleanLiteralExprSyntax.self) + else { return false } + + return booleanLiteral.literal.text == "true" + } + } + + private static func collectMemberProperties( + from decl: MemberBlockItemListSyntax + ) throws -> ([MemberProperty], [Diagnostic]) { + return + decl + .compactMap { (member: MemberBlockItemSyntax) -> VariableDeclSyntax? in + guard + let variable = member.decl.as(VariableDeclSyntax.self), + variable.attributes.isEmpty || variable.attributes.contains(attributeNamed: "Init"), + variable.modifiersExclude([.static, .lazy]) + else { return nil } + return variable + } + .flatMap { variable -> [PropertyBinding] in + variable.bindings + .reversed() + .reduce( + into: ( + bindings: [PropertyBinding](), + typeFromTrailingBinding: TypeAnnotationSyntax?.none + ) + ) { acc, binding in + let ( + configuredIgnore, + configuredAccessLevel, + configuredForceEscaping + ) = extractMemberConfiguration(from: variable) + + guard !configuredIgnore else { return } + + acc.bindings.append( + PropertyBinding( + accessLevel: variable.accessLevel, + adoptedType: binding.typeAnnotation == nil ? acc.typeFromTrailingBinding : nil, + binding: binding, + configuredAccessLevel: configuredAccessLevel, + configuredForceEscaping: configuredForceEscaping, + keywordToken: variable.bindingSpecifier.tokenKind + ) + ) + acc.typeFromTrailingBinding = binding.typeAnnotation ?? acc.typeFromTrailingBinding + } + .bindings + .reversed() + } + .reduce( + ( + [MemberProperty](), + [Diagnostic]() + ) + ) { acc, property in + let (properties, diagnostics) = acc + if property.isComputedProperty || property.isPreinitializedLet { + return (properties, diagnostics) + } + if property.isPreinitializedVarWithoutType { + return ( + properties, + diagnostics + [property.diagnostic(message: .missingExplicitTypeForVarProperty)] + ) + } + if property.isTuplePattern { + return ( + properties, + diagnostics + [property.diagnostic(message: .tupleDestructuringInProperty)] + ) + } + + guard + let name = property.name, + let effectiveTypeAnnotation = property.effectiveTypeAnnotation + else { return (properties, diagnostics) } + + let newProperty = MemberProperty( + accessLevel: property.accessLevel, + configuredAccessLevel: property.configuredAccessLevel, + configuredForceEscaping: property.configuredForceEscaping, + initializer: property.initializer, + keywordToken: property.keywordToken, + name: name, + type: effectiveTypeAnnotation.type.trimmed + ) + return (properties + [newProperty], diagnostics) + } + } + + private static func extractMemberConfiguration( + from variable: VariableDeclSyntax + ) -> (Bool, AccessLevelModifier?, Bool) { + let memberConfiguration = variable.attributes + .first(where: { + $0.as(AttributeSyntax.self)?.attributeName.trimmedDescription == "Init" + })? + .as(AttributeSyntax.self)? + .arguments? + .as(LabeledExprListSyntax.self) + + let configuredValues = + memberConfiguration?.compactMap { + $0.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.trimmedDescription + } ?? [] + + let configuredIgnore = configuredValues.contains("ignore") + let configuredForceEscaping = configuredValues.contains("escaping") + let configuredAccessLevel = + configuredValues + .compactMap(AccessLevelModifier.init(rawValue:)) + .first + + return (configuredIgnore, configuredAccessLevel, configuredForceEscaping) + } + + private static func formatParameter( + from property: MemberProperty, + optionalsDefaultNil: Bool + ) -> String { + let defaultValue = + property.initializer.map { " = \($0.description)" } + ?? (optionalsDefaultNil && property.type.isOptionalType ? " = nil" : "") + let escaping = + (property.configuredForceEscaping || property.type.isFunctionType) ? "@escaping " : "" + + return "\(property.externalName): \(escaping)\(property.type.description)\(defaultValue)" + } + + private static func formatInitializerAssignmentStatement( + from property: MemberProperty + ) -> String { + "self.\(property.name) = \(property.externalName)" + } +} + +private struct NonEmptyArray { + let head: Element + let tail: [Element] + + var allElements: [Element] { + return [head] + tail + } + + init(head: Element, tail: [Element]) { + self.head = head + self.tail = tail + } + + // func min(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> Element { + // return try allElements.min(by: areInIncreasingOrder)! + // } +} + +extension NonEmptyArray where Element: Comparable { + fileprivate func min() -> Element { + return allElements.min()! + } +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/AccesssLevelSyntax.swift b/Sources/MemberwiseInitMacros/Macros/Support/AccesssLevelSyntax.swift new file mode 100644 index 0000000..afc5dfe --- /dev/null +++ b/Sources/MemberwiseInitMacros/Macros/Support/AccesssLevelSyntax.swift @@ -0,0 +1,120 @@ +import SwiftSyntax + +// Modified from IanKeen's MacroKit: +// https://github.com/IanKeen/MacroKit/blob/main/Sources/MacroKitMacros/Support/AccessLevelSyntax.swift +// +// Modifications: +// - Declarations can have multiple access level modifiers, e.g. `public private(set)` +// I'm still not dealing with the "detail" (`set`) in any way +// - Apply Swift's rules concering default access when no access level is explicitly stated (it's not always `internal`): +// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol#Custom-Types +// - Added missing DeclGroupSyntax kinds + +// TODO: Rules for local DeclGroup's nested within a function definition +// TODO: Rules for local access functions? e.g. `func foo() { func bar() { … } … }` + +enum AccessLevelModifier: String, Comparable, CaseIterable, Sendable { + case `private` + case `fileprivate` + case `internal` + case `public` + case `open` + + var keyword: Keyword { + switch self { + case .private: return .private + case .fileprivate: return .fileprivate + case .internal: return .internal + case .public: return .public + case .open: return .open + } + } + + static func < (lhs: AccessLevelModifier, rhs: AccessLevelModifier) -> Bool { + let lhs = Self.allCases.firstIndex(of: lhs)! + let rhs = Self.allCases.firstIndex(of: rhs)! + return lhs < rhs + } +} + +public protocol AccessLevelSyntax { + var parent: Syntax? { get } + var modifiers: DeclModifierListSyntax { get set } +} + +extension AccessLevelSyntax { + var accessLevelModifiers: [AccessLevelModifier]? { + get { + let accessLevels = modifiers.lazy.compactMap { AccessLevelModifier(rawValue: $0.name.text) } + return accessLevels.isEmpty ? nil : Array(accessLevels) + } + set { + guard let newModifiers = newValue else { + modifiers = [] + return + } + let newModifierKeywords = newModifiers.map { DeclModifierSyntax(name: .keyword($0.keyword)) } + let filteredModifiers = modifiers.filter { + AccessLevelModifier(rawValue: $0.name.text) == nil + } + modifiers = filteredModifiers + newModifierKeywords + } + } +} + +protocol DeclGroupAccessLevelSyntax: AccessLevelSyntax { +} +extension DeclGroupAccessLevelSyntax { + public var accessLevel: AccessLevelModifier { + self.accessLevelModifiers?.first ?? .internal + } +} + +extension ActorDeclSyntax: DeclGroupAccessLevelSyntax {} +extension ClassDeclSyntax: DeclGroupAccessLevelSyntax {} +extension EnumDeclSyntax: DeclGroupAccessLevelSyntax {} +extension StructDeclSyntax: DeclGroupAccessLevelSyntax {} + +// NB: MemberwiseInit doesn't need this on FunctionDeclSyntax extension +//extension FunctionDeclSyntax: AccessLevelSyntax { +// public var accessLevel: AccessLevelModifier { +// get { +// // a decl (function, variable) can +// if let formalModifier = self.accessLevelModifiers?.first { +// return formalModifier +// } +// +// guard let parent = self.parent else { return .internal } +// +// if let parent = parent as? DeclGroupSyntax { +// return [parent.declAccessLevel, .internal].min()! +// } else { +// return .internal +// } +// } +// } +//} + +extension VariableDeclSyntax: AccessLevelSyntax { + var accessLevel: AccessLevelModifier { + // TODO: assuming the least access of the modifiers may not be correct, but it suits the special case of MemberwiseInit + // 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 + self.accessLevelModifiers?.min() ?? inferDefaultAccessLevel(node: self._syntaxNode) + } +} + +private func inferDefaultAccessLevel(node: Syntax?) -> AccessLevelModifier { + guard let node else { return .internal } + guard let decl = node.asProtocol(DeclGroupSyntax.self) else { + return inferDefaultAccessLevel(node: node.parent) + } + + return [decl.declAccessLevel, .internal].min()! +} + +// NB: This extension is sugar to avoid user needing to first cast to a specific kind of decl group syntax +extension DeclGroupSyntax { + var declAccessLevel: AccessLevelModifier { + (self as? DeclGroupAccessLevelSyntax)!.accessLevel + } +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift new file mode 100644 index 0000000..5c52032 --- /dev/null +++ b/Sources/MemberwiseInitMacros/Macros/Support/Diagnostics.swift @@ -0,0 +1,67 @@ +import SwiftDiagnostics +import SwiftSyntax + +enum MemberwiseInitMacroDiagnostic: Error, DiagnosticMessage { + case invalidDeclarationKind(DeclGroupSyntax) + case missingExplicitTypeForVarProperty + case tupleDestructuringInProperty + + private var rawValue: String { + switch self { + case .invalidDeclarationKind(let declGroup): + ".invalidDeclarationKind(\(declGroup.kind))" + + case .missingExplicitTypeForVarProperty: + ".missingExplicitTypeForVarProperty" + + case .tupleDestructuringInProperty: + ".tupleUsedInProperty" + } + } + + var severity: DiagnosticSeverity { .error } + + var message: String { + switch self { + case .invalidDeclarationKind(let declGroup): + return """ + @MemberwiseInit can only be attached to a struct, class, or actor; \ + not to \(declGroup.descriptiveDeclKind(withArticle: true)). + """ + + case .missingExplicitTypeForVarProperty: + return "@MemberwiseInit requires explicit type declarations for `var` stored properties." + + case .tupleDestructuringInProperty: + return """ + @MemberwiseInit does not support tuple destructuring for property declarations. \ + Use multiple declarations instead. + """ + } + } + + var diagnosticID: MessageID { + .init(domain: "MemberwiseInitMacro", id: rawValue) + } +} + +extension DeclGroupSyntax { + func descriptiveDeclKind(withArticle article: Bool = false) -> String { + switch self { + case is ActorDeclSyntax: + return article ? "an actor" : "actor" + case is ClassDeclSyntax: + return article ? "a class" : "class" + case is ExtensionDeclSyntax: + return article ? "an extension" : "extension" + case is ProtocolDeclSyntax: + return article ? "a protocol" : "protocol" + case is StructDeclSyntax: + return article ? "a struct" : "struct" + case is EnumDeclSyntax: + return article ? "an enum" : "enum" + default: + return "`\(self.kind)`" + } + } +} diff --git a/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift new file mode 100644 index 0000000..23c6bcc --- /dev/null +++ b/Sources/MemberwiseInitMacros/Macros/Support/SyntaxHelpers.swift @@ -0,0 +1,61 @@ +import SwiftSyntax + +extension AttributeListSyntax { + func contains(attributeNamed name: String) -> Bool { + return self.contains { + $0.as(AttributeSyntax.self)?.attributeName.trimmedDescription == name + } + } +} + +extension VariableDeclSyntax { + func modifiersExclude(_ keywords: [Keyword]) -> Bool { + return !self.modifiers.containsAny(of: keywords.map { TokenSyntax.keyword($0) }) + } +} + +extension DeclModifierListSyntax { + fileprivate func containsAny(of tokens: [TokenSyntax]) -> Bool { + return self.contains { modifier in + tokens.contains { $0.text == modifier.name.text } + } + } +} + +extension PatternBindingSyntax { + var isComputedProperty: Bool { + guard let accessors = self.accessorBlock?.accessors else { return false } + + switch accessors { + case .accessors(let accessors): + let tokenKinds = accessors.compactMap { $0.accessorSpecifier.tokenKind } + let propertyObservers: [TokenKind] = [.keyword(.didSet), .keyword(.willSet)] + + return !tokenKinds.allSatisfy(propertyObservers.contains) + + case .getter(_): + return true + } + } +} + +extension TypeSyntax { + var isFunctionType: Bool { + // NB: Check for `FunctionTypeSyntax` directly or when wrapped within `AttributedTypeSyntax`, + // e.g., `@Sendable () -> Void`. + return self.is(FunctionTypeSyntax.self) + || (self.as(AttributedTypeSyntax.self)?.baseType.is(FunctionTypeSyntax.self) ?? false) + } +} + +extension TypeSyntax { + var isOptionalType: Bool { + self.as(OptionalTypeSyntax.self) != nil + } +} + +extension PatternSyntax { + var isTuplePattern: Bool { + self.as(TuplePatternSyntax.self) != nil + } +} diff --git a/Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift b/Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift new file mode 100644 index 0000000..1e0ba34 --- /dev/null +++ b/Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift @@ -0,0 +1,2668 @@ +// This file is automatically generated by '../../bin/generate_access_level_tests.sh'. +// Do not edit this file directly. + +import MacroTesting +import MemberwiseInitMacros +import SwiftSyntaxMacros +import XCTest + +final class MemberwiseInitAccessLevelTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + // isRecording: true, + macros: [ + "MemberwiseInit": MemberwiseInitMacro.self, + "Init": InitMacro.self, + ] + ) { + super.invokeTest() + } + } + + func testMemberwiseInitPrivate_PrivateStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PrivateStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + private struct S { + } + """ + } expansion: { + """ + private struct S { + + private init() { + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_DefaultStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + struct S { + } + """ + } expansion: { + """ + struct S { + + private init() { + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPrivate_PublicStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.private) + public struct S { + } + """ + } expansion: { + """ + public struct S { + + private init() { + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PrivateStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit + private struct S { + } + """ + } expansion: { + """ + private struct S { + + internal init() { + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_DefaultStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit + struct S { + } + """ + } expansion: { + """ + struct S { + + internal init() { + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitDefault_PublicStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit + public struct S { + } + """ + } expansion: { + """ + public struct S { + + internal init() { + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + private struct S { + private let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + private struct S { + let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + private struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PrivateStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + private struct S { + } + """ + } expansion: { + """ + private struct S { + + public init() { + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + struct S { + private let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + struct S { + let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_DefaultStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + struct S { + } + """ + } expansion: { + """ + struct S { + + public init() { + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPrivate_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.private) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitInternal_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.internal) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPublic_PrivateProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.public) private let v: T + } + """ + } expansion: { + """ + public struct S { + private let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPrivate_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.private) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitInternal_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.internal) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPublic_DefaultProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.public) let v: T + } + """ + } expansion: { + """ + public struct S { + let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPrivate_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.private) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + private init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitInternal_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.internal) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + internal init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_InitPublic_PublicProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + @Init(.public) public let v: T + } + """ + } expansion: { + """ + public struct S { + public let v: T + + public init( + v: T + ) { + self.v = v + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_NoProperty() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct S { + } + """ + } expansion: { + """ + public struct S { + + public init() { + } + } + """ + } + } + +} diff --git a/Tests/MemberwiseInitTests/MemberwiseInitTests.swift b/Tests/MemberwiseInitTests/MemberwiseInitTests.swift new file mode 100644 index 0000000..d7749c6 --- /dev/null +++ b/Tests/MemberwiseInitTests/MemberwiseInitTests.swift @@ -0,0 +1,1764 @@ +import MacroTesting +import MemberwiseInitMacros +import SwiftSyntaxMacros +import XCTest + +// TODO: cover valid `open` usages (on class and member decl) +// TODO: consider exhaustive coverage for multiple-properties on a struct +// TODO: warn when `@Init(.private) is applied to reduce access, e.g. `public let v: T`? + +final class MemberwiseInitTests: XCTestCase { + override func invokeTest() { + // TODO: PR swift-macro-testing to support explicit indentationWidth Trivia in assertMacro/withMacroTesting + // Waiting for: https://github.com/pointfreeco/swift-macro-testing/pull/8 + withMacroTesting( + indentationWidth: .spaces(2), + macros: [ + "MemberwiseInit": MemberwiseInitMacro.self, + "Init": InitMacro.self, + ] + ) { + super.invokeTest() + } + } + + // MARK: - Test simple usage + + func testEmptyStruct() { + assertMacro { + """ + @MemberwiseInit + struct Person { + } + """ + } expansion: { + """ + struct Person { + + internal init() { + } + } + """ + } + } + + func testEmptyPublicStruct() { + assertMacro { + """ + @MemberwiseInit + public struct Person { + } + """ + } expansion: { + """ + public struct Person { + + internal init() { + } + } + """ + } + } + + func testLetProperty() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let name: String + } + """ + } expansion: { + """ + struct Person { + let name: String + + internal init( + name: String + ) { + self.name = name + } + } + """ + } + } + + func testOptionlProperty() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let nickname: String? + } + """ + } expansion: { + """ + struct Person { + let nickname: String? + + internal init( + nickname: String? + ) { + self.nickname = nickname + } + } + """ + } + } + + func testUnderscorePrefixConvention() { + assertMacro { + """ + @MemberwiseInit + struct Person { + let _name: String + } + """ + } expansion: { + """ + struct Person { + let _name: String + + internal init( + name: String + ) { + self._name = name + } + } + """ + } + } + + // MARK: - Test assignment variations + + func testVarProperty() throws { + assertMacro { + """ + @MemberwiseInit + struct Pedometer { + var stepsToday: Int + } + """ + } expansion: { + """ + struct Pedometer { + var stepsToday: Int + + internal init( + stepsToday: Int + ) { + self.stepsToday = stepsToday + } + } + """ + } + } + + func testLetWithInitializer_IsIgnored() { + assertMacro { + """ + @MemberwiseInit + public struct Earth { + private let radiusInMiles: Float = 3958.8 + } + """ + } expansion: { + """ + public struct Earth { + private let radiusInMiles: Float = 3958.8 + + internal init() { + } + } + """ + } + } + + func testLetMarkedInitWithInitializer_IsIgnored() throws { + assertMacro { + """ + @MemberwiseInit + struct Earth { + @Init let name = "Earth" + } + """ + } expansion: { + """ + struct Earth { + let name = "Earth" + + internal init() { + } + } + """ + } + } + + func testVarWithInitializer_IsIncludedWithDefaultValue() throws { + assertMacro { + """ + @MemberwiseInit + struct Pedometer { + var stepsToday: Int = 0 + } + """ + } expansion: { + """ + struct Pedometer { + var stepsToday: Int = 0 + + internal init( + stepsToday: Int = 0 + ) { + self.stepsToday = stepsToday + } + } + """ + } + } + + func testVarPropertyWithInitializerWithoutExplicitType_FailsWithDiagnostic() { + assertMacro { + """ + @MemberwiseInit + public struct Pedometer { + var stepsToday = 0 + } + """ + } diagnostics: { + """ + @MemberwiseInit + public struct Pedometer { + var stepsToday = 0 + ┬───────────── + ╰─ 🛑 @MemberwiseInit requires explicit type declarations for `var` stored properties. + } + """ + } + } + + func testInlineCommentOnPropertyHasNoEffect() throws { + assertMacro { + """ + @MemberwiseInit + struct Pedometer { + let stepsToday: Int // number of steps taken today + } + """ + } expansion: { + """ + struct Pedometer { + let stepsToday: Int // number of steps taken today + + internal init( + stepsToday: Int + ) { + self.stepsToday = stepsToday + } + } + """ + } + } + + // MARK: - Test automatic @escaping + + func testAutomaticEscaping() throws { + assertMacro { + """ + @MemberwiseInit + struct APIRequest: Sendable { + let onSuccess: (Data) -> Void + let onFailure: @MainActor @Sendable (Error) -> Void + } + """ + } expansion: { + """ + struct APIRequest: Sendable { + let onSuccess: (Data) -> Void + let onFailure: @MainActor @Sendable (Error) -> Void + + internal init( + onSuccess: @escaping (Data) -> Void, + onFailure: @escaping @MainActor @Sendable (Error) -> Void + ) { + self.onSuccess = onSuccess + self.onFailure = onFailure + } + } + """ + } + } + + // MARK: - Test binding variations + + func testLetHavingTwoBindings() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let firstName: String, lastName: String + } + """ + } expansion: { + """ + struct Person { + let firstName: String, lastName: String + + internal init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + func testLetHavingTwoBindingsWhereFirstInitialized() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let firstName: String = "", lastName: String + } + """ + } expansion: { + """ + struct Person { + let firstName: String = "", lastName: String + + internal init( + lastName: String + ) { + self.lastName = lastName + } + } + """ + } + } + + func testLetHavingTwoBindingsWhereFirstInitializedWithoutExplicitType() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let firstName = "", lastName: String + } + """ + } expansion: { + """ + struct Person { + let firstName = "", lastName: String + + internal init( + lastName: String + ) { + self.lastName = lastName + } + } + """ + } + } + + func testLetHavingTwoBindingsWhereFirstLacksExplicitType() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let firstName, lastName: String + } + """ + } expansion: { + """ + struct Person { + let firstName, lastName: String + + internal init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + func testLetHavingTwoBindingsWhereLastInitialized() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let firstName: String, lastName: String = "" + } + """ + } expansion: { + """ + struct Person { + let firstName: String, lastName: String = "" + + internal init( + firstName: String + ) { + self.firstName = firstName + } + } + """ + } + } + + func testLetStatementHavingThreeBindingsWhereMiddleLacksExplicitType() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let name: String, age, favoriteNumber: Int + } + """ + } expansion: { + """ + struct Person { + let name: String, age, favoriteNumber: Int + + internal init( + name: String, + age: Int, + favoriteNumber: Int + ) { + self.name = name + self.age = age + self.favoriteNumber = favoriteNumber + } + } + """ + } + } + + func testLetStatementHavingThreeBindingsWhereOnlyLastHasExplicitFunctionType_AllEscaping() throws + { + assertMacro { + """ + @MemberwiseInit + struct Person { + let say, whisper, yell: () -> Void + } + """ + } expansion: { + """ + struct Person { + let say, whisper, yell: () -> Void + + internal init( + say: @escaping () -> Void, + whisper: @escaping () -> Void, + yell: @escaping () -> Void + ) { + self.say = say + self.whisper = whisper + self.yell = yell + } + } + """ + } + } + + // MARK: - Test destructured tuples + + // TODO: @MemberwiseInit should support tuple destructuring for property declarations + // func testLetDestructuredTupleWithoutInitializer() throws { + // assertMacro { + // """ + // @MemberwiseInit + // struct Point2D { + // let (x, y): (Int, Int) + // } + // """ + // } expansion: { + // """ + // struct Point2D { + // let (x, y): (Int, Int) + // + // internal init( + // x: Int, + // y: Int + // ) { + // self.x = x + // self.y = y + // } + // } + // """ + // } + // } + + // NB: @MemberwiseInit does not support tuple destructuring for property declarations, but + // already initialized `let` properties are ignored, so the tuple can be ignored. + func testLetDestructuredTupleWithInitializer() throws { + assertMacro { + """ + @MemberwiseInit + struct Point2D { + let (defaultX, defaultY): (Int, Int) = (0, 0) + } + """ + } expansion: { + """ + struct Point2D { + let (defaultX, defaultY): (Int, Int) = (0, 0) + + internal init() { + } + } + """ + } + } + + func testLetDestructuredTupleWithoutInitializer_FailsNotSupported() throws { + assertMacro { + """ + @MemberwiseInit + struct Point2D { + let (x, y): (Int, Int) + } + """ + } diagnostics: { + """ + @MemberwiseInit + struct Point2D { + let (x, y): (Int, Int) + ┬───────────────── + ╰─ 🛑 @MemberwiseInit does not support tuple destructuring for property declarations. Use multiple declarations instead. + } + """ + } + } + + func testVarDestructuredTupleWithInitializer_FailsNotSupported() throws { + assertMacro { + """ + @MemberwiseInit + struct Point2D { + var (x, y): (Int, Int) = (0, 0) + } + """ + } diagnostics: { + """ + @MemberwiseInit + struct Point2D { + var (x, y): (Int, Int) = (0, 0) + ┬────────────────────────── + ╰─ 🛑 @MemberwiseInit does not support tuple destructuring for property declarations. Use multiple declarations instead. + } + """ + } + } + + // MARK: - Test enum and extension + + func testAppliedToEnum_FailsWithDiagnostic() throws { + assertMacro { + """ + @MemberwiseInit + enum Action { + } + """ + } diagnostics: { + """ + @MemberwiseInit + ┬────────────── + ╰─ 🛑 @MemberwiseInit can only be attached to a struct, class, or actor; not to an enum. + enum Action { + } + """ + } + } + + func testAppliedToExtension_FailsWithDiagnostic() throws { + assertMacro { + """ + @MemberwiseInit + extension Int { + var isGoodNumber: Bool { + true + } + } + """ + } diagnostics: { + """ + @MemberwiseInit + ┬────────────── + ╰─ 🛑 @MemberwiseInit can only be attached to a struct, class, or actor; not to an extension. + extension Int { + var isGoodNumber: Bool { + true + } + } + """ + } + } + + // MARK: - Test computed properties + + func testComputedProperty_IsIgnored() { + assertMacro { + """ + @MemberwiseInit + struct Person { + var hasGoodFavoriteNumber: Bool { + true + } + } + """ + } expansion: { + """ + struct Person { + var hasGoodFavoriteNumber: Bool { + true + } + + internal init() { + } + } + """ + } + } + + func testGetterOnlyComputedProperty_IsIgnored() { + assertMacro { + """ + @MemberwiseInit + struct Person { + var name: String { + get { + return "John Doe" + } + } + } + """ + } expansion: { + """ + struct Person { + var name: String { + get { + return "John Doe" + } + } + + internal init() { + } + } + """ + } + } + + func testGetterSetterComputedProperty_IsIgnored() { + assertMacro { + """ + @MemberwiseInit + struct Person { + var firstName: String + var lastName: String + var fullName: String { + get { + return "\\(firstName) \\(lastName)" + } + set { + let nameParts = newValue.split(separator: " ") + firstName = String(nameParts[0]) + lastName = String(nameParts[1]) + } + } + } + """ + } expansion: { + #""" + struct Person { + var firstName: String + var lastName: String + var fullName: String { + get { + return "\(firstName) \(lastName)" + } + set { + let nameParts = newValue.split(separator: " ") + firstName = String(nameParts[0]) + lastName = String(nameParts[1]) + } + } + + internal init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """# + } + } + + // MARK: - Test annotations and attributes + + func testMacroAdjacentAttribute() throws { + assertMacro { + """ + @MemberwiseInit @available(iOS 15, *) + struct Person { + let name: String + } + """ + } expansion: { + """ + @available(iOS 15, *) + struct Person { + let name: String + + internal init( + name: String + ) { + self.name = name + } + } + """ + } + } + + func testPropertyWithAttribute_IsIgnored() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct MyView { + @State private var isOn: Bool + } + """ + } expansion: { + """ + public struct MyView { + @State private var isOn: Bool + + public init() { + } + } + """ + } + } + + // NB: Separating attributes on multiple lines to work around peculiar SwiftSyntaxMacroExpansion + // trivia handling. + // Waiting for: https://github.com/apple/swift-syntax/pull/2215 + func testPropertyWithInitAndAttribute_IsIncluded() { + assertMacro { + """ + @MemberwiseInit + struct MyView { + @Init + @State + var isOn: Bool + } + """ + } expansion: { + """ + struct MyView { + @State + var isOn: Bool + + internal init( + isOn: Bool + ) { + self.isOn = isOn + } + } + """ + } + } + + // NB: Separating attributes on multiple lines to work around peculiar SwiftSyntaxMacroExpansion + // trivia handling. + // Waiting for: https://github.com/apple/swift-syntax/pull/2215 + func testPropertyWithAttributeAndInit_IsIncluded() { + assertMacro { + """ + @MemberwiseInit + struct MyView { + @State + @Init + var isOn: Bool + } + """ + } expansion: { + """ + struct MyView { + @State + var isOn: Bool + + internal init( + isOn: Bool + ) { + self.isOn = isOn + } + } + """ + } + } + + // NB: Separating attributes on multiple lines to work around peculiar SwiftSyntaxMacroExpansion + // trivia handling. + // Waiting for: https://github.com/apple/swift-syntax/pull/2215 + func testPropertyWithAttributeAndInitArgs_IsIncludedArgsApplied() { + assertMacro { + """ + @MemberwiseInit(.public) + struct MyView { + @State + @Init(.public) + var isOn: Bool + } + """ + } expansion: { + """ + struct MyView { + @State + var isOn: Bool + + public init( + isOn: Bool + ) { + self.isOn = isOn + } + } + """ + } + } + + // NB: Separating attributes on multiple lines to work around peculiar SwiftSyntaxMacroExpansion + // trivia handling. + // Waiting for: https://github.com/apple/swift-syntax/pull/2215 + func testPropertyWithInitEmptyParensAndAttribute_IsIncluded() { + assertMacro { + """ + @MemberwiseInit + struct MyView { + @Init() + @State + var isOn: Bool + } + """ + } expansion: { + """ + struct MyView { + @State + var isOn: Bool + + internal init( + isOn: Bool + ) { + self.isOn = isOn + } + } + """ + } + } + + func + testMemberwiseInitInternalOnPublicStruct_InitPublicEscapingOnPrivateProperty_InternalInitEscaping() + { + assertMacro { + """ + @MemberwiseInit(.internal) + public struct Person { + @Init(.public, .escaping) private let _name: String + } + """ + } expansion: { + """ + public struct Person { + private let _name: String + + internal init( + name: @escaping String + ) { + self._name = name + } + } + """ + } + } + + // MARK: - Test invalid syntax + + func testInvalidLetProperty_NoExcessiveDiagnostic() { + assertMacro { + """ + @MemberwiseInit + struct Person { + let name + } + """ + } expansion: { + """ + struct Person { + let name + + internal init() { + } + } + """ + } + } + + func testInvalidVarProperty_NoExcessiveDiagnostic() { + assertMacro { + """ + @MemberwiseInit + struct Person { + var name + } + """ + } expansion: { + """ + struct Person { + var name + + internal init() { + } + } + """ + } + } + + // MARK: - Test init access level + + func testInitAccessLevelBaseline_MatchesAnnotationTarget() { + assertMacro { + """ + @MemberwiseInit + private struct Person { + } + + @MemberwiseInit + fileprivate struct Person { + } + + @MemberwiseInit + struct Person { + } + + @MemberwiseInit + internal struct Person { + } + + @MemberwiseInit + public struct Person { + } + + @MemberwiseInit + open class Person { + } + """ + } expansion: { + """ + private struct Person { + + internal init() { + } + } + fileprivate struct Person { + + internal init() { + } + } + struct Person { + + internal init() { + } + } + internal struct Person { + + internal init() { + } + } + public struct Person { + + internal init() { + } + } + open class Person { + + internal init() { + } + } + """ + } + } + + // NB: This is almost covered by the exhaustive AccessLevelTests, but `open class Person` + // is missing. This test touches on all the access levels (instead of a meaningful few). + func testDefaultInitAccessLeves() { + assertMacro { + """ + @MemberwiseInit + private struct Person { + private let name: String + } + + @MemberwiseInit + fileprivate struct Person { + private let name: String + } + + @MemberwiseInit + struct Person { + fileprivate let name: String + } + + @MemberwiseInit + internal struct Person { + fileprivate let name: String + } + + @MemberwiseInit + public struct Person { + let name: String + } + + @MemberwiseInit + open class Person { + public var name: String + } + """ + } expansion: { + """ + private struct Person { + private let name: String + + private init( + name: String + ) { + self.name = name + } + } + fileprivate struct Person { + private let name: String + + private init( + name: String + ) { + self.name = name + } + } + struct Person { + fileprivate let name: String + + fileprivate init( + name: String + ) { + self.name = name + } + } + internal struct Person { + fileprivate let name: String + + fileprivate init( + name: String + ) { + self.name = name + } + } + public struct Person { + let name: String + + internal init( + name: String + ) { + self.name = name + } + } + open class Person { + public var name: String + + internal init( + name: String + ) { + self.name = name + } + } + """ + } + } + + func testMemberwiseInitPublic_PublicStruct_PublicAndImplicitlyInternalProperties_InternalInit() + throws + { + assertMacro { + """ + @MemberwiseInit(.public) + public struct Person { + public let firstName: String + let lastName: String + } + """ + } expansion: { + """ + public struct Person { + public let firstName: String + let lastName: String + + internal init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + func testPublicStruct_PublicAndFileprivateProperty_FileprivateInit() throws { + assertMacro { + """ + @MemberwiseInit + public struct Person { + public let firstName: String + fileprivate let lastName: String + } + """ + } expansion: { + """ + public struct Person { + public let firstName: String + fileprivate let lastName: String + + fileprivate init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + func testPublicStruct_PublicAndPrivateProperty_PrivateInit() throws { + assertMacro { + """ + @MemberwiseInit + public struct Person { + public let firstName: String + private let lastName: String + } + """ + } expansion: { + """ + public struct Person { + public let firstName: String + private let lastName: String + + private init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + func testImplicitlyInternalStructWithPublicAndPrivateProperty_PrivateInit() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + public let firstName: String + private let lastName: String + } + """ + } expansion: { + """ + struct Person { + public let firstName: String + private let lastName: String + + private init( + firstName: String, + lastName: String + ) { + self.firstName = firstName + self.lastName = lastName + } + } + """ + } + } + + // NB: The compiler's member-wise init has the same behavior. + func testPublicStructWithPreinitializedPrivateLet_InternalInit() throws { + assertMacro { + """ + @MemberwiseInit + public struct Person { + private let lastName: String = "" + } + """ + } expansion: { + """ + public struct Person { + private let lastName: String = "" + + internal init() { + } + } + """ + } + } + + func testMemberwiseInitPubic_PublicStructWithPreinitializedPrivateLet_PublicInit() throws { + assertMacro { + """ + @MemberwiseInit(.public) + public struct Person { + private let lastName: String = "" + } + """ + } expansion: { + """ + public struct Person { + private let lastName: String = "" + + public init() { + } + } + """ + } + } + + func testPublicFinalClass_InternalInit() { + assertMacro { + """ + @MemberwiseInit + public final class Person { + } + """ + } expansion: { + """ + public final class Person { + + internal init() { + } + } + """ + } + } + + func testPrivateSetProperty_IsIncludedPrivateInit() throws { + assertMacro { + """ + @MemberwiseInit + struct Pedometer { + private(set) var stepsToday: Int + } + """ + } expansion: { + """ + struct Pedometer { + private(set) var stepsToday: Int + + private init( + stepsToday: Int + ) { + self.stepsToday = stepsToday + } + } + """ + } + } + + func testNonInternalDefaultAccess() throws { + assertMacro { + """ + struct S { + @MemberwiseInit + private struct T { + let v: Int + } + } + """ + } expansion: { + """ + struct S { + private struct T { + let v: Int + + private init( + v: Int + ) { + self.v = v + } + } + } + """ + } + } + + // MARK: - Test macro parameters + + func testInitParameterIgnore() throws { + assertMacro { + """ + @MemberwiseInit(.public) + public struct Person { + @Init(.public) let name: String + @Init(.ignore) private var age: Int? = nil + } + """ + } expansion: { + """ + public struct Person { + let name: String + private var age: Int? = nil + + public init( + name: String + ) { + self.name = name + } + } + """ + } + } + + // TODO: This test is extremely similar to `testLetWithInitializer_IsIgnored`, just with @Init. Bring them together? + // TODO: emit diagnostic on "@Init" when applied nonsensically + func testInitOnLetWithInitializer_IsIgnored() throws { + assertMacro { + """ + @MemberwiseInit + public struct Earth { + @Init private let radiusInMiles: Float = 3958.8 + } + """ + } expansion: { + """ + public struct Earth { + private let radiusInMiles: Float = 3958.8 + + internal init() { + } + } + """ + } + } + + // NB: @MemberwiseInit cannot that see the typealias's underlying type is a closure type that + // needs to be escaped. + func testExplicitInitParameterEscaping() throws { + assertMacro { + """ + typealias LoggingMechanism = @Sendable (String) -> Void + + @MemberwiseInit + struct TaskRunner: Sendable { + @Init(.escaping) let log: LoggingMechanism + } + """ + } expansion: { + """ + typealias LoggingMechanism = @Sendable (String) -> Void + struct TaskRunner: Sendable { + let log: LoggingMechanism + + internal init( + log: @escaping LoggingMechanism + ) { + self.log = log + } + } + """ + } + } + + func testExplicitInitParameterEscapingForMultiple() throws { + assertMacro { + """ + typealias LoggingMechanism = @Sendable (String) -> Void + typealias CompletionHandler = @Sendable () -> Void + + @MemberwiseInit + struct TaskRunner: Sendable { + @Init(.escaping) let log: LoggingMechanism + @Init(.escaping) let onCompletion: CompletionHandler + } + """ + } expansion: { + """ + typealias LoggingMechanism = @Sendable (String) -> Void + typealias CompletionHandler = @Sendable () -> Void + struct TaskRunner: Sendable { + let log: LoggingMechanism + let onCompletion: CompletionHandler + + internal init( + log: @escaping LoggingMechanism, + onCompletion: @escaping CompletionHandler + ) { + self.log = log + self.onCompletion = onCompletion + } + } + """ + } + } + + // NB: @MemberwiseInit can't validate Swift syntax. Here, '@escaping' is misapplied, and the + // compiler will generate a helpful diagnostic error message. + func testIncorrectUseOfExplicitEscaping_SucceedsWithInvalidCode() throws { + assertMacro { + """ + @MemberwiseInit + struct Config: Sendable { + @Init(.escaping) let version: Int + } + """ + } expansion: { + """ + struct Config: Sendable { + let version: Int + + internal init( + version: @escaping Int + ) { + self.version = version + } + } + """ + } + } + + func testOptionalsDefaultNilFalse_AssignsNoDefaultToOptional() throws { + assertMacro { + """ + @MemberwiseInit(_optionalsDefaultNil: false) + struct Product { + let discountCode: String? + } + """ + } expansion: { + """ + struct Product { + let discountCode: String? + + internal init( + discountCode: String? + ) { + self.discountCode = discountCode + } + } + """ + } + } + + func testOptionalsDefaultNilTrueWithExplicitEscaping() throws { + assertMacro { + """ + typealias TaskCallback = (String) -> Void + + @MemberwiseInit(_optionalsDefaultNil: true) + struct TaskRunner { + @Init(.escaping) let completionHandler: TaskCallback + let retryCount: Int? + } + """ + } expansion: { + """ + typealias TaskCallback = (String) -> Void + struct TaskRunner { + let completionHandler: TaskCallback + let retryCount: Int? + + internal init( + completionHandler: @escaping TaskCallback, + retryCount: Int? = nil + ) { + self.completionHandler = completionHandler + self.retryCount = retryCount + } + } + """ + } + } + + func testOptionalsDefaultNilTrue_ParameterValueDefaultsNil() throws { + assertMacro { + """ + @MemberwiseInit(_optionalsDefaultNil: true) + struct Person { + let nickname: String? + } + """ + } expansion: { + """ + struct Person { + let nickname: String? + + internal init( + nickname: String? = nil + ) { + self.nickname = nickname + } + } + """ + } + } + + // MARK: - Test complex usage + + func testNestedStructs() throws { + assertMacro { + """ + @MemberwiseInit + struct Person { + let name: String + let address: Address + + @MemberwiseInit + struct Address { + let city: String + let state: String + } + } + """ + } expansion: { + """ + struct Person { + let name: String + let address: Address + struct Address { + let city: String + let state: String + + internal init( + city: String, + state: String + ) { + self.city = city + self.state = state + } + } + + internal init( + name: String, + address: Address + ) { + self.name = name + self.address = address + } + } + """ + } + } + + // NB: This is the only case where attaching @MemberwiseInit multiple times is useful, and + // most cases of multiple attachement are invalid or nonsensical. + // TODO: Generate a helpful diagnostic error message when multiple attachment is nonsensical. + func testAttachedMultipleTimes() throws { + assertMacro { + """ + @MemberwiseInit(.public) + @MemberwiseInit(.internal, _optionalsDefaultNil: true) + public struct Person { + @Init(.public) let name: String? + } + """ + } expansion: { + """ + public struct Person { + let name: String? + + public init( + name: String? + ) { + self.name = name + } + + internal init( + name: String? = nil + ) { + self.name = name + } + } + """ + } + } + + // https://github.com/tgrapperon/swift-dependencies-additions/blob/main/Sources/UserDefaultsDependency/UserDefaultsDependency.swift + func testComplexProtocolWitnessDependency() { + assertMacro { + """ + @MemberwiseInit(.public) + public struct Dependency: Sendable { + public let _get: @Sendable (_ key: String, _ type: Any.Type) -> (any Sendable)? + public let _set: @Sendable (_ value: (any Sendable)?, _ key: String) -> Void + public let _values: @Sendable (_ key: String, _ value: Any.Type) -> AsyncStream<(any Sendable)?> + } + """ + } expansion: { + """ + public struct Dependency: Sendable { + public let _get: @Sendable (_ key: String, _ type: Any.Type) -> (any Sendable)? + public let _set: @Sendable (_ value: (any Sendable)?, _ key: String) -> Void + public let _values: @Sendable (_ key: String, _ value: Any.Type) -> AsyncStream<(any Sendable)?> + + public init( + get: @escaping @Sendable (_ key: String, _ type: Any.Type) -> (any Sendable)?, + set: @escaping @Sendable (_ value: (any Sendable)?, _ key: String) -> Void, + values: @escaping @Sendable (_ key: String, _ value: Any.Type) -> AsyncStream<(any Sendable)?> + ) { + self._get = get + self._set = set + self._values = values + } + } + """ + } + } + + // TODO: Consider SE-0400: Init Accessors: + // https://github.com/apple/swift-evolution/blob/main/proposals/0400-init-accessors.md#init-accessors + // + // - May need something like `@MemberwiseInit(properties: ["title", "text"])` to cause the to + // generate `init(title: String, text: String)`. + func testInitAccessor() { + assertMacro { + """ + @MemberwiseInit + struct Angle { + var degrees: Double + var radians: Double { + @storageRestrictions(initializes: degrees) + init(initialValue) { + degrees = initialValue * 180 / .pi + } + + get { degrees * .pi / 180 } + set { degrees = newValue * 180 / .pi } + } + + init(radiansParam: Double) { + self.radians = radiansParam + } + } + """ + } expansion: { + """ + struct Angle { + var degrees: Double + var radians: Double { + @storageRestrictions(initializes: degrees) + init(initialValue) { + degrees = initialValue * 180 / .pi + } + + get { degrees * .pi / 180 } + set { degrees = newValue * 180 / .pi } + } + + init(radiansParam: Double) { + self.radians = radiansParam + } + + internal init( + degrees: Double + ) { + self.degrees = degrees + } + } + """ + } + } + + // MARK: - Test class property variations + + // Lazy properties must be declared with an initializer. + // + // A `lazy var` property shouldn't be initialized in an `init` method, as the purpose of + // `lazy var` is to defer initialization until some time after the instance itself is initialized. + // Immediately initializing the property defeats this purpose. + func testLazyVarInClass_IsIgnored() { + assertMacro { + """ + @MemberwiseInit + class Calculator { + lazy var lastResult: Double = 0.0 + } + """ + } expansion: { + """ + class Calculator { + lazy var lastResult: Double = 0.0 + + internal init() { + } + } + """ + } + } + + func testStaticProperties_AreIgnored() throws { + assertMacro { + """ + @MemberwiseInit + struct Coordinate { + static let originX: Int = 0 + static var displayGrid: Bool = true + } + """ + } expansion: { + """ + struct Coordinate { + static let originX: Int = 0 + static var displayGrid: Bool = true + + internal init() { + } + } + """ + } + } + + func testStoredPropertyWithObservers_IsIncluded() { + assertMacro { + """ + @MemberwiseInit + struct Person { + var age: Int { + willSet { print("Will set to \\(newValue).") } + didSet { ageLabel.text = "Age: \\(age)" } + } + } + """ + } expansion: { + #""" + struct Person { + var age: Int { + willSet { print("Will set to \(newValue).") } + didSet { ageLabel.text = "Age: \(age)" } + } + + internal init( + age: Int + ) { + self.age = age + } + } + """# + } + } +} diff --git a/bin/generate_access_level_tests.sh b/bin/generate_access_level_tests.sh new file mode 100755 index 0000000..11ed50c --- /dev/null +++ b/bin/generate_access_level_tests.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Define the output file location +output_file="Tests/MemberwiseInitTests/MemberwiseInitAccessLevelTests.swift" + +# Check for dirty changes in the target file +if git diff --name-only | grep -q "$output_file"; then + echo "Error: $output_file has uncommitted changes. Commit or stash them before running this script." + exit 1 +fi + +# Extract the class name from the output file name +class_name=$(basename "$output_file" .swift) + +# Define the possible values for each variable +access_levels=("private" "" "public") + +# Initialize init_modifiers with an empty string and access levels +init_modifiers=("") +for level in "${access_levels[@]}"; do + if [ -n "$level" ]; then + init_modifiers+=("@Init(.$level)") + else + # For the empty/default case, use "@Init(.internal)" + init_modifiers+=("@Init(.internal)") + fi +done + +# Include '[none]' for property access +property_access_levels=("private" "" "public" "[none]") + +# Function to capitalize the first letter of a string +capitalize() { + echo "$1" | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2));}1' +} + +# Start writing to the output file +{ + echo "// This file is automatically generated by '../../bin/generate_access_level_tests.sh'." + echo "// Do not edit this file directly." + echo "" + echo "import SwiftSyntaxMacros" + echo "import MacroTesting" + echo "import MemberwiseInitMacros" + echo "import XCTest" + echo "" + echo "final class $class_name: XCTestCase {" + echo " override func invokeTest() {" + echo " withMacroTesting(" + echo "// isRecording: true," + echo " macros: [" + echo " \"MemberwiseInit\": MemberwiseInitMacro.self," + echo " \"Init\": InitMacro.self" + echo " ]" + echo " ) {" + echo " super.invokeTest()" + echo " }" + echo " }" + echo "" + + # Generate test cases for each combination of values + for memberwise_access in "${access_levels[@]}"; do + for struct_access in "${access_levels[@]}"; do + for property_access in "${property_access_levels[@]}"; do + + # If property is [none], we don't want to iterate over different init_modifiers + if [ "$property_access" == "[none]" ]; then + init_modifiers_temp=("") + else + init_modifiers_temp=("${init_modifiers[@]}") + fi + + for init_modifier in "${init_modifiers_temp[@]}"; do + + memberwise_str=$([ -z "$memberwise_access" ] && echo "" || echo ".$memberwise_access") + struct_str=$([ -z "$struct_access" ] && echo "" || echo "$struct_access") + init_str=$([ -z "$init_modifier" ] && echo "" || echo "$init_modifier") + property_str=$([ -z "$property_access" ] && echo "" || echo "$property_access") + + 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') + + # Adjusting the test name for the "[none]" property access case + test_property_name=$([ "$property_access" == "[none]" ] && echo "No" || echo "$(capitalize ${property_access:-default})") + + test_name="testMemberwiseInit$(capitalize ${memberwise_access:-default})_$(capitalize ${struct_access:-default})Struct_${init_name:+Init}${init_name}_${test_property_name}Property" + test_name="${test_name//,/_}" + test_name="${test_name// /}" + test_name="${test_name//__/_}" + + echo " func $test_name() {" + echo " assertMacro {" + echo " \"\"\"" + echo " @MemberwiseInit${memberwise_str:+($memberwise_str)}" + echo " ${struct_str}${struct_str:+ }struct S {" + + # Only add the property declaration if property_access is not '[none]' + if [ "$property_access" != "[none]" ]; then + echo " ${init_str}${init_str:+ }${property_str}${property_str:+ }let v: T" + fi + + echo " }" + echo " \"\"\"" + echo " }" + echo " }" + echo "" + done + done + done + done + + echo "}" + +} > "$output_file" + +echo "Test file generated at $output_file"