Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update redundantEquatable rule to support nested types #1895

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions Sources/DeclarationHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ enum Declaration: Hashable {

/// The name of this type or variable
var name: String? {
// Conditional compilation blocks don't have a "name"
guard keyword != "#if" else { return nil }

let parser = Formatter(openTokens)
guard let keywordIndex = openTokens.firstIndex(of: .keyword(keyword)),
let nameIndex = parser.index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex)
Expand Down Expand Up @@ -191,6 +194,13 @@ enum Declaration: Hashable {
func originalKeywordIndex(in formatter: Formatter) -> Int? {
formatter.index(of: .keyword(keyword), after: originalRange.lowerBound - 1)
}

/// Computes the fully qualified name of this declaration, given the array of parent declarations.
func fullyQualifiedName(parentDeclarations: [Declaration]) -> String? {
guard let name = name else { return nil }
let typeNames = parentDeclarations.compactMap(\.name) + [name]
return typeNames.joined(separator: ".")
}
}

extension Formatter {
Expand Down Expand Up @@ -808,8 +818,8 @@ extension Declaration {

extension Formatter {
/// Recursively calls the `operation` for every declaration in the source file
func forEachRecursiveDeclaration(_ operation: (Declaration, _ parent: Declaration?) -> Void) {
parseDeclarations().forEachRecursiveDeclaration(operation: operation, parent: nil)
func forEachRecursiveDeclaration(_ operation: (Declaration, _ parents: [Declaration]) -> Void) {
parseDeclarations().forEachRecursiveDeclaration(operation: operation, parents: [])
}

/// Applies `mapRecursiveDeclarations` in place
Expand All @@ -830,13 +840,13 @@ extension Formatter {
extension Array where Element == Declaration {
/// Applies `operation` to every recursive declaration of this array of declarations
func forEachRecursiveDeclaration(
operation: (Declaration, _ parent: Declaration?) -> Void,
parent: Declaration? = nil
operation: (Declaration, _ parents: [Declaration]) -> Void,
parents: [Declaration] = []
) {
for declaration in self {
operation(declaration, parent)
operation(declaration, parents)
if let body = declaration.body {
body.forEachRecursiveDeclaration(operation: operation, parent: declaration)
body.forEachRecursiveDeclaration(operation: operation, parents: parents + [declaration])
}
}
}
Expand Down
61 changes: 44 additions & 17 deletions Sources/Rules/RedundantEquatable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,30 +164,34 @@ extension Formatter {
/// Finds all of the types in the current file with an Equatable conformance,
/// which also have a manually-implemented `static func ==` method.
func manuallyImplementedEquatableTypes(in declarations: [Declaration]) -> [EquatableType] {
var typeDeclarationsByName: [String: Declaration] = [:]
var typesWithEquatableConformances: [(typeName: String, equatableConformanceIndex: Int)] = []
var equatableImplementations: [String: Declaration] = [:]
var typeDeclarationsByFullyQualifiedName: [String: Declaration] = [:]
var typesWithEquatableConformances: [(fullyQualifiedTypeName: String, equatableConformanceIndex: Int)] = []
var equatableImplementationsByFullyQualifiedName: [String: Declaration] = [:]

declarations.forEachRecursiveDeclaration { declaration, parentDeclaration in
declarations.forEachRecursiveDeclaration { declaration, parentDeclarations in
guard let declarationName = declaration.name else { return }
let fullyQualifiedName = declaration.fullyQualifiedName(parentDeclarations: parentDeclarations)

if declaration.definesType {
typeDeclarationsByName[declarationName] = declaration
if declaration.definesType, let fullyQualifiedName = fullyQualifiedName {
typeDeclarationsByFullyQualifiedName[fullyQualifiedName] = declaration
}

// Support the Equatable conformance being declared in an extension
// separately from the Equatable
if declaration.definesType || declaration.keyword == "extension",
let keywordIndex = declaration.originalKeywordIndex(in: self)
let keywordIndex = declaration.originalKeywordIndex(in: self),
let fullyQualifiedName = fullyQualifiedName
{
let conformances = parseConformancesOfType(atKeywordIndex: keywordIndex)

// Both an Equatable and Hashable conformance will cause the Equatable conformance to be synthesized
if let equatableConformance = conformances.first(where: {
$0.conformance == "Equatable" || $0.conformance == "Hashable"
}) {
typesWithEquatableConformances.append(
(typeName: declarationName, equatableConformanceIndex: equatableConformance.index))
typesWithEquatableConformances.append((
fullyQualifiedTypeName: fullyQualifiedName,
equatableConformanceIndex: equatableConformance.index
))
}
}

Expand All @@ -205,21 +209,44 @@ extension Formatter {
functionArguments[1].internalLabel == "rhs",
functionArguments[0].type == functionArguments[1].type
{
var typeName = functionArguments[0].type

// If the function uses `Self`, resolve that to the name of the parent type
if typeName == "Self", let parentDeclarationName = parentDeclaration?.name {
typeName = parentDeclarationName
var comparedTypeName = functionArguments[0].type

if let parentDeclaration = parentDeclarations.last {
// If the function uses `Self`, resolve that to the name of the parent type
if comparedTypeName == "Self",
let parentDeclarationName = parentDeclaration.fullyQualifiedName(parentDeclarations: parentDeclarations.dropLast())
{
comparedTypeName = parentDeclarationName
}

// If the function uses `Bar` in an extension `Foo.Bar`, then resolve
// the name of the compared type to be the fully-qualified `Foo.Bar` type.
if parentDeclaration.keyword == "extension",
let extendedType = parentDeclaration.name,
comparedTypeName != extendedType,
extendedType.hasSuffix("." + comparedTypeName)
{
comparedTypeName = extendedType
}

// If the function uses `Bar` in a type `Bar`, then resolve the
// the name of the compared type to be the fully-qualified parent type.
// - For example, `Bar` could be defined in a parent `Foo` type.
if comparedTypeName == parentDeclaration.name,
let parentDeclarationName = parentDeclaration.fullyQualifiedName(parentDeclarations: parentDeclarations.dropLast())
{
comparedTypeName = parentDeclarationName
}
}

equatableImplementations[typeName] = declaration
equatableImplementationsByFullyQualifiedName[comparedTypeName] = declaration
}
}
}

return typesWithEquatableConformances.compactMap { typeName, equatableConformanceIndex in
guard let typeDeclaration = typeDeclarationsByName[typeName],
let equatableImplementation = equatableImplementations[typeName]
guard let typeDeclaration = typeDeclarationsByFullyQualifiedName[typeName],
let equatableImplementation = equatableImplementationsByFullyQualifiedName[typeName]
else { return nil }

return EquatableType(
Expand Down
139 changes: 139 additions & 0 deletions Tests/Rules/RedundantEquatableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,143 @@ final class RedundantEquatableTests: XCTestCase {
options: options
)
}

func testRemoveSimpleEquatableConformanceOnNestedType() {
let input = """
enum Foo {
enum Bar {
struct Baaz: Equatable {
let foo: String
let bar: String

static func ==(lhs: Baaz, rhs: Baaz) -> Bool {
lhs.foo == rhs.foo
&& lhs.bar == rhs.bar
}
}
}
}
"""

let output = """
enum Foo {
enum Bar {
struct Baaz: Equatable {
let foo: String
let bar: String
}
}
}
"""

testFormatting(for: input, [output], rules: [.redundantEquatable, .blankLinesAtEndOfScope])
}

func testRemoveSimpleSelfEquatableConformanceOnNestedType() {
let input = """
enum Foo {
enum Bar {
struct Baaz: Equatable {
let foo: String
let bar: String

static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.foo == rhs.foo
&& lhs.bar == rhs.bar
}
}
}
}
"""

let output = """
enum Foo {
enum Bar {
struct Baaz: Equatable {
let foo: String
let bar: String
}
}
}
"""

testFormatting(for: input, [output], rules: [.redundantEquatable, .blankLinesAtEndOfScope])
}

func testRemoveSimpleEquatableConformanceOnNestedTypeWithExtension() {
let input = """
enum Foo {
enum Bar {
struct Baaz {
let foo: String
let bar: String
}
}
}

extension Foo.Bar.Baaz: Equatable {
static func ==(lhs: Baaz, rhs: Baaz) -> Bool {
lhs.foo == rhs.foo
&& lhs.bar == rhs.bar
}
}
"""

let output = """
enum Foo {
enum Bar {
struct Baaz {
let foo: String
let bar: String
}
}
}

extension Foo.Bar.Baaz: Equatable {}
"""

testFormatting(for: input, [output], rules: [.redundantEquatable, .emptyBraces])
}

func testAdoptsEquatableMacroOnNestedTypeWithExtension() {
let input = """
enum Foo {
enum Bar {
final class Baaz {
let foo: String
let bar: String
}
}
}

extension Foo.Bar.Baaz: Equatable {
static func ==(lhs: Self, rhs: Self) -> Bool {
lhs.foo == rhs.foo
&& lhs.bar == rhs.bar
}
}
"""

let output = """
import MyEquatableMacroLib

enum Foo {
enum Bar {
@Equatable
final class Baaz {
let foo: String
let bar: String
}
}
}

"""

let options = FormatOptions(
typeAttributes: .prevLine,
equatableMacroInfo: EquatableMacroInfo(rawValue: "@Equatable,MyEquatableMacroLib")
)

testFormatting(for: input, [output], rules: [.redundantEquatable, .emptyBraces, .wrapAttributes, .emptyExtension, .consecutiveBlankLines], options: options)
}
}