# Plugins, Run Scripts and Other Integrations

Back to [SKILL.md](../SKILL.md).

## Build tool plugin (first choice)

`SwiftLintBuildToolPlugin` comes from the `SimplyDanny/SwiftLintPlugins`
package. Homebrew and `PATH` tweaks are unnecessary.

### SwiftPM

```swift
// swift-tools-version: 5.9
import PackageDescription

let lint: Target.PluginUsage = .plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins")

let package = Package(
    name: "Orchard",
    dependencies: [
        // pin an exact, reviewed release
        .package(url: "https://github.com/SimplyDanny/SwiftLintPlugins", exact: "<reviewed-version>")
    ],
    targets: [
        .target(name: "Orchard", plugins: [lint]),
        .testTarget(name: "OrchardTests", dependencies: ["Orchard"], plugins: [lint])
    ]
)
```

### Xcode project without `Package.swift`

1. Choose Add Package Dependencies from the File menu and paste the
   SwiftLintPlugins URL.
2. For every target to lint: Build Phases > Run Build Tool Plug-ins, add
   `SwiftLintBuildToolPlugin`.
3. Trust the plugin when Xcode asks.

### Headless CI with `xcodebuild`

The trust prompt cannot be answered on a build agent, so skip validation:

```bash
xcodebuild -scheme Orchard -skipPackagePluginValidation -skipMacroValidation build
```

### What the plugin cannot do

- No `--fix`: build tool plugins get read-only access to sources.
- No command-line flags at all, `--baseline` included. Use the `baseline:` and
  `write_baseline:` config keys, or move to the command plugin or the CLI.
- Because it fires with each build, clean builds of big projects get
  noticeably slower.

## Command plugin

Same `SwiftLintPlugins` dependency, but invoked on demand through SwiftPM with
read-write access to sources. Anything after `--` goes straight to the CLI, so
`--fix`, `--baseline` and `analyze` all work.

```bash
swift package plugin swiftlint --fix           # rewrite correctable violations
swift package plugin swiftlint                 # plain lint
swift package plugin swiftlint -- --baseline .swiftlint.baseline --strict   # CLI flags after --
swift package plugin swiftlint -- analyze --compile-commands .build/debug.yaml
```

## Run script build phase

Reach for this when the plugin does not fit or you want CLI-only behaviour in
Xcode, such as a local `--fix` or `--baseline`. Note that a plain Xcode project
can still use the plugin through a package dependency, no `Package.swift`
needed.

Create it from the target's Build Phases tab (the + button, then New Run
Script Phase) and move it so it sits below Compile Sources. SwiftLint assumes code that compiles; linting
earlier produces confusing results.

```bash
if [ "$(uname -m)" = "arm64" ]; then
  PATH="/opt/homebrew/bin:${PATH}"; export PATH
fi

command -v swiftlint > /dev/null || {
  echo "warning: SwiftLint missing. Install it with: brew install swiftlint"
  exit 0
}
swiftlint
```

- Homebrew on Apple silicon installs to `/opt/homebrew/bin/swiftlint`, which is
  not on Xcode's default `PATH`. Export it as above, or symlink the binary into
  `/usr/local/bin`.
- User script sandboxing (`ENABLE_USER_SCRIPT_SANDBOXING`) is on by default in
  projects created with Xcode 15 or later. When the build log shows a
  `deny(1) file-read-data` sandbox error naming swiftlint, set
  `ENABLE_USER_SCRIPT_SANDBOXING = NO` on that target.

### Limiting the files a run script lints

Input files are an optional speed-up on top of the basic script:

1. Turn sandboxing off if the script needs it.
2. Tick "Based on dependency analysis".
3. List sources under Input Files, or point Input File Lists at `.xcfilelist`
   files.

Then tell SwiftLint where to read them from:

- `--use-script-input-file-lists` reads `SCRIPT_INPUT_FILE_LIST_COUNT` and
  `SCRIPT_INPUT_FILE_LIST_0`, `_1`, and so on; each points at a readable
  `.xcfilelist`.
- `--use-script-input-files` reads `SCRIPT_INPUT_FILE_COUNT` and
  `SCRIPT_INPUT_FILE_0`, `_1`, and so on, taken from Input Files directly.

To lint the whole target every time instead:

```bash
swiftlint lint --config "${SRCROOT}/.swiftlint.yml"
```

and untick "Based on dependency analysis" so the phase is never skipped.

