Skip to content

Commit

Permalink
Add rule to use @entry macro instead of EnvironmentKeys (#1908)
Browse files Browse the repository at this point in the history
  • Loading branch information
miguel-jimenez-0529 authored and nicklockwood committed Nov 3, 2024
1 parent 9969166 commit 96cb9bd
Show file tree
Hide file tree
Showing 6 changed files with 685 additions and 5 deletions.
27 changes: 27 additions & 0 deletions Rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
* [blankLinesBetweenImports](#blankLinesBetweenImports)
* [blockComments](#blockComments)
* [docComments](#docComments)
* [environmentEntry](#environmentEntry)
* [isEmpty](#isEmpty)
* [markTypes](#markTypes)
* [noExplicitOwnership](#noExplicitOwnership)
Expand Down Expand Up @@ -873,6 +874,32 @@ Option | Description
</details>
<br/>

## environmentEntry

Updates SwiftUI `EnvironmentValues` definitions to use the @Entry macro.

<details>
<summary>Examples</summary>

```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")
}
```

</details>
<br/>

## extensionAccessControl

Configure the placement of an extension's access control keyword.
Expand Down
15 changes: 10 additions & 5 deletions Sources/DeclarationHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions Sources/RuleRegistry.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ let ruleRegistry: [String: FormatRule] = [
"emptyBraces": .emptyBraces,
"emptyExtension": .emptyExtension,
"enumNamespaces": .enumNamespaces,
"environmentEntry": .environmentEntry,
"extensionAccessControl": .extensionAccessControl,
"fileHeader": .fileHeader,
"fileMacro": .fileMacro,
Expand Down
195 changes: 195 additions & 0 deletions Sources/Rules/EnvironmentEntry.swift
Original file line number Diff line number Diff line change
@@ -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<Token>?
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<Token>?, 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<String>) {
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)
}
}
}
14 changes: 14 additions & 0 deletions SwiftFormat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1034,6 +1039,8 @@
9BDB4F1A2C94760000C93995 /* PrivateStateVariablesTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateStateVariablesTests.swift; sourceTree = "<group>"; };
9BDB4F1C2C94773600C93995 /* PrivateStateVariables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateStateVariables.swift; sourceTree = "<group>"; };
A3DF48242620E03600F45A5F /* JSONReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONReporter.swift; sourceTree = "<group>"; };
ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentEntryTests.swift; sourceTree = "<group>"; };
ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnvironmentEntry.swift; sourceTree = "<group>"; };
B9C4F55B2387FA3E0088DBEE /* SupportedContentUTIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportedContentUTIs.swift; sourceTree = "<group>"; };
C2FFD1812BD13C9E00774F55 /* XMLReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLReporter.swift; sourceTree = "<group>"; };
D52F6A632A82E04600FE1448 /* GitFileInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GitFileInfo.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1340,6 +1347,7 @@
2E2BABBB2C57F6DD00590239 /* WrapSingleLineComments.swift */,
2E2BABDF2C57F6DD00590239 /* WrapSwitchCases.swift */,
2E2BABF92C57F6DD00590239 /* YodaConditions.swift */,
ABC4BA2B2CB9B094002C6874 /* EnvironmentEntry.swift */,
);
path = Rules;
sourceTree = "<group>";
Expand Down Expand Up @@ -1459,6 +1467,7 @@
2E8DE6F12C57FEB30032BF25 /* WrapSwitchCasesTests.swift */,
2E8DE6A42C57FEB30032BF25 /* WrapTests.swift */,
2E8DE6AC2C57FEB30032BF25 /* YodaConditionsTests.swift */,
ABC11AF72CC082D300556471 /* EnvironmentEntryTests.swift */,
);
path = Rules;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
Loading

0 comments on commit 96cb9bd

Please sign in to comment.