Using custom lint rules to improve our DLS coverage


By Kiran Puppala

At Gojek, we have our very own DLS (design language system) called Asphalt. Asphalt is a collection of guidelines and components used to create amazing user experiences. We’ve created this system to optimise our designs and bring consistency across all our products.

While creating the system is one part of the story, it’s important for us to track the adoption of our DLS and help engineers and designers figure out what they can do to increase adoption.

Here’s where Project Sonar comes in.

Tracking adoption

Asphalt DLS covers several aspects:

  • Custom components
  • Colour and gradient tokens to be used across the app
  • Spacing tokens to provide consistent margins and paddings
  • Typography
  • Themes

We wrote several rules to check if a product is actually using all components and utilities Asphalt DLS provides.

A rule is a piece of code which parses the source code of product modules and finds places where Asphalt DLS could fit in.

For example, if a TextView is used instead of AsphaltTextView, it will be considered as a Component Issue and we write a rule to find this.

Custom lint rules

Android provides lint and it comes preconfigured with hundreds of checks. It can be extended with additional rules and these rules can be shipped as a library.

For example, if you’re the author of a library project and your library project has certain usage requirements, you can write additional lint rules to check if your library is used correctly and then distribute those extra lint rules for users of the library. Similarly, you may have company-specific rules you’d like to enforce.

We took advantage of this tool and wrote several rules to find out all the DLS issues.

To add custom lint rules on iOS, we created a command-line tool which uses SourceKitten framework to communicate to the SourceKit APIs to parse the Swift AST. The SourceKitten framework gives a structure of the code along with certain metadata used to generate warnings. This CLI is then added as a part of the build phase to show warnings on Xcode.

Creating a custom lint rule

Android

Let’s implement a custom lint rule to detect hardcoded spacings (i.e., margins and padding).

A custom lint rule has two important parts:

  • Issue
  • Detector

Issue
The issue contains the metadata of the problem. From Issue.create() method documentation, an Issue accepts the below parameters.

* @param id the fixed id of the issue
* @param briefDescription short summary (typically 5-6 words or less), typically
* describing the **problem** rather than the **fix**
* (e.g. "Missing minSdkVersion")
* @param explanation a full explanation of the issue, with suggestions for
* @param implementation the default implementation for this issue
* @param moreInfo additional information URL
* how to fix it
* @param category the associated category, if any
* @param priority the priority, a number from 1 to 10 with 10 being most
* important/severe
* @param severity the default severity of the issue
* @param androidSpecific true if this issue only applies to Android, false if it does
*     not apply to Android at all, and null if not specified or should run on all
*     platforms. Convenience for specifying platforms=[ANDROID].
*     @param platforms Set of platform scopes where this issue applies.

Some of these parameters are optional.

Our Spacing Token Issue has id, brief description, explanation, category, severity, and implementation details.

Detector
A detector is responsible for detecting occurrences of an issue in the source code.

Spacing Token Detector skims through every element of a layout XML file and finds if any of its spacing attributes use hardcoded spacings (i.e., margins or paddings) and suggests using spacing tokens provided as part of Asphalt Library.

The logic is simple. We extended the Layout Detector which has a useful lifecycle method visitElement. XmlScanner invokes this method when it visits a particular element. We can use this method to get the attributes of each element.

If the attribute matches with any of the following spacing attributes, we check if its value starts with @dimen/ ?attr/ or ?. Otherwise, it’s considered a violation.

More information for creating a custom lint rule can be found here.

Highlighting Issues in IDE

Issue Highlighting and Quick Fix in Android Studio

Through Custom lint rules, we were also able to highlight issues in IDE so that the developer can readily see them in Android Studio.

We provided a quick fix for almost every issue, so developers can just replace each issue with a suitable fix as easy as pressing alt+enter. 🤷‍♂️

Comments in PR using Danger

We also integrated Danger script to highlight these issues as comments in PR.

Danger comments in PR when Android Native Toast is used instead of our Asphalt Toast

iOS

As mentioned earlier, in iOS we use SourceKitten framework to interact with the SourceKit APIs to parse the Swift AST. We also use Apple’s Swift Argument Parser to parse the command line arguments and options.

Before talking about the implementation process, let’s take a look at what SourceKitten is and what data can we get from it.

SourceKitten is a framework and a command-line tool for interacting with SourceKit. SourceKitten links and communicates with sourcekitd.framework to parse the Swift AST, extract comment docs for Swift or Objective-C projects, and get syntax data for a Swift file. SourceKitten is used in many tools including SwiftLint and Jazzy.

Let’s take a look at this with an example.

When SourceKitten parses this file, it generates the below JSON output:

This helps us get a lot of key information such as name, kind, accessibility, etc.

The part we’re most interested in: The substructure.

A Substructure is a list of elements that represents a certain part of the code. This element can be a class, struct, function/methods, method calls, etc. Every Substructure also contains a substructure inside of it which helps us understand the structure of the code.

From the above example, the first substructure defines the ViewController, which contains an array on all the substructures within it, i.e. testView variable and the viewDidLoad method. The above data structure is a tree and we traverse it breadth-first to visit all the substructures in a swift file.

Rule

The most basic element of the tool is a Rule that specifies what aspect of the DLS is being checked (component, theme, tokens etc.). We have a Rule protocol which encapsulates all the data related to a particular rule. Every custom rule conforms to this protocol.

Core view usage rule

We provide Core views (ThemeableUIView, ThemeableUIControl etc.) as a replacement for traditional UIKit elements. Here’s an example rule which checks the places where Core views can be used.

When a substructure is passed to this rule, the rule checks for inheritance usage, declaration usage, and initialisation usage.

Once a lint command is executed, we check for all the rules that have been enabled, traverse through the substructure tree, and check for all violations.

To run this as part of build phase on Xcode, we make use of a ruby script to check those files that have been updated using git command and the run lint only on those files.

Website to render lint report

It’s always better to visually represent issues to make it easier for developers to notice and fix them.

Visual representation generally trumps textual ones.

We wrote CI pipelines to download lint reports generated by Android and iOS lint libraries and rendered these adoption metrics on a website providing percentages to each module. Percentage denotes how much a product module adopted Asphalt DLS.

Report Website

We linked each file name and line numbers to the corresponding source code URL.

Project Sonar enables us to navigate to the source code right from the website and find the documentation link which explains the issue — with just a click. 💚

Thanks to Adwin for pitching in for content about iOS. If you have thoughts and suggestions about this article, we’d love to hear it! Hit us up on Twitter: Kiran Puppala | Adwin Ross

Click here for more stories on how we build our Gojek #SuperApp. 💚