Skip to main content
Lint covers 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.
Lowercase module names sort by their own spelling, which is why 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
A new file is invisible to Xcode until scripts/generate-project.sh runs again.

Localization

CLAUDE.md holds the localization rule in full. The trap worth repeating is interpolation:
That builds a different key on every call, so it never matches an entry in the strings catalog. The string ships untranslated and no tool reports it. Take a format argument instead, which is one key and one catalog entry:
Technical terms stay unlocalized: font names, database types, SQL keywords, encoding names. Exporting and merging a translation is on Development Overview.