Skip to content

Repository files navigation

Harmonize

Tests

🎤 Harmonize is coming to a conference near you! We'll be presenting it at NSSpain (Logroño, September 16–18, 2026) and Swift Connection (Paris, November 2–3, 2026). Come say hi!

Harmonize is a modern linter for Swift that allows you to assert, validate, and harmonize your code’s structure and architecture by writing lint rules as unit tests—using Quick, XCTest, or Swift Testing.

This allows your team to keep your codebase clean, maintainable, and consistent as it grows, without relying on manual code reviews.

Harmonize aims to solve the limitations of regex-based linters such as SwiftLint, which focus primarily on Swift style and simple conventions. Inspired by Konsist for Kotlin and ArchUnit for Java, Harmonize provides a richer, semantic way to enforce your project's architecture and structural guidelines.

Architectural linters in the era of AI-generated code

AI-generated code can help teams move faster, but it can also introduce architectural flaws and subtle bugs that are hard to spot in manual code reviews. Harmonize gives your codebase deterministic guardrails by turning your team’s architectural and structural rules into unit tests. When AI-generated code violates those rules, the tests fail, giving your AI agent clear feedback to fix its own mistakes.

Usage

With Harmonize, you can write a lint rule similarly as you would write a unit test:

Example using Quick:

import Harmonize
import Quick

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")
                }
            }
        }
    }
}

Example using XCTest:

import Harmonize
import XCTest

final class ViewModelsInheritBaseViewModelSpec: XCTestCase {
    func testViewModels() throws {
        let viewModels = Harmonize.productionCode().classes()
            .withNameEndingWith("ViewModel")
        
        viewModels.assertTrue(message: "All ViewModels must inherit from BaseViewModel") {
            $0.inherits(from: "BaseViewModel")
        }
    }
}

Example using Swift Testing:

import Harmonize
import Testing

@Test
func viewModelsInheritBaseViewModel() {
    let viewModels = Harmonize.productionCode().classes()
        .withNameEndingWith("ViewModel")
        
    viewModels.assertTrue(message: "All ViewModels must inherit from BaseViewModel") {
        $0.inherits(from: "BaseViewModel")
    }
}

This lint rule enforces all ViewModels to inherit from BaseViewModel. Since it runs as a unit test, it will fail once it detects a violation. You can add exceptions or a baseline to this rule using the withoutName function:

let viewModels = Harmonize.productionCode().classes()
    .withNameEndingWith("ViewModel")
    .withoutName(["LegacyViewModel"])

You can create similar rules for any architectural or structural pattern that you want to enforce.

Unlike regex-based linters such as SwiftLint, Harmonize provides you with a rich and simple API to directly access any component in your codebase—including files, packages, classes, functions, and properties—and make assertions about them.

Adding context to error messages

Instead of passing a plain message string in the error message of a lint rule, you can pass a Rule with additional context. This helps AI agents understand and fix the violations more reliably:

viewModels.assertTrue(rule: rule) {
    $0.inherits(from: "BaseViewModel")
}

private static let rule = Rule(
    description: "ViewModels inherit from BaseViewModel.",
    rationale: "BaseViewModel provides the lifecycle callbacks and stores Combine cancellables used by our ViewModels.",
    fixHint: "Declare the class as `final class MyViewModel: BaseViewModel`.",
    badExample: "final class MyViewModel { }",
    goodExample: "final class MyViewModel: BaseViewModel { }"
)

When the assertion fails, each violation is reported at its source location along with the full rule context:

RULE: ViewModels inherit from BaseViewModel.

WHY: BaseViewModel provides the lifecycle callbacks and stores Combine cancellables used by our ViewModels.

HOW TO FIX: Declare the class as `final class MyViewModel: BaseViewModel`.

❌ BAD:
final class MyViewModel { }

✅ GOOD:
final class MyViewModel: BaseViewModel { }

Installation

Swift Package Manager (SPM)

To add Harmonize using Swift Package Manager, follow these steps:

In Xcode:

  • Go to File > Add Package Dependencies....
  • Enter the repository URL: https://github.com/perrystreetsoftware/Harmonize.git.
  • Add it as a dependency to your test target

Or, manually add it to your Package.swift file:

.package(url: "https://github.com/perrystreetsoftware/Harmonize.git", from: "0.2.0"),

You can optionally create a dedicated Swift package for your Harmonize lint rules to separate them from the rest of the unit tests.

Configuration

Add an empty .harmonize.yaml file to the root of your project. This file is required for Harmonize to correctly detect the project root and apply your lint rules.

You can also optionally specify which files or folders you want to exclude from all lint rules, using the excludes key:

excludes:
  - Package.swift

Integrating with CI/CD

Since Harmonize lint rules run as unit tests, you can integrate them easily into your existing CI/CD pipeline and automate them, similarly as you would automate your unit tests. Here’s an example of a GitHub Action that runs all your Harmonize lint rules when a pull request is opened:

name: Run Harmonize Lint Rules
on:
  pull_request:
    types: [opened, synchronize]

jobs:
  harmonize:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Harmonize Rules (macOS)
        run: |
          xcodebuild -scheme YourHarmonizeTestScheme -sdk macosx test

Integrate Harmonize with AI agents

In your AGENTS.md or CLAUDE.md file, depending on the coding agent you use, add the following instruction to run the lint rules after every task:

- After implementing a task, run the Harmonize lint rules using the `run-harmonize` skill.

Add the run-harmonize/SKILL.md into your project and adjust the Swift package path if your rules live somewhere other than SwiftPackages/HarmonizeRules:

---
name: run-harmonize
description: Runs the Harmonize lint rules and fixes violations. Use after implementing a task.
---

Harmonize rules are unit tests in `SwiftPackages/HarmonizeRules/Tests/HarmonizeRulesTests/`.

## How to run

```bash
# All rules
swift test --package-path SwiftPackages/HarmonizeRules

# One rule, by its class name
swift test --package-path SwiftPackages/HarmonizeRules --filter ViewModelsInheritBaseViewModel
```

## How to fix violations

For each failing rule:

1. Read the failure. `RULE` says what is enforced, `WHY` explains the reasoning,
`HOW TO FIX` says what to change, `❌ BAD` and `✅ GOOD` show the pattern to replace and the one to use.
2. Fix the production code the failure points to. Do not add it to the baseline.
3. Re-run only that rule with `--filter <RuleName>`.
4. Repeat until it passes, then run the full suite once more.

Articles

Example project

See how our Woof demo app is using Harmonize to enforce its architecture. Woof mirrors the patterns we use in our production iOS apps, so its lint rules are a good starting point for your own project.

Contributing

All contributions are welcome through pull requests, issues, or discussions.

About

Harmonize is a modern linter for Swift that allows you to write architectural lint rules as unit tests. In the era of AI-generated code, it provides your team with deterministic guardrails to keep your codebase clean, maintainable, and consistent as it grows.

Topics

Resources

Code of conduct

Stars

335 stars

Watchers

7 watching

Forks

Releases

Used by

Contributors

Languages