Custom Lint rules

Improve your code quality
with dedicated conventions

Droidcon Berlin, June 3rd 2015

André Diermann

Software Architect @ it-objects

Our motivation for custom Lint rules

In words

  • enterprise mobility solution
    (> 10.000 users in EU, Asia, USA)
  • several developers working distributed
    in different countries
  • complex business logic due to full offline mode
  • custom application architecture
  • huge code base

Our motivation for custom Lint rules

In numbers

  • 32 build variants
  • >2.000 classes in ~100 packages
    (with ~150.000 lines of code)
  • >200 screens with >1.500 widgets
    (>30.000 lines of XML)
  • >3.500 string references
    (>35.000 translations)
  • ...

Agenda

  • Introduction
  • Lint API basics
  • Hands-on: custom Lint rules
  • Real world scenarios
  • Q & A

Introduction

  • Lint
  • Custom rules
  • Challenges

Lint

  • tool for command-line and IDE
  • scans all kind of development artifacts
  • reports potential bugs, bad coding habits, broken conventions, ...
  • features more than 200 built-in checks (January 2015)

Example

Example

Custom rules

Motivation

  • work in large and distributed teams requires dedicated conventions
  • huge code bases require automated checks
  • need for 'Android specific' validations
    (vs. checkstyle, FindBugs, ...)
  • ...

Challenges

Writing custom Lint rules

  • poor documentation
  • bad testability

Lint API basics

Core principles of the Lint API

  • Issue
  • Detector
  • Scanner
  • IssueRegistry

Issue

An Issue is a type of problem you want to find and show to the user.

Issue

  • registered in an IssueRegistry
  • reported by a Detector
  • final class
  • created by static factory method
  • has certain attributes, such as
    • Severity
    • Scope
    • ...

Example

 public static final Issue ISSUE = Issue.create(
    "HelloWorld",                                 //ID
    "Unexpected application title",               //brief description
    "The application title should"                //explanation
      + "state 'Hello world'",
    Category.CORRECTNESS,                         //category
    5,                                            //priority
    Severity.INFORMATIONAL,                       //severity
    new Implementation(                           //implementation
        HelloWorldDetector.class,                 //detector
        Scope.MANIFEST_SCOPE                      //scope
    )
);

Detector

A Detector is responsible for scanning through code and finding Issue instances and reporting them.

Detector

  • implementation of a Lint rule
  • analyzes development artifacts
  • reports Issues
  • specialized by Scanners

Example

 public class HelloWorldDetector extends Detector
  implements XmlScanner {

  public static final Issue ISSUE = Issue.create(...);

  @Override public Collection<String> getApplicableElements() {...}

  @Override public Collection<String> getApplicableAttributes() {...}

  @Override public void visitElement(@NonNull XmlContext context,
      @NonNull Element element) {...}

  @Override public void visitAttribute(@NonNull XmlContext context,
      @NonNull Attr attribute) {...}
 }

Scanner

A Scanner is a specialized interface for Detectors.

Scanner types

  • JavaScanner
  • ClassScanner
  • BinaryResourceScanner
  • ResourceFolderScanner
  • XmlScanner
  • GradleScanner
  • OtherFileScanner

Example

JavaScanner XmlScanner
applicableSuperClasses() getApplicableElements()
checkClass(...) visitElement(...)
getApplicableMethodNames() getApplicableAttributes()
visitMethod(...) visitAttribute(...)
... ...

IssueRegistry

An IssueRegistry is a registry which provides a list of checks to be performed on an Android project.

IssueRegistry

  • subclass IssueRegistry
  • override getIssues()
  • reference in MANIFEST
jar {
    manifest {
        attributes 'Lint-Registry':
         'your.package.name.CustomIssueRegistry'
    }
}

Example

public class CustomIssueRegistry extends IssueRegistry {
  @Override
  public List<Issue> getIssues() {
    return Arrays.asList(             //Note:
      MyCustomCheck.ISSUE,            //A check actually is a detector.
      MyAdvancedCheck.AN_ISSUE,       //One detector can report
      MyAdvancedCheck.ANOTHER_ISSUE   //multiple types of issues.
    );
  }
}

Hands-on

  • Getting started
  • Detectors
  • Testing
  • Application

Getting started

github.com/a11n/CustomLintRulesWorkshop

$ git clone https://github.com/a11n/CustomLintRulesWorkshop.git
$ cd CustomLintRulesWorkshop

$ git checkout -f section-1

Pro tips

  • have a look at the default set of checks
  • use SdkConstants wherever possible
  • utilize LintUtils when applicable

Detectors

  • Simple detectors
  • Advanced detectors

Simple detectors

  • scan isolated artifacts of one type
    (e.g. just code or just resources)
  • perform scan and evaluation in one phase

Advanced detectors

  • scan related artifacts of different types
  • perform scan and evaluation in two phases

Testing

