Android lints are used to analyze your code for correctness, performance, security, typography and usability; and it has proven itself useful from time to time. In an android studio, there are already some predefined lint rules, which checks for spelling mistakes, possible bugs, performance wise customisations etc., but to follow a standard within a team, we need custom lint rules.
e.g.: in some projects, developers are asked to use a utility class to access SharedPreference, but few of them create a new instance of it every time. To check such thing, we create custom lint rules.
Reference: This post is an extension to Matt Compton’s post Building Custom Lint Checks in Android. In this post, I will just mention few steps to create a custom lint rule. Please refer to Matt’s post for a complete overview.
For making custom lint rules, you need a separate project, you can, fork this git repository, and follow steps to create a rule.
- To create a lint rule, create a class in detectors package, name it SharedPreferenceClass
- We are going to make the rule mentioned in the example, so, we need to check the uses of SharedPreferences.Editor everywhere except Utils.java
- Extend Detector class and implement Detector.JavaScanner (because we’re going to scan through all the Java files. You can also implement ClassScanner and XmlScanner, based on your requirement. The class will look something like this:
public class SharedPreferenceDetector extends Detector implements Detector.JavaScanner { }
- Define the string you want to search for
private static final String SP_MATCHER_STRING = "SharedPreferences.Editor";
- For implementing the issue tracker, we need to define detector class and scope.
private static final Class<? extends Detector> DETECTOR_CLASS = SharedPreferenceDetector.class; private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;
Detector class is, you can say the engine, and scope is where we want to search. You can search - JAVA_FILE
- RESOURCE_FILE
- RESOURCE_FOLDER
- GRADLE etc.
We are going to search for JAVA_FILE_SCOPE
- Initialise the implementation:
private static final Implementation IMPLEMENTATION = new Implementation( DETECTOR_CLASS, DETECTOR_SCOPE );
- Now you need to define Issue properties, for example, we need
private static final String ISSUE_ID = "SharedPreferenceUtils"; private static final String ISSUE_DESCRIPTION = "Shared Preference Class Used"; private static final String ISSUE_EXPLANATION = "Using the Shared Preference Class is not secure. Consider using our Utils.java for such purpose"; private static final Category ISSUE_CATEGORY = Category.CORRECTNESS; private static final int ISSUE_PRIORITY = 8; private static final Severity ISSUE_SEVERITY = Severity.WARNING;
- ISSUE_ID should be always unique
ISSUE_DESCRIPTION, the title you want to display
ISSUE_EXPLANATION, message you want to display
ISSUE_CATEGORY, same I mentioned earlier, I am choosing it to be correctness
ISSUE_PRIORITY, it is self-explanatory
ISSUE_SEVERITY, there are FATAL, WARNING, ERROR, INFORMATION, IGNORE
- Now create an ISSUE object:
- public static final Issue ISSUE = Issue.create(
- ISSUE_ID,
- ISSUE_DESCRIPTION,
- ISSUE_EXPLANATION,
- ISSUE_CATEGORY,
- ISSUE_PRIORITY,
- ISSUE_SEVERITY,
- IMPLEMENTATION
- );
- Create a constructor of course,
public SharedPreferenceDetector() {}
- getApplicableNodeTypes() is used to check the object type.
@Override public List<Class<? extends Node>> getApplicableNodeTypes() { return null; }
- There is an appliesTo method in the code you forked. Please read this issue tracker post. “appliesTo is a leftover from the beginning of lint when it was just passing through the project, file by file, and letting each detector take a look at the file.”
So, I am leaving appliesTo method out of the code.
- The fun part: we will create a java visitor by overriding the method from JavaScanner interface, and using context.file.getName() we will match if file is Utils.java, don’t scan it
Now, we need to register it with CustomIssueRegistry. Add your detector in already present Issue array. The complete SharedPreferenceDetector.java will be:
import com.android.annotations.NonNull;
import com.android.ddmlib.Log;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Context;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.android.tools.lint.detector.api.TextFormat;
import java.io.File;
import java.util.EnumSet;
import java.util.List;
import lombok.ast.AstVisitor;
import lombok.ast.Node;
/**
* Created by tanmaybaranwal on 21/02/17.
*/
public class SharedPreferenceDetector extends Detector implements Detector.JavaScanner {
private static final String SP_MATCHER_STRING = "SharedPreferences.Editor";
private static final Class<? extends Detector> DETECTOR_CLASS = SharedPreferenceDetector.class;
private static final EnumSet<Scope> DETECTOR_SCOPE = Scope.JAVA_FILE_SCOPE;
public static String fileName;
private static final Implementation IMPLEMENTATION = new Implementation(
DETECTOR_CLASS,
DETECTOR_SCOPE
);
private static final String ISSUE_ID = "SharedPreferenceUtils";
private static final String ISSUE_DESCRIPTION = "Shared Preference Class Used";
private static final String ISSUE_EXPLANATION = "Using the Shared Preference Class is not secure. Consider using our Utils.java for such purpose";
private static final Category ISSUE_CATEGORY = Category.CORRECTNESS;
private static final int ISSUE_PRIORITY = 8;
private static final Severity ISSUE_SEVERITY = Severity.WARNING;
public static final Issue ISSUE = Issue.create(
ISSUE_ID,
ISSUE_DESCRIPTION,
ISSUE_EXPLANATION,
ISSUE_CATEGORY,
ISSUE_PRIORITY,
ISSUE_SEVERITY,
IMPLEMENTATION
);
/**
* Constructs a new {@link SharedPreferenceDetector} check
*/
public SharedPreferenceDetector() {
}
@Override
public List<Class<? extends Node>> getApplicableNodeTypes() {
return null;
}
@Override
public AstVisitor createJavaVisitor(@NonNull JavaContext context) {
String source = context.getContents();
//Leave the Utils.java file
if(context.file.getName().equals("Utils.java")){
return null;
}
// Check validity of source
if (source == null) {
return null;
}
// Check for uses of to-dos
int index = source.indexOf(SP_MATCHER_STRING);
for (int i = index; i >= 0; i = source.indexOf(SP_MATCHER_STRING, i + 1)) {
Location location = Location.create(context.file, source, i, i + SP_MATCHER_STRING.length());
context.report(ISSUE, location, ISSUE.getBriefDescription(TextFormat.TEXT));
}
return null;
}
}
To test the detector, we need a test file. The repo contains a package named test, you can make a class in that like this one. (Please find comments to understand the modules).
package com.bignerdranch.linette.detectors;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.TextFormat;
import com.bignerdranch.linette.AbstractDetectorTest;
import java.util.Arrays;
import java.util.List;
/**
* Created by tanmaybaranwal on 21/02/17.
*/
public class SharedPreferenceDetectorTest extends AbstractDetectorTest{
@Override
protected Detector getDetector() {
return new SharedPreferenceDetector();
}
@Override
protected List<Issue> getIssues() {
return Arrays.asList(SharedPreferenceDetector.ISSUE);
}
@Override
protected String getTestResourceDirectory() {
return "shared";
}
/**
* Test that an empty java file has no warnings.
*/
public void testEmptyCase() throws Exception {
String file = "EmptyTestCase.java";
assertEquals(
NO_WARNINGS,
lintFiles(file)
);
}
/**
* Test that an Utils.java java file has no warnings.
*/
public void testUtilsCase() throws Exception {
String file = "Utils.java";
assertEquals(
NO_WARNINGS,
lintFiles(file)
);
}
/**
* Test that a java file with a to-do has a warning.
*/
public void testSharedPrefCase() throws Exception {
String file = "SharedPreferenceTestCase.java";
String warningMessage = file
+ ":7: Warning: "
+ SharedPreferenceDetector.ISSUE.getBriefDescription(TextFormat.TEXT)
+ " ["
+ SharedPreferenceDetector.ISSUE.getId()
+ "]\n"
+ " SharedPreferences.Editor editor = mSharedPreferences.edit();\n"
+ " ~~~~~~~~~~~~~~~~~~~~~~~~\n"
+ "0 errors, 1 warnings\n";
assertEquals(
warningMessage,
lintFiles(file)
);
}
}
Run the lint:
- for MacOS users, open terminal in the android studio, do $chmod 755 gradlew
- $ ./gradlew clean build test install
You will be able to check the success report in the logged link. To check it
- Open terminal
- Set the path at SDK/tools
- lint ——show <issue_id>
To include lint in a project separately
- navigate to /Users/<user_name>/.android/lint/<file.jar>
- Copy the jar file, paste it in the lib folder of your project
- Add the file dependency in app’s build.gradle
- In the gradle, write the lint options:
- Run the lint using $ ./gradlew lint
lintOptions{
abortOnError false //if build has to abort
check ‘SharedPreferenceUtils' //to check one custom rule
showAll true //show report
textReport true
textOutput ‘stdout' //shows report in message
}
While building the project, I got “Error: Error converting bytecode to dex: Cause: Dex cannot parse version 52 bytecode.”. To overcome this, compile your lint project with Java 1.7 to do so, add
apply plugin: 'jacoco'
dependencies{
sourceCompatibility = 1.7
targetCompatibility = 1.7
}
in the gradle of lint project.