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.
- 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
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;
}
}
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)
);
}
}
- for MacOS users, open terminal in the android studio, do $chmod 755 gradlew
- $ ./gradlew clean build test install
- Open terminal
- Set the path at SDK/tools
- lint ——show <issue_id>
- 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
}
apply plugin: 'jacoco'
dependencies{
sourceCompatibility = 1.7
targetCompatibility = 1.7
}


























