mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
164 lines
7.2 KiB
Swift
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)
|
|
}
|
|
}
|
|
}
|