I wrote a SwiftSyntax-based import-boundary linter (Clean Architecture / TCA)

As a freelancer I've worked on a lot of projects with legacy architecture, often a couple dozen SPM modules, Clean Architecture, several years of history, and it was usually rotting quietly in the same way...

A Presentation import that ended up in the data layer here, a view model reaching straight into a data toolkit instead of going through the domain there. Easy enough to spot in review on a small change, much less so once the codebase is huge.

What I actually wanted was simple: fail the build when a module imports across a boundary it isn't supposed to. I couldn't find something that did just that for Swift, so I ended up writing it.

It parses each file with SwiftSyntax (real syntax tree, no regex), works out which layer a file belongs to from a small config, and flags any import that points "outward". You describe the layers in a .solid.yml:

dependencyOrder: [Domain, Data, Presentation]
  layers:
    - name: Domain
      paths: [Sources/Domain/**]
    - name: Data
      paths: [Sources/Data/**]
    - name: Presentation
      paths: [Sources/Presentation/**]
      deny: [Data]   # the UI goes through the Domain, not straight to Data

and a violation comes out looking like a normal compiler diagnostic:

 Sources/Presentation/HomeView.swift:5: error: SolidLikeARock: layer 'Presentation' must not import 'Data'

A few things that came directly out of using it on a real codebase rather than designing it up front:

  • init generates a starter config from your actual import graph, so you're not writing the whole thing by hand. There's a --freeze mode that adopts it on a legacy project with zero violations on day one and then only fails on new ones;
  • basically baselining, which is the only realistic way I found to roll it out on an old codebase;
  • It knows about TCA: an isolatePeers rule for "feature modules must not import each other", and init --tca to group a TCA project automatically;
  • A graph command prints your layers as a Mermaid (or DOT) diagram straight from the imports, with rule-breaking edges in red. I mostly added it so the architecture diagram in the README stops lying.

It's deliberately small, SwiftSyntax + Yams, nothing else, and runs as a plain CLI, a SwiftPM build-tool plugin, a GitHub Action, or through Danger.

To be clear about what it isn't: it doesn't replace SwiftLint or Periphery, it's a different job. And it leans on the SwiftPM module graph, so plain Xcode targets without SPM modules aren't auto-discovered you'd write the config by hand there.

Repo (MIT): GitHub - nenadvulic/solid-like-a-rock: Architecture linter for Swift — enforce Clean Architecture import rules via SwiftSyntax. A CI-ready guardrail for AI-assisted development. · GitHub

I'd genuinely like feedback on the config model, whether the layer/dependencyOrder/deny shape maps onto how you actually structure your apps, and whether the violation reasons read clearly.

Happy to answer anything.

4 Likes

v0.9.0 — security checks

Since the original post, the linter grew a second half, an opt-in security, section that flags insecure patterns on the same syntax-tree pass as the import rules 14 rules across Keychain, Crypto, Network, Auth and Logging.

Things like kSecAttrAccessibleAlways, Insecure.MD5, tokens stored in UserDefaults, canEvaluatePolicy(_, error: nil), trust-all URLSession challenge handlers, privacy: .public on PII interpolations, or NSAllowsArbitraryLoads in the Info.plist.

Each rule is documented with what fires, why it matters, and just as important what it deliberately does not flag: security rules reference.

Feedback very welcome, especially on the rule set.

1 Like