Files
SwiftFormat/Sources/Rules/EnvironmentEntry.swift
2026-01-25 09:02:04 -08:00

164 lines
7.2 KiB
Swift

// 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."
) { 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 = formatter.findAllEnvironmentKeys(declarations)
// 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
for environmentValuesProperty in environmentValuesProperties {
if let environmentKey = environmentKeys[environmentValuesProperty.key] {
environmentKey.declaration.remove()
}
}
} 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: [Token]?
}
struct EnvironmentValueProperty {
let key: String
let associatedEnvironmentKey: EnvironmentKey
let declaration: Declaration
}
extension Formatter {
func findAllEnvironmentKeys(_ declarations: [Declaration]) -> [String: EnvironmentKey] {
var environmentKeys = [String: EnvironmentKey]()
for declaration in declarations {
guard let typeDeclaration = declaration.asTypeDeclaration,
typeDeclaration.keyword == "struct" || typeDeclaration.keyword == "enum",
typeDeclaration.conformances.contains(where: { $0.conformance.string == "EnvironmentKey" }),
let keyName = typeDeclaration.name,
typeDeclaration.body.count == 1,
let defaultValueDeclaration = typeDeclaration.body.first(where: {
($0.keyword == "var" || $0.keyword == "let") && $0.name == "defaultValue"
})
else { continue }
environmentKeys[keyName] = EnvironmentKey(
key: keyName,
declaration: typeDeclaration,
defaultValueTokens: findEnvironmentKeyDefaultValue(defaultValueDeclaration)
)
}
return environmentKeys
}
func findEnvironmentKeyDefaultValue(_ defaultValueDeclaration: Declaration) -> [Token]? {
guard let property = defaultValueDeclaration.parsePropertyDeclaration() else { return nil }
if let valueRange = property.value?.expressionRange {
return Array(tokens[valueRange])
}
else if let body = property.body {
// If the body contains multiple expressions, the final output will need to be wrapped
// in an immediately-executed closure.
if !scopeBodyIsSingleExpression(at: body.scopeRange.lowerBound) {
let existingBodyScope = Array(tokens[body.scopeRange])
return existingBodyScope + [.startOfScope("("), .endOfScope(")")]
} else {
return Array(tokens[body.range])
}
}
return nil
}
func findAllEnvironmentValuesProperties(
_ declarations: [Declaration],
referencing environmentKeys: [String: EnvironmentKey]
) -> [EnvironmentValueProperty] {
declarations
.filter {
$0.keyword == "extension" && $0.name == "EnvironmentValues"
}.compactMap { environmentValuesDeclaration -> [EnvironmentValueProperty]? in
environmentValuesDeclaration.body?.compactMap { propertyDeclaration -> (EnvironmentValueProperty)? in
guard 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.
guard let bodyRange = propertyDeclaration.parsePropertyDeclaration()?.body?.range,
let indexOfSetter = index(of: .identifier("set"), in: Range(bodyRange)),
isAccessorKeyword(at: indexOfSetter)
else { return nil }
return EnvironmentValueProperty(
key: key,
associatedEnvironmentKey: environmentKey,
declaration: propertyDeclaration
)
}
}.flatMap { $0 }
}
func modifyEnvironmentValuesProperties(_ environmentValuesPropertiesDeclarations: [EnvironmentValueProperty]) {
for envProperty in environmentValuesPropertiesDeclarations {
guard let propertyDeclaration = envProperty.declaration.parsePropertyDeclaration(),
let bodyScopeRange = propertyDeclaration.body?.scopeRange
else { continue }
// Remove `EnvironmentValues.property` getter and setters
if let nonSpaceTokenIndexBeforeBody = index(of: .nonSpaceOrLinebreak, before: bodyScopeRange.lowerBound), nonSpaceTokenIndexBeforeBody != bodyScopeRange.lowerBound {
// There are some spaces between the property body and the property type definition, we should remove the extra spaces.
removeTokens(in: nonSpaceTokenIndexBeforeBody + 1 ... bodyScopeRange.upperBound)
} else {
removeTokens(in: bodyScopeRange)
}
// Add `EnvironmentKey.defaultValue` to `EnvironmentValues property`
if let defaultValueTokens = envProperty.associatedEnvironmentKey.defaultValueTokens {
let defaultValueTokens = [.space(" "), .operator("=", .infix), .space(" ")] + defaultValueTokens
insert(defaultValueTokens, at: endOfLine(at: propertyDeclaration.range.lowerBound))
}
// Add @Entry Macro
insert([.identifier("@Entry"), .space(" ")], at: propertyDeclaration.range.lowerBound)
}
}
}