Swift Quality Tools

swift-quality-tools

Twelve custom Swift lint rules built on SwiftSyntax AST analysis, plus three smart CLI wrappers that find their config automatically.

synodic-studio/swift-quality-tools· MIT

Standard linters match text patterns. The rules in this package analyze the abstract syntax tree. When a rule says “body must have exactly one top-level view,” it needs to understand Swift’s syntax: what counts as a top-level view, what counts as a modifier chain, what counts as a conditional. SwiftSyntax gives each rule access to the actual parsed structure of the code, so the rules can express architectural constraints that text-pattern linters cannot.

The package contains 12 custom rules, 3 smart CLI tools, a directive suppression system, parallel file processing, and Xcode build phase integration. 86 tests cover rule triggers, non-triggers, edge cases, and fix-it suggestions.


The Rules

The twelve rules group into four areas. Each enforces something a text-pattern linter cannot see.

View body

  • skimmable_body — a SwiftUI body may not exceed 15 lines, which forces extraction once it grows complex.
  • one_top_level_view — a body must have exactly one top-level view, so the implicit result builder stays predictable.
  • no_group_body — no bare Group as a container unless it actually carries modifiers.
  • no_if_modifier — bans the .if(condition) { ... } modifier, which breaks SwiftUI’s structural identity and its diffing.
  • no_if_without_else — flags if without else in a @ViewBuilder, where a view should not be deciding its own visibility.

Code quality

  • excessive_nesting — caps indentation depth at 3, measured from the AST so it survives reformatting and relaxes inside #Preview.
  • prefer_shorthand_optional_bindingif let foo = foo should be the shorthand if let foo.

View structure

  • stack_minimum_children — a VStack/HStack/ZStack should hold at least two children; a single child wants no stack.
  • single_modifier_per_line — one SwiftUI modifier per line.

Framework

  • prefer_swift_testing — flags XCTest in favor of Swift Testing, with migration hints (XCTAssertEqual becomes #expect(a == b)).
  • no_exported_import — bans @_exported import, an unstable underscore-prefixed Swift API.
  • prefer_zero_param_onchange — use the zero-parameter .onChange(of:) form when the old value is ignored.

Architecture

Parallel AST Processing

Files are processed concurrently via Swift’s TaskGroup:

Source files discovered
    ↓ filter by .swift extension
    ↓ exclude paths from .swiftlint.yml
    ↓ TaskGroup: one task per file
Each file → SwiftParser → SyntaxTree → 12 Visitors → Violations
    ↓ aggregate
Sorted violation report

Smart Config Discovery

Three CLI tools (swiftformat-smart, swiftlint-smart, swiftlintcustom-smart) find configuration automatically:

  1. Check for an explicit --config parameter.
  2. Search the current directory (error if multiple configs found).
  3. Walk up the directory tree to root.
  4. Fall back to a shared config at ~/Developer/swift-quality-tools/Configs/.

Works from any directory in any project. No path configuration needed.

Directive Suppression

Full SwiftLint-compatible suppression syntax with the swiftlintcustom: prefix:

// swiftlintcustom:disable:next skimmable_body
var body: some View { /* allowed to exceed 15 lines */ }

// swiftlintcustom:disable excessive_nesting
// ... block region ...
// swiftlintcustom:enable excessive_nesting

Xcode Integration

Two-stage parsing works around Xcode’s build phase sandbox, which suppresses subprocess output. The tool detects the XCODE_VERSION_ACTUAL environment variable and adjusts output format. A shell wrapper re-echoes violations in Xcode’s expected format so they appear as inline warnings in the editor.


What to Take From This

If you are building Swift quality tooling for a codebase larger than yourself, two patterns from this package are worth lifting:

Push opinionated rules into AST analysis. Anything you can express as “the structure of this code is wrong, regardless of formatting” should live as a SwiftSyntax visitor. Anything you can express as “this character pattern is wrong” can stay in SwiftLint. The split is real and the AST side is much smaller than you think.

Make the CLI find its own config. Tools that require explicit --config flags get used wrong. Tools that walk up the directory tree to find their config get used right.

The full source is on GitHub at synodic-studio/swift-quality-tools, MIT-licensed.