diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4de1ced --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,64 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + # This project requires Xcode 26+ (see README Requirements). Pin an exact + # version here if `latest-stable` ever resolves below that floor. + xcode-version: latest-stable + + - name: Cache SwiftPM dependency sources + uses: actions/cache@v4 + with: + # Package.resolved isn't committed (swift-build/swift-tools-protocols float + # on `branch: main`), so it always hashes to the same empty match — key off + # Package.swift instead. Cache only fetched sources, not .build's compiled + # objects, so a stale cache can't silently serve outdated build products + # across dependency or toolchain changes; sources just get an incremental + # `git fetch` instead of a full clone. + path: | + ~/Library/Caches/org.swift.swiftpm + .build/checkouts + .build/repositories + key: ${{ runner.os }}-spm-${{ hashFiles('Package.swift') }} + restore-keys: | + ${{ runner.os }}-spm- + + - name: Build + run: swift build + + - name: Run tests + run: swift test + + lint: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Install SwiftLint + env: + SWIFTLINT_VERSION: "0.63.3" # keep in sync with the version contributors install locally + run: | + curl -sL "https://github.com/realm/SwiftLint/releases/download/${SWIFTLINT_VERSION}/portable_swiftlint.zip" -o swiftlint.zip + unzip -q swiftlint.zip -d "$RUNNER_TEMP/swiftlint-bin" + chmod +x "$RUNNER_TEMP/swiftlint-bin/swiftlint" + echo "$RUNNER_TEMP/swiftlint-bin" >> "$GITHUB_PATH" + + - name: Lint + run: swiftlint lint diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f6098f0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: local + hooks: + - id: swiftlint + name: SwiftLint + description: Lint staged Swift files with SwiftLint. + entry: swiftlint lint + language: system + files: \.swift$ + exclude: ^\.build/ diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..f2aa6cc --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,47 @@ +included: + - Sources + - Tests + +excluded: + - .build + - .swiftpm + +opt_in_rules: + - empty_count + - closure_spacing + - contains_over_first_not_nil + - fatal_error_message + - first_where + - force_unwrapping + - implicitly_unwrapped_optional + - last_where + - redundant_nil_coalescing + - sorted_first_last + - unneeded_parentheses_in_closure_argument + +disabled_rules: + - todo + +line_length: + warning: 120 + error: 200 + +trailing_comma: + mandatory_comma: true + +identifier_name: + excluded: + - id + - s + - t + - fm + - op + - v26_2 + - v16_0 + - v16_2_1 + +force_unwrapping: + severity: warning + +implicitly_unwrapped_optional: + severity: warning diff --git a/README.md b/README.md index b30b524..48a3c58 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,24 @@ swift test # Build release binary swift build -c release + +# Lint +swiftlint lint ``` +### Pre-commit hooks + +This repo uses [pre-commit](https://pre-commit.com) to lint staged Swift files with +[SwiftLint](https://github.com/realm/SwiftLint) before each commit. + +```bash +brew install pre-commit swiftlint +pre-commit install +``` + +After that, `git commit` runs SwiftLint automatically against whatever `.swift` files are +staged. Run it manually against everything with `pre-commit run --all-files`. + ### Project layout ``` diff --git a/Sources/SourceKitXcodeBSP/Bootstrap/BuildServerBootstrap.swift b/Sources/SourceKitXcodeBSP/Bootstrap/BuildServerBootstrap.swift index 14c12f2..977d7ab 100644 --- a/Sources/SourceKitXcodeBSP/Bootstrap/BuildServerBootstrap.swift +++ b/Sources/SourceKitXcodeBSP/Bootstrap/BuildServerBootstrap.swift @@ -323,4 +323,3 @@ public extension BuildServerBootstrap { ) } } - diff --git a/Sources/SourceKitXcodeBSP/Xcode/XcodePaths.swift b/Sources/SourceKitXcodeBSP/Xcode/XcodePaths.swift index d1854fd..737d6a2 100644 --- a/Sources/SourceKitXcodeBSP/Xcode/XcodePaths.swift +++ b/Sources/SourceKitXcodeBSP/Xcode/XcodePaths.swift @@ -174,7 +174,7 @@ public struct XcodeVersion: Sendable, CustomStringConvertible { public init(versionString: String) { let parts = versionString.split(separator: ".").compactMap { Int($0) } - major = parts.count > 0 ? parts[0] : 0 + major = !parts.isEmpty ? parts[0] : 0 minor = parts.count > 1 ? parts[1] : 0 patch = parts.count > 2 ? parts[2] : 0 } diff --git a/Sources/sourcekit-xcode-bsp/SourcekitXcodeBspCommand.swift b/Sources/sourcekit-xcode-bsp/SourcekitXcodeBspCommand.swift index 5b19aa5..1238f52 100644 --- a/Sources/sourcekit-xcode-bsp/SourcekitXcodeBspCommand.swift +++ b/Sources/sourcekit-xcode-bsp/SourcekitXcodeBspCommand.swift @@ -59,4 +59,4 @@ struct Serve: AsyncParsableCommand { serviceProvider: serviceProvider ) } -} \ No newline at end of file +} diff --git a/Sources/test-ipc/main.swift b/Sources/test-ipc/main.swift index 5f62f9d..9323727 100644 --- a/Sources/test-ipc/main.swift +++ b/Sources/test-ipc/main.swift @@ -3,7 +3,7 @@ import SwiftBuild private final class NopDelegate: SWBPlanningOperationDelegate, @unchecked Sendable { func provisioningTaskInputs(targetGUID: String, provisioningSourceData: SWBProvisioningTaskInputsSourceData) async -> SWBProvisioningTaskInputs { .init() } - func executeExternalTool(commandLine: [String], workingDirectory: String?, environment: [String : String]) async throws -> SWBExternalToolResult { + func executeExternalTool(commandLine: [String], workingDirectory: String?, environment: [String: String]) async throws -> SWBExternalToolResult { print(" executeExternalTool: \(commandLine.first ?? "?") (returning .deferred)") return .deferred } diff --git a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift index c97662d..810c96a 100644 --- a/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift +++ b/Tests/SourceKitXcodeBSPTests/Watcher/WorkspaceWatcherTests.swift @@ -70,7 +70,7 @@ struct WorkspaceChangeFilterTests { func rejectsOtherProjects() { let filter = WorkspaceChangeFilter( allowedPaths: [ - "/Projects/App/App.xcodeproj/project.pbxproj", + "/Projects/App/App.xcodeproj/project.pbxproj" ] ) @@ -110,7 +110,7 @@ struct WorkspaceChangeFilterTests { // Even if a checkout path were mistakenly allowlisted, ignore components win. let filter = WorkspaceChangeFilter( allowedPaths: [ - "/Projects/App/SourcePackages/checkouts/Dep/Package.swift", + "/Projects/App/SourcePackages/checkouts/Dep/Package.swift" ] ) #expect( @@ -126,7 +126,7 @@ struct WorkspaceChangeFilterTests { // be an ignore component or reloads never fire. let filter = WorkspaceChangeFilter( allowedPaths: [ - "/Users/me/checkouts/MyApp/MyApp.xcodeproj/project.pbxproj", + "/Users/me/checkouts/MyApp/MyApp.xcodeproj/project.pbxproj" ] ) #expect( diff --git a/Tests/SourceKitXcodeBSPTests/Xcode/XcodePathsTests.swift b/Tests/SourceKitXcodeBSPTests/Xcode/XcodePathsTests.swift index 2c1b400..1441eaf 100644 --- a/Tests/SourceKitXcodeBSPTests/Xcode/XcodePathsTests.swift +++ b/Tests/SourceKitXcodeBSPTests/Xcode/XcodePathsTests.swift @@ -83,7 +83,7 @@ struct XcodePathsTests { .infoPlistInvalid, .versionNotFound, .unsupportedVersion(XcodeVersion(major: 15, minor: 0)), - .swbBuildServiceNotFound + .swbBuildServiceNotFound, ] for error in errors {