Wednesday 22 February 2017

Filled Under: ,

Writing Custom Lint for Android Studio

Post By - Tanmay | 2/22/2017 03:05:00 am





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.

  1. To create a lint rule, create a class in detectors package, name it SharedPreferenceClass
  2. 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

  3. 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 {  
     }  
    

  4. Define the string you want to search for

     private static final String SP_MATCHER_STRING = "SharedPreferences.Editor";  
    


  5. 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
    1. JAVA_FILE
    2. RESOURCE_FILE
    3. RESOURCE_FOLDER
    4. GRADLE etc.

      We are going to search for JAVA_FILE_SCOPE

  6. Initialise the implementation:

     private static final Implementation IMPLEMENTATION = new Implementation( DETECTOR_CLASS, DETECTOR_SCOPE );  
    

  7. 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;  
    

    1. 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
    2. Now create an ISSUE object:
      1. public static final Issue ISSUE = Issue.create(
      2.             ISSUE_ID,
      3.             ISSUE_DESCRIPTION,
      4.             ISSUE_EXPLANATION,
      5.             ISSUE_CATEGORY,
      6.             ISSUE_PRIORITY,
      7.             ISSUE_SEVERITY,
      8.             IMPLEMENTATION
      9.     );
    3. Create a constructor of course,

       public SharedPreferenceDetector() {}  
      


    4. getApplicableNodeTypes() is used to check the object type.

       @Override  
         public List<Class<? extends Node>> getApplicableNodeTypes() {  
           return null;  
         }  
      
    5. 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.


    6. 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:

  1. for MacOS users, open terminal in the android studio, do $chmod 755 gradlew
  2. $ ./gradlew clean build test install

You will be able to check the success report in the logged link. To check it

  1. Open terminal
  2. Set the path at SDK/tools
  3. lint ——show <issue_id>

To include lint in a project separately

  1. navigate to /Users/<user_name>/.android/lint/<file.jar>
  2. Copy the jar file, paste it in the lib folder of your project
  3. Add the file dependency in app’s build.gradle
  4. In the gradle, write the lint options:
     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  
       }  
    

  1. Run the lint using $ ./gradlew lint


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.


0 comments:

Post a Comment