TablePro/ and nothing else. Plugins/, Packages/, LocalPackages/ and the test
bundles get no automated style check at all, locally or in CI. On a change outside the app target,
the conventions here are all there is.
.swiftlint.yml and .swiftformat settle the mechanical half. CLAUDE.md carries the rest, the
rules no linter can check: no comments, early returns over nested conditionals, explicit access
control, String(localized:) for user-facing strings, OSLog instead of print().
Running the tools
--strict promotes every warning to an error. Several rules are configured as warnings on purpose,
so a plain swiftlint lint exits 0 on code the release workflow rejects. There is no SwiftLint
build phase in the generated project, so nothing lints during a normal Xcode build.
The scope comes from included: [TablePro] in .swiftlint.yml, and that key beats any path on the
command line: swiftlint lint Plugins/ lints TablePro/ and reports nothing about your plugin. It
exits 0, which reads like a pass.
SwiftFormat is not run in CI at all. It rewrites files, so run it before you stage, not after.
Formatting
line_length ignores URLs, function declarations, comments and interpolated strings, so a long
signature or a long log line is not what trips it.
Trailing commas are omitted throughout the tree, and nothing enforces that: SwiftFormat has
trailingCommas disabled and SwiftLint has trailing_comma in disabled_rules. Match what the
file around you does.
Naming
Nothing enforces the acronym row: SwiftFormat’s
acronyms rule is disabled, so no tool rewrites
Url to URL. One corner of the tree disagrees with it. The MCP wire types under
TablePro/Core/MCP/Wire/ spell it HttpRequestParser while the files beside them spell MCP in
full. New types take the all-caps form.
Imports
One alphabetical block, no blank lines inside it, one blank line after it. SwiftFormat enforces this through--importgrouping alpha, blankLinesBetweenImports and blankLineAfterImports, and
SwiftLint’s sorted_imports catches an out-of-order import that never went through SwiftFormat.
os lands between Foundation and
TableProPluginKit.
Rules that bite
.swiftlint.yml opts into 55 rules beyond the defaults. These are the ones that stop a clean-looking
change most often:
Every warning in that table fails under
--strict, which is how the gate runs it.
force_try sits in disabled_rules, so nothing flags try!. The rule against forcing still
applies; the linter is not what holds it up.
Custom rules
Two project rules are configured at error severity, so they fire without--strict. Violate either
and a UI test writes into your own store instead of a throwaway one:
Resolve the directory through
AppStorageEnvironment.shared and preferences through
AppStorageEnvironment.shared.defaults. Only a preference macOS itself owns reads the standard
domain.
Size limits
.swiftlint.yml sets thresholds for file_length, type_body_length, function_body_length and
cyclomatic_complexity. Read the current numbers there rather than from memory.
When a type approaches one, split it into TypeName+Category.swift files under an Extensions/
folder beside it, grouped by domain and not by line count. MainContentCoordinator is the worked
example:
MainContentCoordinator.swift
Extensions
MainContentCoordinator+RowOperations.swift
MainContentCoordinator+Pagination.swift
MainContentCoordinator+Filtering.swift
MainContentCoordinator+Alerts.swift
scripts/generate-project.sh runs again.
Localization
CLAUDE.md holds the localization rule in full. The trap worth repeating is interpolation:

