diff --git a/Rules.md b/Rules.md
index 75b013328..960a7854d 100644
--- a/Rules.md
+++ b/Rules.md
@@ -100,6 +100,7 @@
* [blankLinesBetweenImports](#blankLinesBetweenImports)
* [blockComments](#blockComments)
* [docComments](#docComments)
+* [environmentEntry](#environmentEntry)
* [isEmpty](#isEmpty)
* [markTypes](#markTypes)
* [noExplicitOwnership](#noExplicitOwnership)
@@ -873,6 +874,32 @@ Option | Description
+## environmentEntry
+
+Updates SwiftUI `EnvironmentValues` definitions to use the @Entry macro.
+
+
+Examples
+
+```diff
+- struct ScreenNameEnvironmentKey: EnvironmentKey {
+- static var defaultValue: Identifier? {
+- .init("undefined")
+- }
+- }
+
+ extension EnvironmentValues {
+- var screenName: Identifier? {
+- get { self[ScreenNameEnvironmentKey.self] }
+- set { self[ScreenNameEnvironmentKey.self] = newValue }
+- }
++ @Entry var screenName: Identifier? = .init("undefined")
+ }
+```
+
+
+
+
## extensionAccessControl
Configure the placement of an extension's access control keyword.
diff --git a/Sources/DeclarationHelpers.swift b/Sources/DeclarationHelpers.swift
index 2caecb2ac..898654ece 100644
--- a/Sources/DeclarationHelpers.swift
+++ b/Sources/DeclarationHelpers.swift
@@ -164,12 +164,17 @@ enum Declaration: Hashable {
/// Whether or not this declaration represents a stored instance property
var isStoredInstanceProperty: Bool {
- guard keyword == "let" || keyword == "var" else { return false }
-
// A static property is not an instance property
- if modifiers.contains("static") {
- return false
- }
+ !modifiers.contains("static") && isStoredProperty
+ }
+
+ /// Whether or not this declaration represents a static stored instance property
+ var isStaticStoredProperty: Bool {
+ modifiers.contains("static") && isStoredProperty
+ }
+
+ var isStoredProperty: Bool {
+ guard keyword == "let" || keyword == "var" else { return false }
// If this property has a body, then it's a stored property
// if and only if the declaration body has a `didSet` or `willSet` keyword,
diff --git a/Sources/RuleRegistry.generated.swift b/Sources/RuleRegistry.generated.swift
index 7386bae40..c75dc5347 100644
--- a/Sources/RuleRegistry.generated.swift
+++ b/Sources/RuleRegistry.generated.swift
@@ -35,6 +35,7 @@ let ruleRegistry: [String: FormatRule] = [
"emptyBraces": .emptyBraces,
"emptyExtension": .emptyExtension,
"enumNamespaces": .enumNamespaces,
+ "environmentEntry": .environmentEntry,
"extensionAccessControl": .extensionAccessControl,
"fileHeader": .fileHeader,
"fileMacro": .fileMacro,
diff --git a/Sources/Rules/EnvironmentEntry.swift b/Sources/Rules/EnvironmentEntry.swift
new file mode 100644
index 000000000..90f106b0d
--- /dev/null
+++ b/Sources/Rules/EnvironmentEntry.swift
@@ -0,0 +1,195 @@
+// Created by miguel_jimenez on 10/11/24.
+// Copyright © 2024 Airbnb Inc. All rights reserved.
+
+import Foundation
+
+public extension FormatRule {
+ /// Removes types conforming `EnvironmentKey` and replaces them with the @Entry macro
+ static let environmentEntry = FormatRule(
+ help: "Updates SwiftUI `EnvironmentValues` definitions to use the @Entry macro.",
+ disabledByDefault: true
+ ) { formatter in
+ // The @Entry macro is only available in Xcode 16 therefore this rule requires the same Xcode version to work.
+ guard formatter.options.swiftVersion >= "6.0" else { return }
+
+ let declarations = formatter.parseDeclarations()
+
+ // Find all structs that conform to `EnvironmentKey`
+ let environmentKeys = Dictionary(uniqueKeysWithValues: formatter.findAllEnvironmentKeys(declarations).map { ($0.key, $0) })
+
+ // Find all `EnvironmentValues` properties
+ let environmentValuesProperties = formatter.findAllEnvironmentValuesProperties(declarations, referencing: environmentKeys)
+
+ // Modify `EnvironmentValues` properties by removing its body and adding the @Entry macro
+ formatter.modifyEnvironmentValuesProperties(environmentValuesProperties)
+
+ // Remove `EnvironmentKey`s
+ let updatedEnvironmentKeys = Set(environmentValuesProperties.map(\.key))
+ formatter.removeEnvironmentKeys(updatedEnvironmentKeys)
+ } examples: {
+ """
+ ```diff
+ - struct ScreenNameEnvironmentKey: EnvironmentKey {
+ - static var defaultValue: Identifier? {
+ - .init("undefined")
+ - }
+ - }
+
+ extension EnvironmentValues {
+ - var screenName: Identifier? {
+ - get { self[ScreenNameEnvironmentKey.self] }
+ - set { self[ScreenNameEnvironmentKey.self] = newValue }
+ - }
+ + @Entry var screenName: Identifier? = .init("undefined")
+ }
+ ```
+ """
+ }
+}
+
+struct EnvironmentKey {
+ let key: String
+ let declaration: Declaration
+ let defaultValueTokens: ArraySlice?
+ let isMultilineDefaultValue: Bool
+}
+
+struct EnvironmentValueProperty {
+ let key: String
+ let associatedEnvironmentKey: EnvironmentKey
+ let declaration: Declaration
+}
+
+extension Formatter {
+ func findAllEnvironmentKeys(_ declarations: [Declaration]) -> [EnvironmentKey] {
+ declarations.compactMap { declaration -> EnvironmentKey? in
+ guard declaration.keyword == "struct" || declaration.keyword == "enum",
+ declaration.openTokens.contains(.identifier("EnvironmentKey")),
+ let keyName = declaration.openTokens.first(where: \.isIdentifier),
+ let structDeclarationBody = declaration.body,
+ structDeclarationBody.count == 1,
+ let defaultValueDeclaration = structDeclarationBody.first(where: {
+ ($0.keyword == "var" || $0.keyword == "let") && $0.name == "defaultValue"
+ }),
+ let (defaultValueTokens, isMultiline) = findEnvironmentKeyDefaultValue(defaultValueDeclaration)
+ else { return nil }
+ return EnvironmentKey(
+ key: keyName.string,
+ declaration: declaration,
+ defaultValueTokens: defaultValueTokens,
+ isMultilineDefaultValue: isMultiline
+ )
+ }
+ }
+
+ func findEnvironmentKeyDefaultValue(_ defaultValueDeclaration: Declaration) -> (tokens: ArraySlice?, isMultiline: Bool)? {
+ if defaultValueDeclaration.isStaticStoredProperty,
+ let equalsIndex = index(of: .operator("=", .infix), after: defaultValueDeclaration.originalRange.lowerBound),
+ equalsIndex <= defaultValueDeclaration.originalRange.upperBound,
+ let valueStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: equalsIndex),
+ let valueEndIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: defaultValueDeclaration.originalRange.upperBound)
+ {
+ // Default value is stored property, not computed (e.g. static var defaultValue: Bool = false)
+ return (tokens[valueStartIndex ... valueEndIndex], false)
+ } else if let valueEndOfScopeIndex = endOfScope(at: defaultValueDeclaration.originalRange.upperBound - 1),
+ let valueStartOfScopeIndex = startOfScope(at: valueEndOfScopeIndex),
+ let valueStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: valueStartOfScopeIndex),
+ let valueEndIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: valueEndOfScopeIndex)
+ {
+ let defaultValueDeclarations = parseDeclarations(in: valueStartIndex ... valueEndIndex)
+ let isMultilineDeclaration = defaultValueDeclarations.count > 1
+ if defaultValueDeclarations.count <= 1 {
+ if defaultValueDeclarations.first?.name == "defaultValue" {
+ // Default value is implicitly `nil` (e.g. static var defaultValue: Bool?)
+ return (nil, false)
+ } else {
+ // Default value is a computed property with a single value (e.g. static var defaultValue: Bool { false })
+ return (tokens[valueStartIndex ... valueEndIndex], isMultilineDeclaration)
+ }
+ } else {
+ // Default value is a multiline computed property:
+ // ```
+ // static var defaultValue: Bool {
+ // let computedValue = compute()
+ // return computedValue
+ // }
+ // ```
+ return (tokens[valueStartOfScopeIndex ... valueEndOfScopeIndex], isMultilineDeclaration)
+ }
+ } else { return nil }
+ }
+
+ func findAllEnvironmentValuesProperties(_ declarations: [Declaration], referencing environmentKeys: [String: EnvironmentKey])
+ -> [EnvironmentValueProperty]
+ {
+ declarations
+ .filter {
+ $0.keyword == "extension" && $0.openTokens.contains(.identifier("EnvironmentValues"))
+ }.compactMap { environmentValuesDeclaration -> [EnvironmentValueProperty]? in
+ environmentValuesDeclaration.body?.compactMap { propertyDeclaration -> (EnvironmentValueProperty)? in
+ guard propertyDeclaration.isSimpleDeclaration,
+ propertyDeclaration.keyword == "var",
+ let key = propertyDeclaration.tokens.first(where: { environmentKeys[$0.string] != nil })?.string,
+ let environmentKey = environmentKeys[key]
+ else { return nil }
+
+ // Ensure the property has a setter and a getter, this can avoid edge cases where
+ // a property references a `EnvironmentKey` and consumes it to perform some computation.
+ let propertyFormatter = Formatter(propertyDeclaration.tokens)
+ guard let indexOfSetter = propertyDeclaration.tokens.firstIndex(where: { $0 == .identifier("set") }),
+ propertyFormatter.isAccessorKeyword(at: indexOfSetter)
+ else { return nil }
+
+ return EnvironmentValueProperty(
+ key: key,
+ associatedEnvironmentKey: environmentKey,
+ declaration: propertyDeclaration
+ )
+ }
+ }.flatMap { $0 }
+ }
+
+ func modifyEnvironmentValuesProperties(_ environmentValuesPropertiesDeclarations: [EnvironmentValueProperty]) {
+ // Loop the collection in reverse to avoid invalidating the declaration indexes as we modify the property
+ for envProperty in environmentValuesPropertiesDeclarations.reversed() {
+ let propertyDeclaration = envProperty.declaration
+ guard let propertyBodyStartIndex = index(of: .startOfScope("{"), after: propertyDeclaration.originalRange.lowerBound),
+ let propertyBodyEndIndex = endOfScope(at: propertyBodyStartIndex),
+ let propertyStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: propertyDeclaration.originalRange.lowerBound)
+ else {
+ continue
+ }
+ // Remove `EnvironmentValues.property` getter and setters
+ if let nonSpaceTokenIndexBeforeBody = index(of: .nonSpaceOrLinebreak, before: propertyBodyStartIndex), nonSpaceTokenIndexBeforeBody != propertyBodyStartIndex {
+ // There are some spaces between the property body and the property type definition, we should remove the extra spaces.
+ let propertyBodyStartIndex = nonSpaceTokenIndexBeforeBody + 1
+ removeTokens(in: propertyBodyStartIndex ... propertyBodyEndIndex)
+ } else {
+ removeTokens(in: propertyBodyStartIndex ... propertyBodyEndIndex)
+ }
+ // Add `EnvironmentKey.defaultValue` to `EnvironmentValues property`
+ if let defaultValueTokens = envProperty.associatedEnvironmentKey.defaultValueTokens {
+ var defaultValueTokens = [.space(" "), .operator("=", .infix), .space(" ")] + defaultValueTokens
+
+ if envProperty.associatedEnvironmentKey.isMultilineDefaultValue {
+ defaultValueTokens.append(contentsOf: [.endOfScope("("), .endOfScope(")")])
+ }
+ insert(defaultValueTokens, at: endOfLine(at: propertyStartIndex))
+ }
+ // Add @Entry Macro
+ insert([.identifier("@Entry"), .space(" ")], at: propertyStartIndex)
+ }
+ }
+
+ func removeEnvironmentKeys(_ updatedEnvironmentKeys: Set) {
+ guard !updatedEnvironmentKeys.isEmpty else { return }
+
+ // After modifying the EnvironmentValues properties, parse declarations again to delete the Environment keys in their new position.
+ let repositionedEnvironmentKeys = findAllEnvironmentKeys(parseDeclarations())
+
+ // Loop the collection in reverse to avoid invalidating the declaration indexes as we remove EnvironmentKey
+ for declaration in repositionedEnvironmentKeys.reversed() where updatedEnvironmentKeys.contains(declaration.key) {
+ removeTokens(in: declaration.declaration.originalRange)
+ }
+ }
+}
diff --git a/SwiftFormat.xcodeproj/project.pbxproj b/SwiftFormat.xcodeproj/project.pbxproj
index 60ac536c9..63fd2a8bf 100644
--- a/SwiftFormat.xcodeproj/project.pbxproj
+++ b/SwiftFormat.xcodeproj/project.pbxproj
@@ -660,6 +660,11 @@
9BDB4F212C94780200C93995 /* PrivateStateVariables.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BDB4F1C2C94773600C93995 /* PrivateStateVariables.swift */; };
A3DF48252620E03600F45A5F /* JSONReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DF48242620E03600F45A5F /* JSONReporter.swift */; };
A3DF48262620E03600F45A5F /* JSONReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3DF48242620E03600F45A5F /* JSONReporter.swift */; };
+ ABC11AF82CC082D300556471 /* EnvironmentEntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */; };
+ ABC4BA2C2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */; };
+ ABC4BA2D2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */; };
+ ABC4BA2E2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */; };
+ ABC4BA2F2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */; };
B9C4F55C2387FA3E0088DBEE /* SupportedContentUTIs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9C4F55B2387FA3E0088DBEE /* SupportedContentUTIs.swift */; };
C2FFD1822BD13C9E00774F55 /* XMLReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FFD1812BD13C9E00774F55 /* XMLReporter.swift */; };
C2FFD1832BD13C9E00774F55 /* XMLReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2FFD1812BD13C9E00774F55 /* XMLReporter.swift */; };
@@ -1034,6 +1039,8 @@
9BDB4F1A2C94760000C93995 /* PrivateStateVariablesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateStateVariablesTests.swift; sourceTree = ""; };
9BDB4F1C2C94773600C93995 /* PrivateStateVariables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateStateVariables.swift; sourceTree = ""; };
A3DF48242620E03600F45A5F /* JSONReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONReporter.swift; sourceTree = ""; };
+ ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentEntryTests.swift; sourceTree = ""; };
+ ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentEntry.swift; sourceTree = ""; };
B9C4F55B2387FA3E0088DBEE /* SupportedContentUTIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedContentUTIs.swift; sourceTree = ""; };
C2FFD1812BD13C9E00774F55 /* XMLReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLReporter.swift; sourceTree = ""; };
D52F6A632A82E04600FE1448 /* GitFileInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileInfo.swift; sourceTree = ""; };
@@ -1340,6 +1347,7 @@
2E2BABBB2C57F6DD00590239 /* WrapSingleLineComments.swift */,
2E2BABDF2C57F6DD00590239 /* WrapSwitchCases.swift */,
2E2BABF92C57F6DD00590239 /* YodaConditions.swift */,
+ ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */,
);
path = Rules;
sourceTree = "";
@@ -1459,6 +1467,7 @@
2E8DE6F12C57FEB30032BF25 /* WrapSwitchCasesTests.swift */,
2E8DE6A42C57FEB30032BF25 /* WrapTests.swift */,
2E8DE6AC2C57FEB30032BF25 /* YodaConditionsTests.swift */,
+ ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */,
);
path = Rules;
sourceTree = "";
@@ -1941,6 +1950,7 @@
2E2BAC832C57F6DD00590239 /* Linebreaks.swift in Sources */,
2E2BAC3F2C57F6DD00590239 /* DuplicateImports.swift in Sources */,
2E2BAC1F2C57F6DD00590239 /* RedundantOptionalBinding.swift in Sources */,
+ ABC4BA2F2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */,
2E2BACBF2C57F6DD00590239 /* TypeSugar.swift in Sources */,
2E2BAD372C57F6DD00590239 /* WrapSwitchCases.swift in Sources */,
2E2BAD472C57F6DD00590239 /* WrapLoopBodies.swift in Sources */,
@@ -2022,6 +2032,7 @@
0142F06F1D72FE10007D66CC /* SwiftFormatTests.swift in Sources */,
2E8DE7432C57FEB30032BF25 /* SpaceAroundBracketsTests.swift in Sources */,
2E8DE7612C57FEB30032BF25 /* RedundantFileprivateTests.swift in Sources */,
+ ABC11AF82CC082D300556471 /* EnvironmentEntryTests.swift in Sources */,
2E8DE6F82C57FEB30032BF25 /* RedundantClosureTests.swift in Sources */,
2E8DE7562C57FEB30032BF25 /* BlankLinesAtStartOfScopeTests.swift in Sources */,
2E8DE75F2C57FEB30032BF25 /* BlankLineAfterImportsTests.swift in Sources */,
@@ -2122,6 +2133,7 @@
2E2BAD802C57F6DD00590239 /* SortTypealiases.swift in Sources */,
2E2BADA02C57F6DD00590239 /* YodaConditions.swift in Sources */,
2E2BACA82C57F6DD00590239 /* WrapSingleLineComments.swift in Sources */,
+ ABC4BA2E2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */,
2E2BAC742C57F6DD00590239 /* EmptyBraces.swift in Sources */,
2E2BAD242C57F6DD00590239 /* RedundantSelf.swift in Sources */,
2E2BAC282C57F6DD00590239 /* RedundantNilInit.swift in Sources */,
@@ -2287,6 +2299,7 @@
E4872114201D3B8C0014845E /* Tokenizer.swift in Sources */,
2E2BAD612C57F6DD00590239 /* RedundantBackticks.swift in Sources */,
2E2BAC752C57F6DD00590239 /* EmptyBraces.swift in Sources */,
+ ABC4BA2C2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */,
2E2BACF92C57F6DD00590239 /* RedundantStaticSelf.swift in Sources */,
2E2BAC512C57F6DD00590239 /* SortImports.swift in Sources */,
2E2BAC112C57F6DD00590239 /* WrapMultilineConditionalAssignment.swift in Sources */,
@@ -2434,6 +2447,7 @@
E4FABAD8202FEF060065716E /* OptionDescriptor.swift in Sources */,
2E2BAD622C57F6DD00590239 /* RedundantBackticks.swift in Sources */,
2E2BAC762C57F6DD00590239 /* EmptyBraces.swift in Sources */,
+ ABC4BA2D2CB9B094002C6874 /* EnvironmentEntry.swift in Sources */,
2E2BACFA2C57F6DD00590239 /* RedundantStaticSelf.swift in Sources */,
2E2BAC522C57F6DD00590239 /* SortImports.swift in Sources */,
2E2BAC122C57F6DD00590239 /* WrapMultilineConditionalAssignment.swift in Sources */,
diff --git a/Tests/Rules/EnvironmentEntryTests.swift b/Tests/Rules/EnvironmentEntryTests.swift
new file mode 100644
index 000000000..a7411f9ac
--- /dev/null
+++ b/Tests/Rules/EnvironmentEntryTests.swift
@@ -0,0 +1,438 @@
+// Created by miguel_jimenez on 10/16/24.
+// Copyright © 2024 Airbnb Inc. All rights reserved.
+
+import SwiftFormat
+import XCTest
+
+final class EnvironmentEntryTests: XCTestCase {
+ func testReplaceEnvironmentKeyDefinitionForEntryMacro() {
+ let input = """
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ .init("undefined")
+ }
+ }
+
+ extension EnvironmentValues {
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var screenName: Identifier? = .init("undefined")
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testReplaceEnvironmentKeyDefinitionForEntryMacroWithKeyDefinitionAfterEnvironmentValue() {
+ let input = """
+ extension EnvironmentValues {
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ .init("undefined")
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var screenName: Identifier? = .init("undefined")
+ }
+
+ """
+ testFormatting(
+ for: input, [output],
+ rules: [
+ .environmentEntry,
+ .blankLinesBetweenScopes,
+ .consecutiveBlankLines,
+ .blankLinesAtEndOfScope,
+ .blankLinesAtStartOfScope,
+ ],
+ options: FormatOptions(swiftVersion: "6.0")
+ )
+ }
+
+ func testReplaceMultipleEnvironmentKeyDefinitionForEntryMacro() {
+ let input = """
+ extension EnvironmentValues {
+ var isSelected: Bool {
+ get { self[IsSelectedEnvironmentKey.self] }
+ set { self[IsSelectedEnvironmentKey.self] = newValue }
+ }
+ }
+
+ struct IsSelectedEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Bool { false }
+ }
+
+ extension EnvironmentValues {
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ .init("undefined")
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var isSelected: Bool = false
+ }
+
+ extension EnvironmentValues {
+ @Entry var screenName: Identifier? = .init("undefined")
+ }
+
+ """
+ testFormatting(
+ for: input, [output],
+ rules: [
+ .environmentEntry,
+ .blankLinesBetweenScopes,
+ .blankLinesAtEndOfScope,
+ .blankLinesAtStartOfScope,
+ .consecutiveBlankLines,
+ ],
+ options: FormatOptions(swiftVersion: "6.0")
+ )
+ }
+
+ func testReplaceMultipleEnvironmentKeyPropertiesInSameEnvironmentValuesExtension() {
+ let input = """
+ extension EnvironmentValues {
+ var isSelected: Bool {
+ get { self[IsSelectedEnvironmentKey.self] }
+ set { self[IsSelectedEnvironmentKey.self] = newValue }
+ }
+
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+
+ struct IsSelectedEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Bool { false }
+ }
+
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ .init("undefined")
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var isSelected: Bool = false
+
+ @Entry var screenName: Identifier? = .init("undefined")
+ }
+
+ """
+ testFormatting(
+ for: input, [output],
+ rules: [
+ .environmentEntry,
+ .blankLinesBetweenScopes,
+ .blankLinesAtEndOfScope,
+ .blankLinesAtStartOfScope,
+ .consecutiveBlankLines,
+ ],
+ options: FormatOptions(swiftVersion: "6.0")
+ )
+ }
+
+ func testEnvironmentKeyIsNotRemovedWhenPropertyAndKeyDontMatch() {
+ let input = """
+ extension EnvironmentValues {
+ var isSelected: Bool {
+ get { self[IsSelectedEnvironmentKey.self] }
+ set { self[IsSelectedEnvironmentKey.self] = newValue }
+ }
+ }
+
+ struct SelectedEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Bool { false }
+ }
+ """
+ testFormatting(for: input, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testReplaceEnvironmentKeyWithMultipleLinesInDefaultValue() {
+ let input = """
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ let domain = "com.mycompany.myapp"
+ let base = "undefined"
+ return .init("\\(domain).\\(base)")
+ }
+ }
+
+ extension EnvironmentValues {
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var screenName: Identifier? = {
+ let domain = "com.mycompany.myapp"
+ let base = "undefined"
+ return .init("\\(domain).\\(base)")
+ }()
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testReplaceEnvironmentKeyWithImplicitNilDefaultValue() {
+ let input = """
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier?
+ }
+
+ extension EnvironmentValues {
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var screenName: Identifier?
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testEnvironmentPropertyWithCommentsPreserved() {
+ let input = """
+ struct ScreenNameEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Identifier? {
+ .init("undefined")
+ }
+ }
+
+ extension EnvironmentValues {
+ /// The name provided to the outer most view representing a full screen width
+ var screenName: Identifier? {
+ get { self[ScreenNameEnvironmentKey.self] }
+ set { self[ScreenNameEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ /// The name provided to the outer most view representing a full screen width
+ @Entry var screenName: Identifier? = .init("undefined")
+ }
+ """
+ testFormatting(
+ for: input, output,
+ rule: .environmentEntry,
+ options: FormatOptions(swiftVersion: "6.0"),
+ exclude: [.docComments]
+ )
+ }
+
+ func testEnvironmentKeyWithMultipleDefinitionsIsNotRemoved() {
+ let input = """
+ extension EnvironmentValues {
+ var isSelected: Bool {
+ get { self[IsSelectedEnvironmentKey.self] }
+ set { self[IsSelectedEnvironmentKey.self] = newValue }
+ }
+
+ var doSomething() {
+ print("do some work")
+ }
+ }
+
+ struct SelectedEnvironmentKey: EnvironmentKey {
+ static var defaultValue: Bool { false }
+ }
+ """
+ testFormatting(for: input, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testEntryMacroReplacementWhenDefaultValueIsNotComputed() {
+ let input = """
+ struct ScreenStyleEnvironmentKey: EnvironmentKey {
+ static var defaultValue: ScreenStyle = ScreenStyle()
+ }
+
+ extension EnvironmentValues {
+ var screenStyle: ScreenStyle {
+ get { self[ScreenStyleEnvironmentKey.self] }
+ set { self[ScreenStyleEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry var screenStyle: ScreenStyle = ScreenStyle()
+ }
+ """
+ testFormatting(
+ for: input, output,
+ rule: .environmentEntry,
+ options: FormatOptions(swiftVersion: "6.0"),
+ exclude: [.redundantType]
+ )
+ }
+
+ func testEntryMacroReplacementWhenPropertyIsPublic() {
+ let input = """
+ struct ScreenStyleEnvironmentKey: EnvironmentKey {
+ static var defaultValue: ScreenStyle { .init() }
+ }
+
+ extension EnvironmentValues {
+ public var screenStyle: ScreenStyle {
+ get { self[ScreenStyleEnvironmentKey.self] }
+ set { self[ScreenStyleEnvironmentKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry public var screenStyle: ScreenStyle = .init()
+ }
+ """
+ testFormatting(
+ for: input, output,
+ rule: .environmentEntry,
+ options: FormatOptions(swiftVersion: "6.0")
+ )
+ }
+
+ func testEntryMacroReplacementWhenKeyDoesntHaveEnvironmentKeySuffix() {
+ let input = """
+ struct ScreenStyle: EnvironmentKey {
+ static var defaultValue: Style { .init() }
+ }
+
+ extension EnvironmentValues {
+ public var screenStyle: Style {
+ get { self[ScreenStyle.self] }
+ set { self[ScreenStyle.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry public var screenStyle: Style = .init()
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testEntryMacroReplacementWithEnumEnvironmentKey() {
+ let input = """
+ private enum InputShouldChangeKey: EnvironmentKey {
+ static var defaultValue: InputShouldChangeHandler { nil }
+ }
+
+ extension EnvironmentValues {
+ public var inputShouldChange: InputShouldChangeHandler {
+ get { self[InputShouldChangeKey.self] }
+ set { self[InputShouldChangeKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry public var inputShouldChange: InputShouldChangeHandler = nil
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testEntryMacroReplacementWhenDefaultValueIsLet() {
+ let input = """
+ private struct ScreenStyleKey: EnvironmentKey {
+ static let defaultValue: Style = .init()
+ }
+
+ extension EnvironmentValues {
+ public var screenStyle: Style {
+ get { self[ScreenStyleKey.self] }
+ set { self[ScreenStyleKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry public var screenStyle: Style = .init()
+ }
+ """
+ testFormatting(for: input, output, rule: .environmentEntry, options: FormatOptions(swiftVersion: "6.0"))
+ }
+
+ func testEntryMacroReplacementWithoutExplicitTypeAnnotation() {
+ let input = """
+ private struct ScreenStyleKey: EnvironmentKey {
+ static let defaultValue = Style()
+ }
+
+ extension EnvironmentValues {
+ public var screenStyle: Style {
+ get { self[ScreenStyleKey.self] }
+ set { self[ScreenStyleKey.self] = newValue }
+ }
+ }
+ """
+ let output = """
+ extension EnvironmentValues {
+ @Entry public var screenStyle: Style = Style()
+ }
+ """
+ testFormatting(
+ for: input, output,
+ rule: .environmentEntry,
+ options: FormatOptions(swiftVersion: "6.0"),
+ exclude: [.redundantType]
+ )
+ }
+
+ func testEnvironmentValuesPropertyWithoutSetterIsNotModified() {
+ let input = """
+ struct AEnvironmentKey: EnvironmentKey {
+ static var defaultValue: A = .default
+ }
+
+ extension EnvironmentValues {
+ public var fallbackA: A {
+ if self[AEnvironmentKey.self] {
+ A()
+ } else {
+ something()
+ }
+ }
+ }
+ """
+ testFormatting(
+ for: input,
+ rule: .environmentEntry,
+ options: FormatOptions(swiftVersion: "6.0"),
+ exclude: [.redundantType]
+ )
+ }
+}