How to Create an Xcode Linter for Swift

SwiftLint is a great open source tool that makes it easier for you to follow Swift style and conventions. It also helps with identifying possible errors early by highlighting problematic usage. You can run SwiftLint on your Xcode project to see all the style guide exceptions on the lines where they occur, and fix them quickly. I found it was a great help when I migrated my code from Objective-c to Swift.

In the spirit of making SwiftLint even more useful for Firebase developers, we’ve added some experimental new Firebase rules into SwiftLint. These rules will display warnings on common mistakes that might lead to errors when using the Firebase SDK.

Having zero knowledge about developing linters or rules, I dived into SwiftLint rules. SwiftLint is designed to enforce style conventions. Thus most of its rules are checking the occurrence of certain keywords or characters. And where they exist. But in Firebase rules I had a different target. Right method calls must be in the right files within the scope of right functions. For example FirebaseCore rules. We want to make sure FirebaseApp.configure() is called. And it’s called inside the UIApplicationDelegate’s application:didFinishLaunchingWithOptions: function.

My initial attempt was doing a complicated regex check. To find the specific class function, than the Firebase method call, and see if they were in the same boundary. This turned out be quite inefficient and stalled Xcode.

I went through SwiftLint’s helper function and found that there is a already a function doing this for me.

func match(pattern: String,
range: NSRange? = nil,
excludingSyntaxKinds: [SyntaxKind],
excludingPattern: String,
exclusionMapping: MatchMapping = { $0.range })->[NSRange]

This finds if a pattern occurs, while the other one doesn’t. This way I finally started seeing results and making the rules work.

I took this as a first pull request attempt. But I found out that this would create too many false-positives. Since I was doing a basic regex check instead of a real syntax check. This made me to deep dive to SwiftLint to understand how it actually does the syntax checking.

Underneath the SwiftLint, there is a adorable little framework called SourceKitten. It interacts with SourceKit to parse the Swift AST. Once you have the AST, you can do the proper syntax checking.

{
"key.substructure" : [
{
"key.kind" : "source.lang.swift.decl.struct",
"key.offset" : 0,
"key.nameoffset" : 7,
"key.namelength" : 1,
"key.bodyoffset" : 10,
"key.bodylength" : 13,
"key.length" : 24,
"key.substructure" : [
{
"key.kind" : "source.lang.swift.decl.function.method.instance",
"key.offset" : 11,
"key.nameoffset" : 16,
"key.namelength" : 3,
"key.bodyoffset" : 21,
"key.bodylength" : 0,
"key.length" : 11,
"key.substructure" : [

],
"key.name" : "b()"
}
],
"key.name" : "A"
}
],
"key.offset" : 0,
"key.diagnostic_stage" : "source.diagnostic.stage.swift.parse",
"key.length" : 24
}

This way I went back to Firebase quickstarts, printed out their Swift AST from this tool’s CLI. I used SourceKitten’s SwiftExpressionKind and SwiftDeclarationKind. Then iterated through the AST. This way I finally had solid rules. They were checking the exact function and syntax kinds in exact files. I realized the available helper recursive functions were not enough for my case. So I extended them for my use to achieve this goal.

public func validateRecursive(dictionary: [String:  
SourceKitRepresentable]) -> Bool {
if validateBaseCase(dictionary: dictionary) {
return true
}
for subDict in dictionary.substructure {
if validateRecursive(dictionary: subDict) {
return true
}
}
return false
}

After finishing my unit tests, I was back with another PR. This time the missing part was the examples. Each SwiftLint rule comes with triggering and non-triggering examples. It helps with readability of your code. But the examples are also used in unit tests. By putting more examples, I checked further false-positives as well false-negatives.

public static let description = RuleDescription(
identifier: "firebase_config_activate",
name: "Firebase Config Activate",
description: "Firebase Config should be activated.",

nonTriggeringExamples: [
"remoteConfig.fetch(withExpirationDuration:
TimeInterval(expirationDuration)) {" +
" (status, error) -> Void in \n
self.remoteConfig.activateFetched() \n }",
"foo.fetch() { }",
"foo.fetch(fromURL: URL) { }"
],

triggeringExamples: [
"remoteConfig.fetch(withExpirationDuration:
TimeInterval(expirationDuration)) {" +
" (status, error) -> Void in \n }",
"remoteConfig.fetch(withExpirationDuration:
TimeInterval(expirationDuration)) {" +
" (status, error) -> Void in \n foo.fetch() \n }"
]
)
Currently we are hosting the rules on our fork in a firebase_rules branch. Our pre-release binary holds the Firebase rules. You can simply download the .pkg file and double click to install. You can also build the binary from the source.
Since the rules are opt-in, you’ll need to add a .swiftlint.yml file in the same folder as your Swift source files, containing the following text:
opt_in_rules:
- firebase_config_activate
- firebase_config_defaults
- firebase_config_fetch
- firebase_core
- firebase_dynamiclinks_customschemeURL
- firebase_dynamiclinks_schemeURL
- firebase_dynamiclinks_universallink
- firebase_invites

Then, just run SwiftLint on your project like normal.

We’d love for you to give it a try and send us feedback on Twitter with #FirebaseLinter. You can also ask questions on StackOverflow using the firebase and swiftlint tags together.