Embrace the Power of Custom SwiftLint Rules and Say Goodbye to Regex

Written by micci | Published 2023/05/31
Tech Story Tags: coding | swift | swiftlint | linting | bazel | ios | ios-app-development | hackernoon-top-story | hackernoon-es | hackernoon-hi | hackernoon-zh | hackernoon-vi | hackernoon-fr | hackernoon-pt | hackernoon-ja

TLDRSwiftLint is the best tool to enforce coding standards and best practices. It comes with pre-defined rules that can be customized to suit individual project needs. With SwiftLint, we can use regular expressions to create complex rules. The rules are written in Swift, efficient, and can catch more violations compared to regex.via the TL;DR App

SwiftLint is the best tool to enforce coding standards and best practices. It comes with pre-defined rules that can be customized to suit individual project needs. However, sometimes the built-in rules fall short of project requirements. In such cases, we write regular expressions to fill the gap.

While regex is a powerful tool, creating complex rules can be difficult or even impossible. This is where native SwiftLint rules come in. They're just like the rules that come with SwiftLint - they're written in Swift, efficient, and can catch more violations compared to regex.

https://twitter.com/simjp/status/1558168307756318720?s=20&embedable=true

The final project can be found at https://github.com/jpsim/swiftlint-bazel-example

Setting Up the Project

Install Bazel if not installed.

brew list --formula | grep -q "bazel" || brew install bazel

Start by creating a new directory.

mkdir MySwiftLintRules && cd MySwiftLintRules

Next, set up a new Bazel workspace. This involves creating several files and enabling bzlmod:

touch .bazelrc WORKSPACE MODULE.bazel BUILD && echo "common --enable_bzlmod" > .bazelrc

Add the boilerplate code to the MODULE.bazel

module(
    name = "swiftlint-bazel-example",
    version = "0.0.0",
    compatibility_level = 1
)

bazel_dep(name = "swiftlint", version = "0.51.0", repo_name = "SwiftLint")
extra_rules = use_extension("@SwiftLint//bazel:extensions.bzl", "extra_rules")
extra_rules.setup(srcs = "@swiftlint-bazel-example//swiftlint_extra_rules:extra_rules")

bazel_dep(name = "rules_xcodeproj", version = "1.4.0")

Add the boilerplate code to the BUILD file. This will add Xcode support for our Bazel project.

load("@rules_xcodeproj//xcodeproj:defs.bzl", "xcodeproj")

xcodeproj(
    name = "swiftlint_xcodeproj",
    project_name = "SwiftLint",
    tags = ["manual"],
    top_level_targets = [
        "@SwiftLint//:swiftlint",
        "@SwiftLint//Tests:ExtraRulesTests",
    ],
)

Create a directory to store the source code for your rules.

mkdir swiftlint_extra_rules && touch swiftlint_extra_rules/Rules.swift && touch swiftlint_extra_rules/BUILD

And for the last time, paste the boilerplate code to the swiftlint_extra_rules/BUILD file

filegroup(
    name = "extra_rules",
    srcs = glob(["**/*.swift"]),
    visibility = ["//visibility:public"],
)

At this point, you should be able to build SwiftLint using the bazel build command.

Developing Rules With Xcode

We’ve added rules_xcodeproj dependency to our project to make developing your custom rules easier. So, let’s generate our project:

bazel run swiftlint_xcodeproj && open SwiftLint.xcodeproj -a Xcode

Now, we can build and debug the swiftlint scheme. You can use this app as usual with swiftlint binary installed from the homebrew.

Try running it on your project: Click the scheme name in the toolbar of your project window, click “Edit Scheme…“, and set the "Working Directory" in the "Options" tab to the path where you would like to execute SwiftLint.

For more information on developing native SwiftLint rules, check out SwiftLint’s CONTRIBUTING.md

Adding a Custom Rule

In this example, we are creating the "forbidden var rule," as shown in the video tutorial at https://vimeo.com/819268038.

Copy and paste this code into the Rules.swift file, then run the swiftlint scheme in Xcode.

import SwiftSyntax

// This function should return an array containing all of your custom rules
func extraRules() -> [Rule.Type] {
	[ForbiddenVarRule.self]
}

struct ForbiddenVarRule: ConfigurationProviderRule, SwiftSyntaxRule {
	var configuration = SeverityConfiguration(.error)

	init() {}

	static let description = RuleDescription(
		identifier: "forbidden_var",
		name: "Forbidden Var",
		description: "Variables should not be called 'forbidden'",
		kind: .idiomatic,
		nonTriggeringExamples: [
			Example("let notForbidden = 0")
		],
		triggeringExamples: [
			Example("let ↓forbidden = 0")
		]
	)

	func makeVisitor(file: SwiftLintFile) -> ViolationsSyntaxVisitor {
		Visitor(viewMode: .sourceAccurate)
	}
}

private extension ForbiddenVarRule {
	final class Visitor: ViolationsSyntaxVisitor {
		override func visitPost(_ node: IdentifierPatternSyntax) {
			if node.identifier.text == "forbidden" {
				violations.append(node.identifier.positionAfterSkippingLeadingTrivia)
			}
		}
	}
}

And that's it! You've created a custom SwiftLint rule and integrated it into your Swift project using Bazel. With this setup, you can create and test as many custom rules as you need, helping to ensure that your code stays consistent and conforms to your team's coding standards.

Happy linting!


Written by micci | With a strong work ethic and a lifelong love of technology, I strive to make a positive impact through my work
Published by HackerNoon on 2023/05/31