javierdemartin


Configuring SwiftLint as a pre-commit hook

One of the most popular ways to configure SwiftLint on a Xcode project is to add it as a Run Script Phase script that will be run on each build. It’s one of those quick-win tooling ideas that can be added to a project and quickly see results pay off.

As a project grows in size, so will the build time if using SwiftLint as a build script. For each build run, the linter script has to check all of the files again. This is not an issue during initial phases of a project but, as it grows in size, build times will also grow in time as all the files need to be checked by the linter script.

An option to avoid the issues above is to run SwiftLint as a pre-commit hook. As soon as work is ready to be committed the commit hook will run and check if the code matches the configured styles and conventions. This approach will only bother the developer when it is time to push the changes rather than being reminded of it on each build while they are focused on the work.

The configuration and implementation

This post describes a brief guide on getting a minimal configuration that runs SwiftLint as a pre-commit hook. All this can be achieved with three files: scripts/pre-commit.sh, fastlane/Fastfile & .swiftlint.yml.

First off, scripts/pre-commit.sh will be run prior to any commit being confirmed and will check for any linting issues on the codebase. As a safety measure, linting will not be run if a merge is taking place.

#!/bin/bash

SWIFT_LINT=$(which swiftlint)

SCRIPT_SUCCESS=0

run_swift_lint() {
  local filename="${1}"
  if [[ "${filename##*.}" == "swift" ]]; then
	${SWIFT_LINT} --quiet --strict --path "${filename}"
	previous_ret_val=$?
	if [ $previous_ret_val -ne 0 ]; then
	  SCRIPT_SUCCESS=$previous_ret_val
	fi

	${SWIFT_LINT} --autocorrect --strict --path "${filename}"
  fi
}

if [[ -e "${SWIFT_LINT}" ]]; then
  # Do not run if there is a merge taking place
  if ! git rev-parse -q --verify MERGE_HEAD; then 
	# Run for to be committed (staged) files
	while IFS= read -r -d '' FILE; do
	  run_swift_lint "${FILE}"
	done < <(git diff --cached --name-only -z)
  fi
else
  echo "⚠️ ${SWIFT_LINT} is missing. Run 'fastlane setup_swiftlint'"
  exit -1
fi

if [ $SCRIPT_SUCCESS -ne 0 ]; then
  echo ""
  echo "💥 Found formatting errors, some might have been autocorrected."
  echo ""
  echo "⚠️ Run '${SWIFT_LINT} --autocorrect --strict' ⚠️"
  echo "Check the changes that have been done and commit them."
fi

exit $SCRIPT_SUCCESS

The missing piece for the pre-commit hook to work is to configure it. Easiest way to do it is to create a symbolic link to it.

ln -s -f scripts/pre-commit.sh .git/hooks/pre-commit

All there’s left now is to configure SwiftLint with the appropriate rules that match your workflow inside the .swiftlint.yml file. The configuration below can serve as a quick start guide.

excluded:
  - Pods
  - scripts
  - fastlane
  - .git

opt_in_rules:
  - sorted_imports
  - convenience_type
  - multiline_parameters
  - vertical_parameter_alignment
  - vertical_parameter_alignment_on_call
  - prohibited_interface_builder
  - unneeded_break_in_switch
  
disabled_rules:
  - line_length
  - function_body_length
  - implicit_getter
  - orphaned_doc_comment
  - force_try
  - identifier_name
  - todo
  - cyclomatic_complexity
  - file_length
  - closure_parameter_position
  - function_parameter_count
  - unused_enumerated
  - redundant_string_enum_value
  - class_delegate_protocol
  - nsobject_prefer_isequal
  - convenience_type

Automating the process with Fastlane

Installation and configuration of the scripts above can be done with Fastlane. Just add the contents of the Fastlane action below. Each member of the project will have to run the setup_swiftlint lane just once on their machine to set up everything.

  desc "Setup development environment and create a pre-commit hook for SwiftLint"
  lane :setup_swiftlint do |options|
	begin
	  sh("brew install swiftlint")
	rescue => exception
	  UI.error("💥 Missing Homebrew installation: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"")
	  raise exception
	end

	pre_commit_link = "../.git/hooks/pre-commit"
	if File.exist?(pre_commit_link) || File.symlink?(pre_commit_link)
	  UI.message("✅ Already linked pre-commit script")
	else
	  UI.message("🔗 Creating a symbolic link for the pre-commit script")
	  Dir.chdir ".." do
		sh("ln -s -f ../../scripts/pre-commit.sh .git/hooks/pre-commit")
	  end
	end
  end

## The results

After installing and configuring everything your git workflow does not have to change at all, do work and commit your changes. If there are any linting issues they will be displayed on the console and all that there’s left to do is to fix them before the commit hook lets you confirm the changes.

➜  SampleProject git:(feature-branch) ✗ git commit -m "Sample change"

SampleFile.swift:27:1: error: Trailing Whitespace Violation: Lines should not have trailing whitespace. (trailing_whitespace)

Correcting 'SampleFile.swift' (1/1)
SampleFile.swift:27:1 Corrected Trailing Whitespace
Done inspecting 1 file for auto-correction!

💥 Found formatting errors, some might have been autocorrected.

⚠️ Run 'swiftlint --autocorrect --strict' ⚠️
Check the changes that have been done and commit them.