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] + ) + } +}