Writing Custom SwiftLint Rule with SwiftSyntax
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 typexed .
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 theForbiddenVarRule
file justimport Foundation
Rule:
1 definition per file but extensions are an exception.
Non triggering example will be:
Triggering example:
While writing custom rules, there are certain steps that needs to be followed:
- Create a
struct
orclass
that conforms toRule
orOptInRule
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 theRuleDescription
andSeverityConfiguration
. - Next in order to traverse the
SyntaxTree
you need to make a visitor. By defaultSwiftLint
walk through the SyntaxTree from the protocol extension. But you can implementvalidate
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 ourViolationsSyntaxVisitor
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 theswiftlint_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.