mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-06-16 10:34:34 +00:00
Add rule to update generic methods to use opaque generic parameter syntax where equivalent (#1206)
This commit is contained in:
committed by
Nick Lockwood
parent
fa8bb65ad9
commit
03e7e138e5
@@ -24,6 +24,7 @@
|
||||
* [linebreaks](#linebreaks)
|
||||
* [modifierOrder](#modifierOrder)
|
||||
* [numberFormatting](#numberFormatting)
|
||||
* [opaqueGenericParameters](#opaqueGenericParameters)
|
||||
* [preferKeyPath](#preferKeyPath)
|
||||
* [redundantBackticks](#redundantBackticks)
|
||||
* [redundantBreak](#redundantBreak)
|
||||
@@ -907,6 +908,36 @@ Option | Description
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## opaqueGenericParameters
|
||||
|
||||
Use opaque generic parameters (`some Protocol`) instead of generic parameters
|
||||
with constraints (`T where T: Protocol`, etc) where equivalent. Also supports
|
||||
primary associated types for common standard library types, so definitions like
|
||||
`T where T: Collection, T.Element == Foo` are upated to `some Collection<Foo>`.
|
||||
|
||||
<details>
|
||||
<summary>Examples</summary>
|
||||
|
||||
```diff
|
||||
- func handle<T: Fooable>(_ value: T) {
|
||||
+ func handle(_ value: some Fooable) {
|
||||
print(value)
|
||||
}
|
||||
|
||||
- func handle<T>(_ value: T) where T: Fooable, T: Barable {
|
||||
+ func handle(_ value: some Fooable & Barable) {
|
||||
print(value)
|
||||
}
|
||||
|
||||
- func handle<T: Collection>(_ value: T) where T.Element == Foo {
|
||||
+ func handle(_ value: some Collection<Foo>) {
|
||||
print(value)
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
<br/>
|
||||
|
||||
## organizeDeclarations
|
||||
|
||||
Organizes declarations within class, struct, enum, actor, and extension bodies.
|
||||
|
||||
@@ -1408,4 +1408,23 @@ private struct Examples {
|
||||
}
|
||||
```
|
||||
"""
|
||||
|
||||
let opaqueGenericParameters = """
|
||||
```diff
|
||||
- func handle<T: Fooable>(_ value: T) {
|
||||
+ func handle(_ value: some Fooable) {
|
||||
print(value)
|
||||
}
|
||||
|
||||
- func handle<T>(_ value: T) where T: Fooable, T: Barable {
|
||||
+ func handle(_ value: some Fooable & Barable) {
|
||||
print(value)
|
||||
}
|
||||
|
||||
- func handle<T: Collection>(_ value: T) where T.Element == Foo {
|
||||
+ func handle(_ value: some Collection<Foo>) {
|
||||
print(value)
|
||||
}
|
||||
```
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -322,6 +322,19 @@ public extension Formatter {
|
||||
removeTokens(in: range.lowerBound ..< range.upperBound + 1)
|
||||
}
|
||||
|
||||
/// Removes the tokens in the specified set of ranges, that must not overlay
|
||||
func removeTokens(in rangesToRemove: [ClosedRange<Int>]) {
|
||||
// We remove the ranges in reverse order, so that removing
|
||||
// one range doesn't invalidate the existings of the other ranges
|
||||
let rangeRemovalOrder = rangesToRemove
|
||||
.sorted(by: { $0.startIndex < $1.startIndex })
|
||||
.reversed()
|
||||
|
||||
for rangeToRemove in rangeRemovalOrder {
|
||||
removeTokens(in: rangeToRemove)
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the last token
|
||||
func removeLastToken() {
|
||||
trackChange(at: tokens.endIndex - 1)
|
||||
|
||||
@@ -6781,4 +6781,401 @@ public struct _FormatRules {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let opaqueGenericParameters = FormatRule(
|
||||
help: """
|
||||
Use opaque generic parameters (`some Protocol`) instead of generic parameters
|
||||
with constraints (`T where T: Protocol`, etc) where equivalent. Also supports
|
||||
primary associated types for common standard library types, so definitions like
|
||||
`T where T: Collection, T.Element == Foo` are upated to `some Collection<Foo>`.
|
||||
""",
|
||||
options: []
|
||||
) { formatter in
|
||||
formatter.forEach(.keyword("func")) { funcIndex, _ in
|
||||
guard
|
||||
// Opaque generic parameter syntax is only supported in Swift 5.7+
|
||||
formatter.options.swiftVersion >= "5.7",
|
||||
// Validate that this is a generic method using angle bracket syntax,
|
||||
// and find the indices for all of the key tokens
|
||||
let paramListStartIndex = formatter.index(of: .startOfScope("("), after: funcIndex),
|
||||
let paramListEndIndex = formatter.endOfScope(at: paramListStartIndex),
|
||||
let genericSignatureStartIndex = formatter.index(of: .startOfScope("<"), after: funcIndex),
|
||||
let genericSignatureEndIndex = formatter.endOfScope(at: genericSignatureStartIndex),
|
||||
genericSignatureStartIndex < paramListStartIndex,
|
||||
genericSignatureEndIndex < paramListStartIndex,
|
||||
let openBraceIndex = formatter.index(of: .startOfScope("{"), after: paramListEndIndex),
|
||||
let closeBraceIndex = formatter.endOfScope(at: openBraceIndex)
|
||||
else { return }
|
||||
|
||||
/// A generic type parameter for a method
|
||||
class GenericType {
|
||||
/// The name of the generic parameter. For example with `<T: Fooable>` the generic parameter `name` is `T`.
|
||||
let name: String
|
||||
/// The source range within angle brackets where the generic parameter is defined
|
||||
let definitionSourceRange: ClosedRange<Int>
|
||||
/// Conformances and constraints applied to this generic parameter
|
||||
var conformances: [GenericConformance]
|
||||
/// Whether or not this generic parameter can be removed and replaced with an opaque generic parameter
|
||||
var eligbleToRemove = true
|
||||
|
||||
/// A constraint or conformance that applies to a generic type
|
||||
struct GenericConformance: Hashable {
|
||||
enum ConformanceType {
|
||||
/// A protocol constraint like `T: Fooable`
|
||||
case protocolConstraint
|
||||
/// A concrete type like `T == Foo`
|
||||
case conceteType
|
||||
}
|
||||
|
||||
/// The name of the type being used in the constraint. For example with `T: Fooable`
|
||||
/// the constraint name is `Fooable`
|
||||
let name: String
|
||||
/// The name of the type being constrained. For example with `T: Fooable` the
|
||||
/// `typeName` is `T`. This can correspond exactly to the `name` of a `GenericType`,
|
||||
/// but can also be something like `T.AssociatedType` where `T` is the `name` of a `GenericType`.
|
||||
let typeName: String
|
||||
/// The type of conformance or constraint represented by this value.
|
||||
let type: ConformanceType
|
||||
/// The source range in the angle brackets or where clause where this conformance is defined.
|
||||
let sourceRange: ClosedRange<Int>
|
||||
}
|
||||
|
||||
init(name: String, definitionSourceRange: ClosedRange<Int>) {
|
||||
self.name = name
|
||||
self.definitionSourceRange = definitionSourceRange
|
||||
conformances = []
|
||||
}
|
||||
|
||||
// The opaque parameter syntax that represents this generic type,
|
||||
// if the constraints can be expressed using this syntax
|
||||
var asOpaqueParameter: [Token]? {
|
||||
if conformances.isEmpty {
|
||||
return tokenize("some Any")
|
||||
}
|
||||
|
||||
// Protocols with primary associated types that can be used with
|
||||
// opaque parameter syntax. In the future we could make this extensible
|
||||
// so users can add their own types here.
|
||||
let knownProtocolsWithAssociatedTypes: [(name: String, primaryAssociatedType: String)] = [
|
||||
(name: "Collection", primaryAssociatedType: "Element"),
|
||||
(name: "Sequence", primaryAssociatedType: "Element"),
|
||||
]
|
||||
|
||||
let constraints = conformances.filter { $0.type == .protocolConstraint }
|
||||
var primaryAssociatedTypes = [GenericConformance: GenericConformance]()
|
||||
|
||||
// Validate that all of the conformances can be represented using this syntax
|
||||
for conformance in conformances {
|
||||
if conformance.typeName.contains(".") {
|
||||
switch conformance.type {
|
||||
case .protocolConstraint:
|
||||
// Constraints like `Foo.Bar: Barable` cannot be represented using
|
||||
// opaque generic parameter syntax
|
||||
return nil
|
||||
|
||||
case .conceteType:
|
||||
// Concrete type constraints like `Foo.Element == Bar` can be
|
||||
// represented using opaque generic parameter syntax if we know
|
||||
// that it's using a primary associated type of the base protocol
|
||||
// (e.g. if `Foo` is a `Collection` or `Sequence`)
|
||||
let typeElements = conformance.typeName.components(separatedBy: ".")
|
||||
guard typeElements.count == 2 else { return nil }
|
||||
|
||||
let associatedTypeName = typeElements[1]
|
||||
|
||||
// Look up if the generic param conforms to any of the protocols
|
||||
// with a primary associated type matching the one we found
|
||||
let matchingProtocolWithAssociatedType = constraints.first(where: { genericConstraint in
|
||||
let knownProtocol = knownProtocolsWithAssociatedTypes.first(where: { $0.name == genericConstraint.name })
|
||||
return knownProtocol?.primaryAssociatedType == associatedTypeName
|
||||
})
|
||||
|
||||
if let matchingProtocolWithAssociatedType = matchingProtocolWithAssociatedType {
|
||||
primaryAssociatedTypes[matchingProtocolWithAssociatedType] = conformance
|
||||
} else {
|
||||
// If this isn't the primary associated type of a protocol constraint, then we can't use it
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let constraintRepresentations = constraints.map { constraint -> String in
|
||||
if let primaryAssociatedType = primaryAssociatedTypes[constraint] {
|
||||
return "\(constraint.name)<\(primaryAssociatedType.name)>"
|
||||
} else {
|
||||
return constraint.name
|
||||
}
|
||||
}
|
||||
|
||||
return tokenize("some \(constraintRepresentations.joined(separator: " & "))")
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the generic signature between the angle brackets so we know all of the generic types
|
||||
var genericTypes = [GenericType]()
|
||||
|
||||
/// Parses generic types between the angle brackets of a function declaration, and in its where clause
|
||||
func parseGenericTypes(from genericSignatureStartIndex: Int, to genericSignatureEndIndex: Int) {
|
||||
var currentIndex = genericSignatureStartIndex
|
||||
|
||||
while currentIndex < genericSignatureEndIndex - 1 {
|
||||
guard let genericTypeNameIndex = formatter.index(of: .identifier, after: currentIndex) else {
|
||||
break
|
||||
}
|
||||
|
||||
let typeEndIndex: Int
|
||||
let nextCommaIndex = formatter.index(of: .delimiter(","), after: genericTypeNameIndex)
|
||||
if let nextCommaIndex = nextCommaIndex, nextCommaIndex < genericSignatureEndIndex {
|
||||
typeEndIndex = nextCommaIndex
|
||||
} else {
|
||||
typeEndIndex = genericSignatureEndIndex - 1
|
||||
}
|
||||
|
||||
// Include all whitespace and comments in the conformance's source range,
|
||||
// so if we remove it later all of the extra whitespace will get cleaned up
|
||||
let sourceRangeEnd: Int
|
||||
if let nextTokenIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: typeEndIndex) {
|
||||
sourceRangeEnd = nextTokenIndex - 1
|
||||
} else {
|
||||
sourceRangeEnd = typeEndIndex
|
||||
}
|
||||
|
||||
// The generic constraint could have syntax like `Foo`, `Foo: Fooable`,
|
||||
// `Foo.Element == Fooable`, etc. Create a reference to this specific
|
||||
// generic parameter (`Foo` in all of these examples) that can store
|
||||
// the constraints and conformances that we encounter later.
|
||||
let fullGenericTypeName = formatter.tokens[genericTypeNameIndex].string
|
||||
let baseGenericTypeName = fullGenericTypeName.components(separatedBy: ".")[0]
|
||||
|
||||
let genericType: GenericType
|
||||
if let existingType = genericTypes.first(where: { $0.name == baseGenericTypeName }) {
|
||||
genericType = existingType
|
||||
} else {
|
||||
genericType = GenericType(
|
||||
name: baseGenericTypeName,
|
||||
definitionSourceRange: genericTypeNameIndex ... sourceRangeEnd
|
||||
)
|
||||
genericTypes.append(genericType)
|
||||
}
|
||||
|
||||
// Parse the constraint after the type name if present
|
||||
var delineatorIndex: Int?
|
||||
var conformanceType: GenericType.GenericConformance.ConformanceType?
|
||||
|
||||
// This can either be a protocol constraint of the form `T: Fooable`
|
||||
if let colonIndex = formatter.index(of: .delimiter(":"), after: genericTypeNameIndex),
|
||||
colonIndex < typeEndIndex
|
||||
{
|
||||
delineatorIndex = colonIndex
|
||||
conformanceType = .protocolConstraint
|
||||
}
|
||||
|
||||
// or a concrete type of the form `T == Foo`
|
||||
else if let equalsIndex = formatter.index(of: .operator("==", .infix), after: genericTypeNameIndex),
|
||||
equalsIndex < typeEndIndex
|
||||
{
|
||||
delineatorIndex = equalsIndex
|
||||
conformanceType = .conceteType
|
||||
}
|
||||
|
||||
if let delineatorIndex = delineatorIndex, let conformanceType = conformanceType {
|
||||
let constrainedTypeName = formatter.tokens[genericTypeNameIndex ..< delineatorIndex]
|
||||
.map { $0.string }
|
||||
.joined()
|
||||
.trimmingCharacters(in: .init(charactersIn: " \n,<>{}"))
|
||||
|
||||
let conformanceName = formatter.tokens[(delineatorIndex + 1) ... typeEndIndex]
|
||||
.map { $0.string }
|
||||
.joined()
|
||||
.trimmingCharacters(in: .init(charactersIn: " \n,<>{}"))
|
||||
|
||||
genericType.conformances.append(.init(
|
||||
name: conformanceName,
|
||||
typeName: constrainedTypeName,
|
||||
type: conformanceType,
|
||||
sourceRange: genericTypeNameIndex ... sourceRangeEnd
|
||||
))
|
||||
}
|
||||
|
||||
currentIndex = typeEndIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the generics in the angle brackets (e.g. `<T, U: Fooable>`)
|
||||
parseGenericTypes(from: genericSignatureStartIndex, to: genericSignatureEndIndex)
|
||||
|
||||
// Parse additional conformances and constraints after the `where` keyword if present
|
||||
// (e.g. `where Foo: Fooable, Foo.Bar: Barable, Foo.Baaz == Baazable`)
|
||||
var whereTokenIndex: Int?
|
||||
if let whereIndex = formatter.index(of: .keyword("where"), after: paramListEndIndex),
|
||||
whereIndex < openBraceIndex
|
||||
{
|
||||
whereTokenIndex = whereIndex
|
||||
parseGenericTypes(from: whereIndex, to: openBraceIndex)
|
||||
}
|
||||
|
||||
// Parse the return type if present
|
||||
var returnTypeTokens: [Token]?
|
||||
if let returnIndex = formatter.index(of: .operator("->", .infix), after: paramListEndIndex),
|
||||
returnIndex < openBraceIndex
|
||||
{
|
||||
let returnTypeRange = (returnIndex + 1) ..< (whereTokenIndex ?? openBraceIndex)
|
||||
returnTypeTokens = Array(formatter.tokens[returnTypeRange])
|
||||
}
|
||||
|
||||
let genericParameterListRange = (genericSignatureStartIndex + 1) ..< genericSignatureEndIndex
|
||||
let genericParameterListTokens = formatter.tokens[genericParameterListRange]
|
||||
|
||||
let parameterListRange = (paramListStartIndex + 1) ..< paramListEndIndex
|
||||
let parameterListTokens = formatter.tokens[parameterListRange]
|
||||
|
||||
let bodyRange = (openBraceIndex + 1) ..< closeBraceIndex
|
||||
let bodyTokens = formatter.tokens[bodyRange]
|
||||
|
||||
for genericType in genericTypes {
|
||||
// If the generic type doesn't occur in the generic parameter list (<...>),
|
||||
// then we inherited it from the generic context and can't replace the type
|
||||
// with an opaque parameter.
|
||||
if !genericParameterListTokens.contains(where: { $0.string == genericType.name }) {
|
||||
genericType.eligbleToRemove = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the generic type occurs multiple times in the parameter list,
|
||||
// it isn't eligible to be removed. For example `(T, T) where T: Foo`
|
||||
// requires the two params to be the same underlying type, but
|
||||
// `(some Foo, some Foo)` does not.
|
||||
let countInParameterList = parameterListTokens.filter { $0.string == genericType.name }.count
|
||||
if countInParameterList > 1 {
|
||||
genericType.eligbleToRemove = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the generic type occurs in the body of the function, then it can't be removed
|
||||
if bodyTokens.contains(where: { $0.string == genericType.name }) {
|
||||
genericType.eligbleToRemove = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the generic type is used in a constraint of any other generic type, then the type
|
||||
// cant be removed without breaking that other type
|
||||
let otherGenericTypes = genericTypes.filter { $0.name != genericType.name }
|
||||
let otherTypeConformances = otherGenericTypes.flatMap { $0.conformances }
|
||||
for otherTypeConformance in otherTypeConformances {
|
||||
let conformanceTokens = formatter.tokens[otherTypeConformance.sourceRange]
|
||||
if conformanceTokens.contains(where: { $0.string == genericType.name }) {
|
||||
genericType.eligbleToRemove = false
|
||||
}
|
||||
}
|
||||
|
||||
// In some weird cases you can also have a generic constraint that references a generic
|
||||
// type from the parent context with the same name. We can't change these, since it
|
||||
// can cause the build to break
|
||||
for conformance in genericType.conformances {
|
||||
if tokenize(conformance.name).contains(where: { $0.string == genericType.name }) {
|
||||
genericType.eligbleToRemove = false
|
||||
}
|
||||
}
|
||||
|
||||
// A generic used as a return type is different from an opaque result type (SE-244).
|
||||
// For example in `-> T where T: Fooable`, the generic type is caller-specified,
|
||||
// but with `-> some Fooable` the generic type is specified by the function implementation.
|
||||
// Because those represent different concepts, we can't convert between them,
|
||||
// so have to mark the generic type as ineligible if it appears in the return type.
|
||||
if let returnTypeTokens = returnTypeTokens,
|
||||
returnTypeTokens.contains(where: { $0.string == genericType.name })
|
||||
{
|
||||
genericType.eligbleToRemove = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the method that generates the opaque parameter syntax doesn't succeed,
|
||||
// then this type is ineligible (because it used a generic constraint that
|
||||
// can't be represented using this syntax).
|
||||
if genericType.asOpaqueParameter == nil {
|
||||
genericType.eligbleToRemove = false
|
||||
continue
|
||||
}
|
||||
|
||||
// If the generic type is used as a closure type parameter, it can't be removed or the compiler
|
||||
// will emit a "'some' cannot appear in parameter position in parameter type <closure type>" error
|
||||
for tokenIndex in funcIndex ... closeBraceIndex {
|
||||
if
|
||||
// Check if this is the start of a closure
|
||||
formatter.tokens[tokenIndex] == .startOfScope("("),
|
||||
tokenIndex != paramListStartIndex,
|
||||
let endOfScope = formatter.endOfScope(at: tokenIndex),
|
||||
let tokenAfterParen = formatter.next(.nonSpaceOrCommentOrLinebreak, after: endOfScope),
|
||||
[.operator("->", .infix), .keyword("throws"), .identifier("async")].contains(tokenAfterParen),
|
||||
// Check if the closure type parameters contains this generic type
|
||||
formatter.tokens[tokenIndex ... endOfScope].contains(where: { $0.string == genericType.name })
|
||||
{
|
||||
genericType.eligbleToRemove = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let genericsEligibleToRemove = genericTypes.filter { $0.eligbleToRemove }
|
||||
let sourceRangesToRemove = Set(genericsEligibleToRemove.flatMap { type in
|
||||
[type.definitionSourceRange] + type.conformances.map { $0.sourceRange }
|
||||
})
|
||||
|
||||
// We perform modifications to the function signature in reverse order
|
||||
// so we don't invalidate any of the indices we've recorded. So first
|
||||
// we remove components of the where clause.
|
||||
if let whereIndex = formatter.index(of: .keyword("where"), after: paramListEndIndex),
|
||||
whereIndex < openBraceIndex
|
||||
{
|
||||
let whereClauseSourceRanges = sourceRangesToRemove.filter { $0.lowerBound > whereIndex }
|
||||
formatter.removeTokens(in: Array(whereClauseSourceRanges))
|
||||
|
||||
// if the where clause is completely empty now, we need to the where token as well
|
||||
if let newOpenBraceIndex = formatter.index(of: .nonSpaceOrLinebreak, after: whereIndex),
|
||||
formatter.token(at: newOpenBraceIndex) == .startOfScope("{")
|
||||
{
|
||||
formatter.removeTokens(in: whereIndex ..< newOpenBraceIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all of the uses of generic types that are eligible to remove
|
||||
// with the corresponding opaque parameter declaration
|
||||
for index in parameterListRange.reversed() {
|
||||
if
|
||||
let matchingGenericType = genericsEligibleToRemove.first(where: { $0.name == formatter.tokens[index].string }),
|
||||
var opaqueParameter = matchingGenericType.asOpaqueParameter
|
||||
{
|
||||
// If this instance of the type is followed by a `.` or `?` then we have to wrap the new type in parens
|
||||
// (e.g. changing `Foo.Type` to `some Any.Type` breaks the build, it needs to be `(some Any).Type`)
|
||||
if let nextToken = formatter.next(.nonSpaceOrCommentOrLinebreak, after: index),
|
||||
[.operator(".", .infix), .operator("?", .postfix)].contains(nextToken)
|
||||
{
|
||||
opaqueParameter.insert(.startOfScope("("), at: 0)
|
||||
opaqueParameter.append(.endOfScope(")"))
|
||||
}
|
||||
|
||||
formatter.replaceToken(at: index, with: opaqueParameter)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove types from the generic parameter list
|
||||
let genericParameterListSourceRanges = sourceRangesToRemove.filter { $0.lowerBound < genericSignatureEndIndex }
|
||||
formatter.removeTokens(in: Array(genericParameterListSourceRanges))
|
||||
|
||||
// If we left a dangling comma at the end of the generic parameter list, we need to clean it up
|
||||
if let newGenericSignatureEndIndex = formatter.endOfScope(at: genericSignatureStartIndex),
|
||||
let trailingCommaIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, before: newGenericSignatureEndIndex),
|
||||
formatter.tokens[trailingCommaIndex] == .delimiter(",")
|
||||
{
|
||||
formatter.removeTokens(in: trailingCommaIndex ..< newGenericSignatureEndIndex)
|
||||
}
|
||||
|
||||
// If we removed all of the generic types, we also have to remove the angle brackets
|
||||
if let newGenericSignatureEndIndex = formatter.index(of: .nonSpaceOrLinebreak, after: genericSignatureStartIndex),
|
||||
formatter.token(at: newGenericSignatureEndIndex) == .endOfScope(">")
|
||||
{
|
||||
formatter.removeTokens(in: genericSignatureStartIndex ... newGenericSignatureEndIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2577,4 +2577,224 @@ class SyntaxTests: RulesTests {
|
||||
"""
|
||||
testFormatting(for: input, output, rule: FormatRules.blockComments)
|
||||
}
|
||||
|
||||
// MARK: - opaqueGenericParameters
|
||||
|
||||
func testGenericNotModifiedBelowSwift5_7() {
|
||||
let input = """
|
||||
func foo<T>(_ value: T) {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.6")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithNoConstraint() {
|
||||
let input = """
|
||||
func foo<T>(_ value: T) {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo(_ value: some Any) {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithConstraintInBracket() {
|
||||
let input = """
|
||||
func foo<T: Fooable, U: Barable>(_ fooable: T, barable: U) -> Baaz {
|
||||
print(fooable, barable)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo(_ fooable: some Fooable, barable: some Barable) -> Baaz {
|
||||
print(fooable, barable)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithConstraintsInWhereClause() {
|
||||
let input = """
|
||||
func foo<T, U>(_ t: T, _ u: U) -> Baaz where T: Fooable, T: Barable, U: Baazable {
|
||||
print(t, u)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo(_ t: some Fooable & Barable, _ u: some Baazable) -> Baaz {
|
||||
print(t, u)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterCanRemoveOneButNotOthers_onOneLine() {
|
||||
let input = """
|
||||
func foo<S: Baazable, T: Fooable, U: Barable>(_ foo: T, bar1: U, bar2: U) where S.AssociatedType == Baaz, T: Quuxable, U: Qaaxable {
|
||||
print(foo, bar1, bar2)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo<S: Baazable, U: Barable>(_ foo: some Fooable & Quuxable, bar1: U, bar2: U) where S.AssociatedType == Baaz, U: Qaaxable {
|
||||
print(foo, bar1, bar2)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterCanRemoveOneButNotOthers_onMultipleLines() {
|
||||
let input = """
|
||||
func foo<
|
||||
S: Baazable,
|
||||
T: Fooable,
|
||||
U: Barable
|
||||
>(_ foo: T, bar1: U, bar2: U) where
|
||||
S.AssociatedType == Baaz,
|
||||
T: Quuxable,
|
||||
U: Qaaxable
|
||||
{
|
||||
print(foo, bar1, bar2)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo<
|
||||
S: Baazable,
|
||||
U: Barable
|
||||
>(_ foo: some Fooable & Quuxable, bar1: U, bar2: U) where
|
||||
S.AssociatedType == Baaz,
|
||||
U: Qaaxable
|
||||
{
|
||||
print(foo, bar1, bar2)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithUnknownAssociatedTypeConstraint() {
|
||||
// If we knew that `T.AssociatedType` was the protocol's primary
|
||||
// associated type we could update this to `value: some Fooable<Bar>`,
|
||||
// but we don't necessarily have that type information available.
|
||||
// - If primary associated types become very widespread, it may make
|
||||
// sense to assume (or have an option to assume) that this would work.
|
||||
let input = """
|
||||
func foo<T: Fooable>(_ value: T) where T.AssociatedType == Bar {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithAssociatedTypeConformance() {
|
||||
// There is no opaque generic parameter syntax that supports this type of constraint
|
||||
let input = """
|
||||
func foo<T: Fooable>(_ value: T) where T.AssociatedType: Bar {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testOpaqueGenericParameterWithKnownAssociatedTypeConstraint() {
|
||||
// For known types (like those in the standard library),
|
||||
// we are able to know their primary associated types
|
||||
let input = """
|
||||
func foo<T: Collection>(_ value: T) where T.Element == Foo {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let output = """
|
||||
func foo(_ value: some Collection<Foo>) {
|
||||
print(value)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testGenericTypeUsedInMultipleParameters() {
|
||||
let input = """
|
||||
func foo<T: Fooable>(_ first: T, second: T) {
|
||||
print(first, second)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testGenericTypeUsedInClosureMultipleTimes() {
|
||||
let input = """
|
||||
func foo<T: Fooable>(_ closure: (T) -> T) {
|
||||
closure(foo)
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testGenericTypeUsedAsReturnType() {
|
||||
// A generic used as a return type is different from an opaque result type (SE-244).
|
||||
// In `-> T where T: Fooable`, the generic type is caller-specified, but with
|
||||
// `-> some Fooable` the generic type is specified by the function implementation.
|
||||
// Because those represent different concepts, we can't convert between them.
|
||||
let input = """
|
||||
func foo<T: Fooable>() -> T {
|
||||
// ...
|
||||
}
|
||||
|
||||
func bar<T>() -> T where T: Barable {
|
||||
// ...
|
||||
}
|
||||
|
||||
func baaz<T: Baazable>() -> Set<SomeComplicatedNestedGeneric<T, Bar>> {
|
||||
// ...
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
|
||||
func testGenericTypeUsedAsReturnTypeAndParameter() {
|
||||
// Since we can't change the return value, we can't change any of the use cases of T
|
||||
let input = """
|
||||
func foo<T: Fooable>(_ value: T) -> T {
|
||||
value
|
||||
}
|
||||
|
||||
func bar<T>(_ value: T) -> T where T: Barable {
|
||||
value
|
||||
}
|
||||
"""
|
||||
|
||||
let options = FormatOptions(swiftVersion: "5.7")
|
||||
testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user