With CocoaPods the binary path is `"${PODS_ROOT}/SwiftLint/swiftlint"`.

## Recipes for CI services

### GitHub Actions: inline annotations

```yaml
name: lint
on:
  pull_request:
    paths: ['**/*.swift']
jobs:
  swiftlint:
    runs-on: macos-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: brew install swiftlint
      - run: swiftlint lint --reporter github-actions-logging --strict
```

### GitHub Actions: code scanning

```yaml
      - name: Lint
        run: swiftlint lint --reporter sarif --strict > lint.sarif
        continue-on-error: true
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: lint.sarif
```

`continue-on-error` lets the upload step run even when lint fails.

### GitHub Actions: during baseline adoption

```yaml
      - run: swiftlint lint --reporter github-actions-logging --baseline .swiftlint.baseline --strict
```

### GitLab CI

```yaml
swiftlint:
  image: ghcr.io/realm/swiftlint:latest
  stage: test
  script:
    - swiftlint lint --reporter codeclimate --strict > code-quality.json
  artifacts:
    reports:
      codequality: code-quality.json
```

GitLab can also show results as test failures: switch to `--reporter gitlab`,
which writes JUnit XML, and attach that file as a `junit` report.

### Bitrise and other hosted runners

```bash
brew install swiftlint
swiftlint --strict
```

## Reporters at a glance

| Reporter | Output | Typical consumer |
|---|---|---|
| `xcode` | Xcode-style diagnostics | local runs, run scripts |
| `emoji` | decorated text | fun in a terminal |
| `github-actions-logging` | workflow commands | inline PR annotations |
| `csv` | comma-separated rows | spreadsheets |
| `sarif` | SARIF | code scanning |
| `checkstyle` | XML | Jenkins, SonarQube |
| `json` | JSON array | scripts, dashboards |

## Swift toolchain compatibility

SwiftLint parses mostly through SwiftSyntax, yet a few rules still go through
SourceKit and Clang, so the linter has to agree with the toolchain that
compiles the code. Rule of thumb: lint with the same toolchain you build with.

On macOS SwiftLint looks for a toolchain in this order:

1. `XCODE_DEFAULT_TOOLCHAIN_OVERRIDE`
2. `TOOLCHAIN_DIR` or `TOOLCHAINS`
3. `xcrun -find swift`
4. `/Applications/Xcode.app/...`
5. `/Applications/Xcode-beta.app/...`
6. `~/Applications/Xcode.app/...`
7. `~/Applications/Xcode-beta.app/...`

- CI machines with several Xcodes installed: export `DEVELOPER_DIR` first, for
  example `DEVELOPER_DIR=/Applications/Xcode_16.4.app/Contents/Developer`.
- The build tool plugin lives inside the build system and picks the right
  toolchain by itself.
- The Homebrew formula can trail a new Swift release. Parse errors right after
  an Xcode upgrade usually mean SwiftLint needs updating.
- The selected toolchain must contain `usr/lib/sourcekitd.framework`. A
  mismatch shows up as parse failures or SourceKit errors.

## VS Code

The `vknabel.vscode-swiftlint` extension lints when you save:

```json
{
  "swiftlint.autoLintWorkspace": false,
  "swiftlint.enable": true,
  "swiftlint.path": "/opt/homebrew/bin/swiftlint"
}
```

## Fastlane

```ruby
lane :lint do
  swiftlint(
    raise_if_swiftlint_error: true,
    strict: true,
    config_file: ".swiftlint.yml",
    mode: :lint
  )
end
```

## Docker

The official image suits Linux CI:

```bash
docker run --rm -v "$PWD:/src" -w /src ghcr.io/realm/swiftlint:<pinned-version> --strict
```

## Pre-commit

### With the pre-commit framework

```yaml
repos:
  - repo: https://github.com/realm/SwiftLint
    rev: <pinned-version>
    hooks:
      - id: swiftlint
        name: SwiftLint (fix, then check)
        entry: swiftlint --fix --strict
```

The `entry` override applies automatic fixes and still fails the commit on any
remaining warning or error.

### Plain git hook

`.git/hooks/pre-commit`:

```bash
#!/bin/bash
git diff --cached --diff-filter=d --name-only -- '*.swift' \
  | xargs -I{} swiftlint lint --strict --quiet "{}"
```

Git ignores the hook until it has the execute bit, so run `chmod +x` on it. Older SwiftLint
releases took the file through `--path "{}"`; current releases take paths as
plain arguments.
