SwiftLint on a Swift Package

I'm trying to run SwiftLint on a Swift package. I have installed SwiftLint via mint. From the root of my project directory I can run mint run swiftlint, and this lints my project correctly.

As a next step, I would like this automated with the errors/warnings showing up in Xcode. I've tried

  1. Editing Package.swift, and adding SwiftLint as a package dependency and a plugin on target. While this shows me the errors/warnings in Xcode, it does not process my .swiftlint.yml file.

  2. Editing my scheme, and then adding a run script to my pre (have also tried post) build actions. While this runs the linting (and honors my .swiftlint.yml), it does not show the issues in Xcode. My script is

# Type a script or drag a script file from your workspace to insert its path.
export PATH="$PATH:/opt/homebrew/bin"
if mint which realm/swiftlint > /dev/null; then
  cd "$WORKSPACE_PATH/../../.."
  mint run realm/swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

What would be a good way to lint a swift package?

Hi, do you have your .swiftlint.yml file located in the root directory? It's mentioned in the SwiftLing README:

Due to limitations with Swift Package Manager Plug-ins this is only recommended for projects that have a SwiftLint configuration in their root directory as there is currently no way to pass any additional options to the SwiftLint executable.

There is no way how to display SwiftLint warnings or errors when you run a custom script in pre/post build actions. This is one of the reasons why build tools plugins were created.

And I have another issue with the SwiftLint plugin - Xcode is hanging when applying SwiftLint to many targets.

Hi @georgemp!

For my project, I chose a different way to implement SwiftLint in my packages. I added the SwiftLint plugin dependencies in the dependency configuration as you can see in the example below.

...
 dependencies: [
        .package(name: "SwiftLint", path: "../SwiftLint")
    ],
    targets: [
        .target(
            name: "Your Package Name",
            dependencies: [], plugins: [.plugin(name: "SwiftLintPlugin", package: "SwiftLint")]),
...

And I added the SwiftLint binary file directly into my project.

import Foundation
import PackagePlugin

@main
struct SwiftLintPlugin: BuildToolPlugin {
    func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
        guard let sourceTarget = target as? SourceModuleTarget else {
            return []
        }
        return createBuildCommands(
            inputFiles: sourceTarget.sourceFiles(withSuffix: "swift").map(\.path),
            packageDirectory: context.package.directory,
            workingDirectory: context.pluginWorkDirectory,
            tool: try context.tool(named: "swiftlint")
        )
    }

    private func createBuildCommands(
        inputFiles: [Path],
        packageDirectory: Path,
        workingDirectory: Path,
        tool: PluginContext.Tool
    ) -> [Command] {
        if inputFiles.isEmpty {
            return []
        }

        var arguments = [
            "lint",
            "--quiet",
            "--force-exclude",
            "--cache-path", "\(workingDirectory)"
        ]

        if let configuration = packageDirectory.firstConfigurationFileInParentDirectories() {
            arguments.append(contentsOf: ["--config", "\(configuration.string)"])
        }
        arguments += inputFiles.map(\.string)

        let outputFilesDirectory = workingDirectory.appending("Output")

        return [
            .prebuildCommand(
                displayName: "SwiftLint",
                executable: tool.path,
                arguments: arguments,
                outputFilesDirectory: outputFilesDirectory
            )
        ]
    }
}

#if canImport(XcodeProjectPlugin)
import XcodeProjectPlugin

extension SwiftLintPlugin: XcodeBuildToolPlugin {
    func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
        let inputFilePaths = target.inputFiles
            .filter { $0.type == .source && $0.path.extension == "swift" }
            .map(\.path)
        return createBuildCommands(
            inputFiles: inputFilePaths,
            packageDirectory: context.xcodeProject.directory,
            workingDirectory: context.pluginWorkDirectory,
            tool: try context.tool(named: "swiftlint")
        )
    }
}
#endif

I have chose to do it this way to prevent lint from running many files and focus only on the added file. It's a test, and for now it is working :slight_smile:

1 Like

Why did you choose not to use the build plugin provided by the core SwiftLint package, @victoriafaria?

It was a CI decision, it greatly improved our build runtimes :slightly_smiling_face:

1 Like