Harmonize: Add Transitive Protocol Conformance Support
In the realm of software architecture, maintaining consistency and adherence to architectural patterns is paramount, especially in large projects. One powerful tool for enforcing such rules in Swift projects is Harmonize. This article delves into a common challenge faced when using Harmonize to validate protocol conformances, specifically transitive protocol conformances, and proposes a solution to enhance its capabilities.
The Challenge: Verifying Protocol Conformances in Complex Architectures
When setting up a large project, architectural patterns like MVVM+Coordinator are crucial for maintaining a clean and organized codebase. Harmonize can be employed to enforce these patterns, ensuring that specific classes conform to certain protocols. For instance, a test can be implemented to verify that all coordinators in the project implement a base coordinator protocol.
@Test func allCoordinatorsImplementCoordinatorBase() {
let coordinators = Harmonize
.productionCode()
.classes()
.withNameEndingWith("CoordinatorImpl")
coordinators.assertTrue(message: "All Coordinators must conform to CoordinatorBase") {
$0.conforms(to: "CoordinatorBase")
}
}
This approach works seamlessly for classes that directly implement the target protocol. However, a problem arises when dealing with protocols that are implemented indirectly through inheritance. Let's illustrate this with an example:
// ✅ works, since directly implemented
final class FooCoordinatorImpl: CoordinatorBase {
// impl left out for brevity
}
// ❌ does not work, since the protocol is only implemented indirectly
protocol CoordinatorThrows: CoordinatorBase {
func start() throws
}
final class BarCoordinatorImpl: CoordinatorThrows {
// impl left out for brevity
}
In this scenario, FooCoordinatorImpl
directly conforms to CoordinatorBase
, and the Harmonize test correctly identifies this. However, BarCoordinatorImpl
conforms to CoordinatorBase
indirectly through the CoordinatorThrows
protocol. The standard Harmonize test, as shown above, fails to recognize this transitive conformance, leading to false negatives and undermining the effectiveness of the architectural enforcement.
Understanding Transitive Protocol Conformance
Before diving into the solution, it's crucial to understand the concept of transitive protocol conformance. In Swift, a protocol can inherit from another protocol, creating a hierarchy. When a class conforms to a protocol, it implicitly conforms to all protocols in its inheritance hierarchy. This is transitive conformance. In the example above, CoordinatorThrows
inherits from CoordinatorBase
, so any class conforming to CoordinatorThrows
also conforms to CoordinatorBase
.
The challenge lies in programmatically determining these indirect conformances. Harmonize's default behavior only checks for direct conformance, necessitating a more sophisticated approach to handle transitive cases.
The Need for a Robust Solution
In complex projects, indirect protocol conformances are common and often desirable for creating more fine-grained abstractions. A robust solution for verifying protocol conformance must account for these transitive relationships to provide accurate and reliable results. Without this capability, Harmonize's utility in enforcing architectural rules is significantly diminished.
Workaround: A Deep Dive into Inheritance Resolution
To address the challenge of transitive protocol conformance, a workaround involves digging deeper into the protocol inheritance hierarchy. This approach resolves the listed protocol names and examines their inheritance clauses to identify all transitively implemented protocols. The core of the workaround lies in extending the InheritanceProviding
functionality to recursively traverse the protocol inheritance graph.
extension InheritanceProviding {
func conforms(to protos: [String], allowIndirectConformance: Bool, strict: Bool = false) -> Bool {
// MARK: – Name matching rules
@inline(__always)
func matches(_ candidate: String, _ target: String) -> Bool {
if strict {
return candidate == target
}
// Case‑insensitive and allow for module‑qualified names (e.g. `MyModule.FooProtocol`).
return candidate.compare(target, options: [.caseInsensitive]) == .orderedSame ||
candidate.hasSuffix("." + target) ||
candidate == target
}
// 1️⃣ Direct conformance first – cheapest path.
let hasAllDirect = protos.allSatisfy { proto in
inheritanceTypesNames.contains { matches($0, proto) }
}
if !allowIndirectConformance || hasAllDirect {
return hasAllDirect
}
// 2️⃣ Walk the protocol‑inheritance graph to gather *all* transitive protocols.
var discovered: Set<String> = []
var stack: [String] = inheritanceTypesNames
while let currentName = stack.popLast() {
guard let decl = protocolsByName[currentName] else { continue }
if !discovered.insert(currentName).inserted { continue }
stack.append(contentsOf: decl.inheritanceTypesNames)
}
// 3️⃣ Verify every requested protocol is either direct or discovered.
return protos.allSatisfy { proto in
inheritanceTypesNames.contains { matches($0, proto) } ||
discovered.contains { matches($0, proto) }
}
}
}
/// Lazily loads and caches **all** protocols in the production code once, keyed by exact name.
private nonisolated(unsafe) var protocolsByName: [String: any InheritanceProviding] = {
let allProtocols = Harmonize.productionCode().protocols()
return Dictionary(uniqueKeysWithValues: allProtocols.map { ($0.name, $0) })
}()
Breaking Down the Workaround
The workaround functions in three primary stages:
- Direct Conformance Check: The function first checks for direct conformance to the specified protocols. This is the most efficient path and serves as the initial filter. If direct conformance is sufficient or
allowIndirectConformance
is disabled, the function returns the result of this check. - Transitive Protocol Discovery: If indirect conformance is allowed, the function walks the protocol inheritance graph. It maintains a
discovered
set to track visited protocols and astack
to manage the traversal. Starting from the initial inheritance types, it iteratively explores each protocol's inheritance types, adding them to thediscovered
set and thestack
until all transitive protocols are identified. - Verification: Finally, the function verifies that every requested protocol is either directly conformed to or has been discovered in the transitive protocol set. This ensures that all direct and indirect conformances are accounted for.
Key Components and Considerations
- Name Matching: The
matches
function handles protocol name comparisons, accounting for case-insensitivity and module-qualified names. This flexibility is crucial for handling various naming conventions and project structures. - Caching: The
protocolsByName
variable lazily loads and caches all protocols in the production code. This caching mechanism optimizes performance by avoiding redundant protocol lookups. - Performance: While this workaround effectively addresses the issue, it's essential to consider its performance implications. Walking the inheritance graph can be computationally intensive, especially in large projects with complex protocol hierarchies. Therefore, optimizing the traversal and caching mechanisms is vital.
Integrating the Workaround
To use this workaround, you would replace the standard conforms(to:)
check in your Harmonize tests with the extended version. This ensures that transitive protocol conformances are considered during the validation process. For example:
coordinators.assertTrue(message: "All Coordinators must conform to CoordinatorBase") {
$0.conforms(to: ["CoordinatorBase"], allowIndirectConformance: true)
}
By setting allowIndirectConformance
to true
, you instruct the extended conforms(to:)
function to perform the transitive protocol discovery.
Proposal: Enhancing Harmonize with Built-in Transitive Protocol Support
While the workaround provides a functional solution, it introduces complexity and potential performance overhead. A more elegant and efficient approach would be to integrate transitive protocol conformance checking directly into Harmonize. This would streamline the testing process and eliminate the need for custom extensions.
Benefits of Built-in Support
- Simplified Testing: Built-in support would simplify test implementation, making it easier to verify protocol conformances without writing custom code.
- Improved Performance: Native support within Harmonize could be optimized for performance, potentially surpassing the efficiency of the workaround.
- Enhanced Readability: The testing code would become more readable and maintainable, as the conformance checks would be expressed in a clear and concise manner.
- Consistency: Integrating the feature into Harmonize ensures consistent behavior across different projects and testing scenarios.
Implementation Considerations
Implementing transitive protocol conformance checking in Harmonize would involve modifying the core logic of the conforms(to:)
function. The implementation would need to recursively traverse the protocol inheritance graph, similar to the workaround. However, it could leverage Harmonize's internal data structures and caching mechanisms for improved efficiency.
Proposed API Extension
To maintain backward compatibility and provide flexibility, a new parameter could be added to the conforms(to:)
function to enable transitive conformance checking. For example:
$0.conforms(to: "CoordinatorBase", allowTransitiveConformance: true)
This would allow developers to explicitly specify whether transitive conformance should be considered, ensuring that existing tests are not affected by the change.
Conclusion: Towards More Robust Architectural Enforcement
Enforcing architectural patterns is crucial for maintaining code quality and consistency in large projects. Harmonize is a valuable tool for this purpose, but its current limitation in handling transitive protocol conformances presents a challenge. The workaround discussed in this article provides a functional solution, but integrating built-in support for transitive conformance checking would significantly enhance Harmonize's capabilities. By incorporating this feature, Harmonize can become an even more powerful tool for ensuring architectural integrity in Swift projects.
Guys, what do you think about adding a feature like this to the library? It would make Harmonize even more powerful and user-friendly for enforcing architectural rules in Swift projects. Let's discuss the possibilities and work towards a more robust solution for verifying protocol conformances!