Testing

  • No official test support
  • official test support since last week's Google I/O
    not working (see #175161)
  • Lint JUnit rule (still in beta)

Testing

LintDetectorTest

public class HardcodedValuesDetectorTest  extends AbstractCheckTest {
    @Override
    protected Detector getDetector() {
        return new HardcodedValuesDetector();
    }
    public void testStrings() throws Exception {
        assertEquals(
            "res/layout/accessibility.xml:3: Warning: [I18N] Hardcoded string \"Button\", should use @string resource [HardcodedText]\n" +
            "    <Button android:text=\"Button\" android:id=\"@+id/button1\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\"></Button>\n" +
            "            ~~~~~~~~~~~~~~~~~~~~~\n" +
            "res/layout/accessibility.xml:6: Warning: [I18N] Hardcoded string \"Button\", should use @string resource [HardcodedText]\n" +
            "    <Button android:text=\"Button\" android:id=\"@+id/button2\" android:layout_width=\"wrap_content\" android:layout_height=\"wrap_content\"></Button>\n" +
            "            ~~~~~~~~~~~~~~~~~~~~~\n" +
            "0 errors, 2 warnings\n",
            lintFiles("res/layout/accessibility.xml"));
    }
}

Testing

Lint JUnit rule

@Rule public Lint lint = new Lint();

@Test
public void test() throws Exception {
  lint.setFiles("AndroidManifest.xml", "res/values/string.xml");
  lint.setIssues(MyCustomRule.ISSUE);

  lint.analyze();

  List<Warning> warnings = lint.getWarnings();

  assertThat(warnings).hasSize(2);
}

Application

  • Basic approach
  • Integrated approach

Basic approach

  • utilizes basic Lint extension feature
  • two steps setup
    1. assemble custom Lint rules into JAR
    2. copy JAR to ~/.android/lint/

Basic approach

#!/bin/sh

# Build .jar
./gradlew assemble

# Install
if [ ! -d "~/.android/lint/" ]; then
  mkdir ~/.android/lint/
fi

cp build/libs/lint.jar ~/.android/lint/

Basic approach

Pros Cons
just assemble and copy no straightforward distribution and configuration
one resulting JAR no project-specific rules
no changes in the project to analyze inconvenient for multi developer teams
applied for all analyzed projects inconvenient for CI environments

Integrated approach

  • uses AAR bundle as wrapper
  • two steps setup
    1. wrap custom Lint rules into an AAR
    2. make application project depend on that AAR

Three options for dependencies

  • Copy the AAR to the libs folder
dependencies {
  compile fileTree(dir: 'libs', include: '*.jar')
}
  • Deploy the AAR to a repository
dependencies {
  compile 'your.package.name:custom-lint:1.0.0@aar'
}

Three options for dependencies

  • Have a Java module for the Lint rules and an Android library module as wrapper
Android application project
--app         //default Android application module
--lint        //Android library, acts as wrapper for the Lint rules
--lintrules   //Java module with your custom Lint rules
project.afterEvaluate {
  def compileLint = project.tasks.getByPath(':lint:compileLint')
  compileLint.dependsOn ':lintrules:jar'
  compileLint << {
    copy{
      from '../lintrules/build/libs'
      into 'build/intermediates/lint'
    }
  }
}

Integrated approach

Pros Cons
allows project specific rules changes in project required
integrated within project not applied for all analyzed projects
ideal for multi developer teams (no official documentation on how to wrap into AAR bundle available)
perfect for CI environments

Real world scenarios

  • Placeholder
  • Naming conventions
    • Activity/Fragment class names
    • Layout names
    • String references
    • ID prefixes

Placeholder

  • used for layout purposes
  • help to get a meaningful overview
  • present applied styles
  • show wrapping

Example

Example

Naming conventions

Activity/Fragment class names

Natural names Conventional names
...
ConsumedMaterialsActivity
CustomerSignatureFragment
OrderConfirmationFragment
OrderDataModel
OrderReportActivity
OrderReportViewModel
VanStockMaterialFragment
...
...
ActivityConsumedMaterials
ActivityOrderReport
FragmentCustomerSignature
FragmentOrderConfirmation
FragmentVanStockMaterial
ModelOrderData
ViewModelOrderReport
...

Layout names

  • should express where they are going to be used
  • Examples:
    • activity_order_status.xml
    • fragment_customer_details.xml
    • list_item_purchase_order.xml

String references

From experience, string references

  • should be tied to a widget in a 1:1 relation!!
    • reduces semantical incorrectness
    • allows outsourcing of translation process
  • may be refined by literals to avoid redundancy
  • could be declared in distinct and dedicated files

Example

<!-- fragment_customer_signature.xml -->
<TextView android:id="@+id/tvCustomerName"
  android:text="@string/fragment_customer_signature_tvCustomerName_text" />
<EditText android:id="@+id/etCustomerName"
  android:hint="@string/fragment_customer_signature_etCustomerName_hint" />
<Button android:id="@+id/btSubmit"
  android:text="@string/fragment_customer_signature_btSubmit_text" />

<!-- string.xml -->
<string name="fragment_customer_signature_tvCustomerName_text">
  Please enter the customer name:
</string>
<string name="fragment_customer_signature_btSubmit_text">
  @string/literal_submit
</string>

ID prefixes

  • infer semantics
    • on widget types
    • on inter-widget relations
  • speed up code completion
<RelativeLayout android:id="@+id/llCustomerDetails" ... >
  <ImageView android:id="@+id/ivCustomerName" ... />
  <TextView android:id="@+id/tvCustomerName" ... />
  <EditText android:id="@+id/etCustomerName" ... />
  ...
  <Button android:id="@+id/btSubmit" ... />
</RelativeLayout>

Summary

  • custom Lint rules are written by extending Detectors and implementing Scanners
  • complex rules have a scan and an evaluation phase
  • Lint API provides convenient access to different types of development artifacts
  • AAR format allows easy deployment and project-related rules

What's next

  • release of reference guide for creating custom Lint rules
  • improving Lint JUnit rule
  • extending API
    • advanced LintUtils
    • advanced BaseDetectors
  • encourage for more exchange
    (e.g. on StackOverflow)

Thank you for your attention.

Q&A

q2ad a11n