> ## Documentation Index
> Fetch the complete documentation index at: https://ngquct-fix-json-import-detects-every-field.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Code Style

> Swift conventions, tooling, naming, and file organization rules

Lint covers `TablePro/` and `Packages/`, which includes `TableProPluginKit` through the symlink in
`TableProCore`. The rest of `Plugins/` and the app's own test bundles get no automated style check,
so on a change there the conventions on this page 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

```bash theme={null}
swiftlint lint --strict     # the pre-commit gate, and what the release workflow runs
swiftlint --fix             # apply the autocorrectable rules
swiftformat .               # format in place
swiftformat --lint .        # report without writing
```

`--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

| Rule | Value | Set by |
| - | - | - |
| Indentation | 4 spaces, never tabs; `case` not indented inside `switch` | `--indent 4`, `--indentcase false` |
| Wrap target | 120 characters | `--maxwidth 120` |
| Hard line limit | Warns at 180, errors at 300 | `line_length` in `.swiftlint.yml` |
| Braces | K\&R, `else` and `guard else` on the closing brace line | `--allman false`, `--elseposition same-line`, `--guardelse same-line` |
| Wrapped arguments | One per line, opening paren last, closing paren balanced | `--wraparguments before-first`, `--closingparen balanced` |
| Line endings | LF | `--linebreaks lf` |
| Semicolons | Removed | `--semicolons never` |
| `self.` | Removed where the compiler does not need it | `--self remove` |

`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

| Element | Convention | Example |
| - | - | - |
| Types and protocols | UpperCamelCase | `DatabaseConnection` |
| Enum cases | lowerCamelCase | `.postgresql` |
| Functions, variables, constants | lowerCamelCase | `executeQuery()`, `maxRetryAttempts` |
| Booleans | `is`, `has`, `can` or `should` prefix | `isConnected`, `hasValidCredentials` |
| Factory methods | `make` prefix | `makeConnection()` |
| Acronyms | All caps, as in Apple's own APIs | `SQLStatementGenerator`, `JSONExportPlugin`, `MCPAuditLogger` |

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.

```swift theme={null}
import AppKit
import Foundation
import os
import TableProEditorKit
import TableProPluginKit

final class QueryRunner { … }
```

Lowercase module names sort by their own spelling, which is why `os` lands between `Foundation` and
`TableProEditorKit`.

## Rules that bite

`.swiftlint.yml` opts into 55 rules beyond the defaults. These are the ones that stop a clean-looking
change most often:

| Rule | Flags | Severity |
| - | - | - |
| `implicit_return` | `return` in a single-expression function, closure or getter | warning |
| `explicit_init` | `Connection.init()` where `Connection()` compiles | warning |
| `force_unwrapping` | `!` on an optional | warning |
| `force_cast` | `as!` | warning |
| `empty_count` | `items.count == 0` instead of `items.isEmpty` | error |
| `number_separator` | `1000000` instead of `1_000_000` | warning |
| `yoda_condition` | `if 5 == limit` | warning |
| `sorted_imports` | An import out of alphabetical order | warning |
| `extension_access_modifier` | Access control repeated on members instead of set on the extension | warning |

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:

| Rule | Fires on | Exempt |
| - | - | - |
| `storage_environment_directory` | `applicationSupportDirectory` | `AppStorageEnvironment.swift` |
| `storage_environment_defaults` | `UserDefaults.standard` | `AppStorageEnvironment.swift`, `GeneralSettings.swift`, `WorkspaceRailViewController.swift` |

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:

<Tree>
  <Tree.File name="MainContentCoordinator.swift" />

  <Tree.Folder name="Extensions" defaultOpen>
    <Tree.File name="MainContentCoordinator+RowOperations.swift" />

    <Tree.File name="MainContentCoordinator+Pagination.swift" />

    <Tree.File name="MainContentCoordinator+Filtering.swift" />

    <Tree.File name="MainContentCoordinator+Alerts.swift" />
  </Tree.Folder>
</Tree>

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:

```swift theme={null}
String(localized: "Preview \(name)")
```

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:

```swift theme={null}
String(format: String(localized: "Preview %@"), name)
```

Technical terms stay unlocalized: font names, database types, SQL keywords, encoding names.
Exporting and merging a translation is on [Development Overview](/development/overview).
