diff --git a/Rules.md b/Rules.md
index c29a9e6d..ac2e1589 100644
--- a/Rules.md
+++ b/Rules.md
@@ -24,6 +24,7 @@
* [linebreaks](#linebreaks)
* [modifierOrder](#modifierOrder)
* [numberFormatting](#numberFormatting)
+* [opaqueGenericParameters](#opaqueGenericParameters)
* [preferKeyPath](#preferKeyPath)
* [redundantBackticks](#redundantBackticks)
* [redundantBreak](#redundantBreak)
@@ -907,6 +908,36 @@ Option | Description
+## 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`.
+
+
+Examples
+
+```diff
+- func handle(_ value: T) {
++ func handle(_ value: some Fooable) {
+ print(value)
+ }
+
+- func handle(_ value: T) where T: Fooable, T: Barable {
++ func handle(_ value: some Fooable & Barable) {
+ print(value)
+ }
+
+- func handle(_ value: T) where T.Element == Foo {
++ func handle(_ value: some Collection) {
+ print(value)
+ }
+```
+
+
+
+
## organizeDeclarations
Organizes declarations within class, struct, enum, actor, and extension bodies.
diff --git a/Sources/Examples.swift b/Sources/Examples.swift
index b6f555cb..f0f28af1 100644
--- a/Sources/Examples.swift
+++ b/Sources/Examples.swift
@@ -1408,4 +1408,23 @@ private struct Examples {
}
```
"""
+
+ let opaqueGenericParameters = """
+ ```diff
+ - func handle(_ value: T) {
+ + func handle(_ value: some Fooable) {
+ print(value)
+ }
+
+ - func handle(_ value: T) where T: Fooable, T: Barable {
+ + func handle(_ value: some Fooable & Barable) {
+ print(value)
+ }
+
+ - func handle(_ value: T) where T.Element == Foo {
+ + func handle(_ value: some Collection) {
+ print(value)
+ }
+ ```
+ """
}
diff --git a/Sources/Formatter.swift b/Sources/Formatter.swift
index 12235686..9b40f9c2 100644
--- a/Sources/Formatter.swift
+++ b/Sources/Formatter.swift
@@ -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]) {
+ // 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)
diff --git a/Sources/Rules.swift b/Sources/Rules.swift
index fa85d9ef..346ac9f0 100644
--- a/Sources/Rules.swift
+++ b/Sources/Rules.swift
@@ -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`.
+ """,
+ 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 `` the generic parameter `name` is `T`.
+ let name: String
+ /// The source range within angle brackets where the generic parameter is defined
+ let definitionSourceRange: ClosedRange
+ /// 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
+ }
+
+ init(name: String, definitionSourceRange: ClosedRange) {
+ 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. ``)
+ 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 " 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)
+ }
+ }
+ }
}
diff --git a/Tests/RulesTests+Syntax.swift b/Tests/RulesTests+Syntax.swift
index 695f5466..7ddbdd99 100644
--- a/Tests/RulesTests+Syntax.swift
+++ b/Tests/RulesTests+Syntax.swift
@@ -2577,4 +2577,224 @@ class SyntaxTests: RulesTests {
"""
testFormatting(for: input, output, rule: FormatRules.blockComments)
}
+
+ // MARK: - opaqueGenericParameters
+
+ func testGenericNotModifiedBelowSwift5_7() {
+ let input = """
+ func foo(_ value: T) {
+ print(value)
+ }
+ """
+
+ let options = FormatOptions(swiftVersion: "5.6")
+ testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
+ }
+
+ func testOpaqueGenericParameterWithNoConstraint() {
+ let input = """
+ func foo(_ 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(_ 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: 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(_ foo: T, bar1: U, bar2: U) where S.AssociatedType == Baaz, T: Quuxable, U: Qaaxable {
+ print(foo, bar1, bar2)
+ }
+ """
+
+ let output = """
+ func foo(_ 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`,
+ // 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(_ 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(_ 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(_ value: T) where T.Element == Foo {
+ print(value)
+ }
+ """
+
+ let output = """
+ func foo(_ value: some Collection) {
+ print(value)
+ }
+ """
+
+ let options = FormatOptions(swiftVersion: "5.7")
+ testFormatting(for: input, output, rule: FormatRules.opaqueGenericParameters, options: options)
+ }
+
+ func testGenericTypeUsedInMultipleParameters() {
+ let input = """
+ func foo(_ 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(_ 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 {
+ // ...
+ }
+
+ func bar() -> T where T: Barable {
+ // ...
+ }
+
+ func baaz() -> Set> {
+ // ...
+ }
+ """
+
+ 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(_ value: T) -> T {
+ value
+ }
+
+ func bar(_ value: T) -> T where T: Barable {
+ value
+ }
+ """
+
+ let options = FormatOptions(swiftVersion: "5.7")
+ testFormatting(for: input, rule: FormatRules.opaqueGenericParameters, options: options)
+ }
}