Writing Custom SwiftLint Rule with SwiftSyntax

Muhammad Zeeshan
Stackademic
Published in
5 min readDec 5, 2023

--

SwiftLint is a tool to enforce Swift style and conventions. It comes with builtin 200 rules for general usage. For example, you want that your team should define Access Modifiers before any declaration or you want to limit the file size at 200 lines etc. SwiftLint is the tool to enforce these kinds of styles and conventions.
Although it contains a set of general purpose and common usage rules. But sometime your project has some specific and customised requirements that aren’t fulfilled by the builtin rules. So there is a way of writing custom rule to cater custom needs.

In this article we will be exploring how can we write Swift based custom rules.

Defining Custom Rules:

Currently SwiftLint support two approaches for writing custom rules:

1. Swift based rules:

Swift based rules allows you to write the rules in swift. By using this approach, you can leverage the power of SwiftSyntax. Key benefits are:

  • Fast
  • Accurate
  • Unit tested

2. Regex based rules:

Regex based rules allows you to write rules by designing custom regex expressions. These expressions can be defined in the configuration file, for example in .swiftlint.yml. More details.

For this article we will be focusing on the Swift based rules only.

Getting started:

As per the official documentation of SwiftLint, for writing custom SwiftLint rules in swift we need to build SwiftLint with Bazel. You need to install Bazel in your system by running brew install bazel.

Don’t worry about Bazel at this time. Just keep in mind that:

  • Its a command line tool for building and testing.
  • Bazel is a build system that is based on certain rules.
  • Rules are already defined for the Apple’s ecosystem by the community. You just have to use them.

High level overview:

In order for Bazel to work, we need to create 4 files at the root of the project directory:

  • .bazelrc (contains single boiler plate i.e. `common — enable_bzlmod`)
  • BUILD (contains the sequential list of statements, that are used to load rules and generate the project for example in our case generation of xcode project utilising xcodeproj rule)
  • MODULE.bazel (from the docs: A Bazel module is a Bazel project that can have multiple versions, each of which publishes metadata about other modules that it depends on.)
  • WORKSPACE (from the docs: Workspace rules are used to pull in external dependencies, typically source code located outside the main repository. It contains the boiler plate code, don’t worry about it. Just copy and paste the required rule defined by the community. That’s it!)

Fortunately there is an active contributor, providing a starter template project for getting started. Lets get started:

  • Download the zip file or clone the template project.
  • Open up the terminal and navigate to the root directory of the cloned or downloaded project.
  • Run the command bazel run :swiftlint_xcodeproj
  • At this point it should successfully generate the SwiftLint.xcodeproj file at the root of the project directory.
  • Open the SwiftLint.xcodeproj or type xed . in the terminal and Build withCMD + B at this point it should build successfully.
  • If you are getting `linker command failed with exit code 1`. Then you need to expand the swiftlint_extra_rules directory and in the ForbiddenVarRule file just import Foundation

Rule:

1 definition per file but extensions are an exception.

Non triggering example will be:

Non triggering example

Triggering example:

Triggering example

While writing custom rules, there are certain steps that needs to be followed:

  • Create a struct or class that conforms to Rule or OptInRule protocol. If you conform your declaration with Rule protocol, then your custom rule will be enabled by default. In case of OptInRule, user have to enable the rule explicitly. Basically OptInRule is inherited from Rule protocol.
  • Annotate your declaration with@SwiftSyntaxRule .
  • In order to conform with Rule protocol you need to provide certain details, for example you need to provide the RuleDescription and SeverityConfiguration .
  • Next in order to traverse the SyntaxTree you need to make a visitor. By default SwiftLint walk through the SyntaxTree from the protocol extension. But you can implement validate function in your declaration for your custom needs.
  • In order to validate one declaration per file, we will define a boolean and initialised it with false value, whenever a declaration of interest is visited we will assign true to our variable.
  • As we are only interested in Classes, Structs, Enums and Protocols. We need to skip child declarations such as function declaration or variable declaration. For this we need to override skippableDeclarations in our ViolationsSyntaxVisitor and we can skip everything here. The declarations themselves will always be visited.
  • When visitor visit the node of our interest, we will change the value of our boolean variable to true. Which will help us in making decision that if we have already visited any declaration.

Enough talk! Lets see the implementation of all above steps.

Create a new file named OneDelarationPerFileRule.swift under the swiftlint_extra_rule directory and paste the following code.

Now lets test our rule. For testing rule, you need to create a new files in the project and paste the code from the triggering and non triggering example. When you run the project by selecting swiftlint scheme. You will see in the console about the violations. Triggering example will generate the output like the following screenshot.

You can also leverage the builtin additional functionality from the SwiftLint. For example if you noticed that the rule is also linting our own written rule. For sure your project will have third party dependencies and you don’t want to lint third party code. You can create .swiftlint.yml at the root of your project and add the skippable files and directories under the excluded attribute. That’s It! Let me know your thoughts under the comment section below. Thanks 🙂.

Final project can be downloaded from my github.

If you want to have some customised requirements and want to write your own SwiftLint custom rules. You can reach me out at email, Upwork or at my site.

Stackademic

Thank you for reading until the end. Before you go:

  • Please consider clapping and following the writer! 👏
  • Follow us on Twitter(X), LinkedIn, and YouTube.
  • Visit Stackademic.com to find out more about how we are democratizing free programming education around the world.

--

--