We built Harmonize: A modern open-source linter for Swift that enforces architecture as unit tests

Hi everyone, we’ve just released the first version of Harmonize, a modern, open-source linter for Swift that lets your team enforce architecture and best practices through lint rules written as unit tests, using Quick, XCTest, or Swift Testing.

With Harmonize, you no longer need to rely on manual code reviews or complex regex-based SwiftLint rules.

Here’s an example rule that enforces all ViewModels to inherit from BaseViewModel:

final class ViewModelsInheritBaseViewModelSpec: QuickSpec {
    override func spec() {
        describe("ViewModels") {
            let viewModels = Harmonize.productionCode().classes()
                .withNameEndingWith("ViewModel")

            it("should inherit from BaseViewModel") {
                viewModels.assertTrue(message: "All ViewModels must inherit from BaseViewModel") {
                    $0.inherits(from: "BaseViewModel")
                }
            }
        }
    }
}

And here’s one that enforces self to be captured weakly in closures of ViewModels:

describe("ViewModel functions") {
    let viewModelFunctions = Harmonize.productionCode().classes()
        .withNameEndingWith("ViewModel")
        .functions()

    it("should capture self weakly in closures") {
        viewModelFunctions.assertTrue {
            $0.closures().filter(\.hasSelfReference).allSatisfy {
                $0.isCapturingWeak(valueOf: "self")
            }
        }
    }
}

This is the GitHub repository if you’d like to try Harmonize in your iOS project

And here’s an intro article that will walk you through it: Goodbye Code Reviews, Hello Harmonize: Enforce Your Architecture in Swift | ITNEXT

We’d love your feedback, ideas, and contributions!

8 Likes

Very interesting approach! Will definitely bring this under attention of iOS teams (multiple teams working on the same code base seems like a great use case).

Can I use this to enforce hexagonal architecture, in particular the dependency directions? And what you are allowed import in certain modules?

Kind of like “banned dependencies” in the Maven Enforcer Plug-In.

Hi @maartene thanks so much for the feedback!

You're right! This is a very common use case for Harmonize, and one that we actively use ourselves in our production apps.

For example, this lint rule would prohibit UseCases from being imported into the UI layer:

describe("View layer") {
    let viewLayer = Harmonize.productionCode()
        .on("Presentation/Screens").sources()

    it("does not import UseCases") {
        viewLayer.assertFalse {
            $0.imports().contains(where: {
                $0.name == "UseCase"
            })
        }
    }
}

Or you can make it even shorter using the withImport function:

it("does not import UseCases") {
    viewLayer.withImport("UseCase").assertEmpty()
}

There’s also an additional benefit compared to the Maven Enforcer Plugin.

With Harmonize, you can easily exclude certain files from a lint rule (i.e., by adding a baseline), so you don’t have to refactor everything at once.

With the Maven Enforcer Plugin, violating a rule would result in a build error, unless you wrote a custom rule, which can get quite complicated.