From baae092c35ed24a19fc21a3ba7d8b0e17287c37b Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Fri, 26 Feb 2021 09:15:21 -0800 Subject: [PATCH 01/55] Cleanup: Flatten directory structure, update to new versions This sample project had multiple subfolders, for Android Studio 2, Android Studio 3, and Android Studio 4. Most of this was obsolete. This is now flattened into just one sample, which shows how to write a lint check for a recent version of Studio/AGP. It also updates the lint checks in various ways: using JUnit4 for the unit test instead of JUnit3, providing a Vendor registration for the issue registry, updating various versions of plugins and libraries to the latest versions, etc etc. --- .gitignore | 2 + README.md | 145 ++++++++++++--- android-studio-2/README.md | 33 ---- android-studio-2/build.gradle | 44 ----- .../gradle/wrapper/gradle-wrapper.properties | 6 - android-studio-2/gradlew | 164 ----------------- android-studio-2/gradlew.bat | 90 --------- .../google/lint/MainActivityDetector.java | 174 ------------------ .../google/lint/ManifestConstants.java | 26 --- .../example/google/lint/MyIssueRegistry.java | 34 ---- .../google/lint/MainActivityDetectorTest.java | 130 ------------- android-studio-3/README.md | 48 ----- android-studio-3/build.gradle | 24 --- android-studio-3/checks/build.gradle | 22 --- .../lint/checks/SampleCodeDetector.java | 93 ---------- .../lint/checks/SampleIssueRegistry.java | 39 ---- .../lint/checks/SampleCodeDetectorTest.java | 51 ----- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes android-studio-3/gradlew.bat | 90 --------- android-studio-3/library/build.gradle | 18 -- android-studio-3/settings.gradle | 1 - android-studio-4/README.md | 47 ----- android-studio-4/build.gradle | 27 --- android-studio-4/gradle.properties | 17 -- .../gradle/wrapper/gradle-wrapper.jar | Bin 53636 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 - android-studio-4/gradlew | 160 ---------------- .../library/src/main/AndroidManifest.xml | 3 - android-studio-4/settings.gradle | 1 - app/build.gradle | 36 ++++ app/src/main/AndroidManifest.xml | 8 + app/src/main/java/com/android/example/Test.kt | 10 + build.gradle | 33 ++++ .../checks => checks}/.gitignore | 0 .../checks => checks}/build.gradle | 15 +- .../example/lint/checks/SampleCodeDetector.kt | 58 +++--- .../lint/checks/SampleIssueRegistry.kt | 16 +- ...ndroid.tools.lint.client.api.IssueRegistry | 0 .../lint/checks/SampleCodeDetectorTest.kt | 33 ++-- .../gradle.properties => gradle.properties | 0 .../wrapper/gradle-wrapper.jar | Bin .../wrapper/gradle-wrapper.properties | 2 +- android-studio-3/gradlew => gradlew | 0 android-studio-4/gradlew.bat => gradlew.bat | 0 .../library => library}/.gitignore | 0 .../library => library}/build.gradle | 4 +- .../src/main/AndroidManifest.xml | 0 settings.gradle | 1 + 48 files changed, 288 insertions(+), 1423 deletions(-) delete mode 100644 android-studio-2/README.md delete mode 100644 android-studio-2/build.gradle delete mode 100644 android-studio-2/gradle/wrapper/gradle-wrapper.properties delete mode 100755 android-studio-2/gradlew delete mode 100644 android-studio-2/gradlew.bat delete mode 100644 android-studio-2/src/main/java/com/example/google/lint/MainActivityDetector.java delete mode 100644 android-studio-2/src/main/java/com/example/google/lint/ManifestConstants.java delete mode 100644 android-studio-2/src/main/java/com/example/google/lint/MyIssueRegistry.java delete mode 100644 android-studio-2/src/test/java/com/example/google/lint/MainActivityDetectorTest.java delete mode 100644 android-studio-3/README.md delete mode 100644 android-studio-3/build.gradle delete mode 100644 android-studio-3/checks/build.gradle delete mode 100644 android-studio-3/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.java delete mode 100644 android-studio-3/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.java delete mode 100644 android-studio-3/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.java delete mode 100644 android-studio-3/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android-studio-3/gradlew.bat delete mode 100644 android-studio-3/library/build.gradle delete mode 100644 android-studio-3/settings.gradle delete mode 100644 android-studio-4/README.md delete mode 100644 android-studio-4/build.gradle delete mode 100644 android-studio-4/gradle.properties delete mode 100644 android-studio-4/gradle/wrapper/gradle-wrapper.jar delete mode 100644 android-studio-4/gradle/wrapper/gradle-wrapper.properties delete mode 100755 android-studio-4/gradlew delete mode 100644 android-studio-4/library/src/main/AndroidManifest.xml delete mode 100644 android-studio-4/settings.gradle create mode 100644 app/build.gradle create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/com/android/example/Test.kt create mode 100644 build.gradle rename {android-studio-4/checks => checks}/.gitignore (100%) rename {android-studio-4/checks => checks}/build.gradle (51%) rename {android-studio-4/checks => checks}/src/main/java/com/example/lint/checks/SampleCodeDetector.kt (61%) rename {android-studio-4/checks => checks}/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt (61%) rename {android-studio-4/checks => checks}/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry (100%) rename {android-studio-4/checks => checks}/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt (72%) rename android-studio-3/gradle.properties => gradle.properties (100%) rename {android-studio-2/gradle => gradle}/wrapper/gradle-wrapper.jar (100%) rename {android-studio-3/gradle => gradle}/wrapper/gradle-wrapper.properties (93%) rename android-studio-3/gradlew => gradlew (100%) rename android-studio-4/gradlew.bat => gradlew.bat (100%) rename {android-studio-4/library => library}/.gitignore (100%) rename {android-studio-4/library => library}/build.gradle (86%) rename {android-studio-3/library => library}/src/main/AndroidManifest.xml (100%) create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore index 12f7e461..44b42418 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,5 @@ Thumbs.db #.idea/workspace.xml - remove # and delete .idea if it better suit your needs. .gradle build/ + +lint-results.sarif diff --git a/README.md b/README.md index c87ccc7f..6e988fb2 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,15 @@ Custom Lint Rules ================= The [Android `lint` tool](http://developer.android.com/tools/help/lint.html) is a static code - analysis tool that checks your Android project source files for potential bugs and optimization - improvements for correctness, security, performance, usability, accessibility, and - internationalization. Lint comes with over 200 checks, however it can be extended with additional - custom rules. +analysis tool that checks your project source files for potential bugs and optimization +improvements for correctness, security, performance, usability, accessibility, and +internationalization. Lint comes with around 400 built-in checks, but it can be extended with +additional custom checks. This sample project shows how those sample checks can be built +and packaged. + +Note that while Android Lint has the name "Android" in it, it is no longer an Android-specific +static analysis tool; it's a general static analysis tool, and inside Google for example it is +run to analyze server-side Java and Kotlin code. **NOTE: The lint API is not a final API; if you rely on this be prepared to adjust your code for the next tools release.** @@ -13,14 +18,66 @@ The [Android `lint` tool](http://developer.android.com/tools/help/lint.html) is Introduction ------------ -The Android Lint API allows users to create custom lint rules. For example, if you are the author of - a library project, and your library project has certain usage requirements, you can write - additional lint rules to check that your library is used correctly, and then you can distribute - those extra lint rules for users of the library. Similarly, you may have company-local rules you'd - like to enforce. +The Android Lint API allows users to create custom lint checks. For example, if you are the author of +an Android library project, and your library project has certain usage requirements, you can write +additional lint rules to check that your library is used correctly, and then you can distribute +those extra lint rules for users of the library. Similarly, you may have company-local rules you'd +like to enforce. This sample demonstrates how to create a custom lint checks and corresponding tests for those rules. + +# Sample Lint Checks + +This project shows how Android Studio as well as the Android Gradle plugin handles packaging of lint +rules. + +## Lint Check Jar Library + +First, there's the lint check implementation itself. That's done in the +"checks" project, which just applies the Gradle "java" or "kotlin" plugins, and +that project produces a jar. Note that the dependencies for the lint +check project (other than its testing dependencies) must all be "compileOnly": + + dependencies { + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.lint:lint-checks:$lintVersion" + ... + +## Lint Check AAR Library + +Next, there's a separate Android library project, called "library". This +library doesn't have any code on its own (though it could). However, +in its build.gradle, it specifies this: + + dependencies { + lintPublish project(':checks') + } + +This tells the Gradle plugin to take the output from the "checks" project +and package that as a "lint.jar" payload inside this library's AAR file. +When that's done, any other projects that depends on this library will +automatically be using the lint checks. + +## App Modules + +Note that you don't have to go through the extra "library indirection" +if you have a lint check that you only want to apply to one or more +app modules. You can simply include the `lintChecks` dependency as shown +above there as well, and then lint will include these rules when analyzing +the project. + +## Lint Version + +The lint version of the libraries (specified in this project as the +`lintVersion` variable in build.gradle) should be the same version +that is used by the Gradle plugin. + +If the Gradle plugin version is *X*.*Y*.*Z*, then the Lint library +version is *X+23*.*Y*.*Z*. + +For example, for AGP 7.0.0-alpha08, the lint API versions are 30.0.0-alpha08. + Getting Started --------------- @@ -31,36 +88,68 @@ git clone https://github.com/googlesamples/android-custom-lint-rules.git cd android-custom-lint-rules ``` -##### Build the code +##### Run The Sample + +Run the :app:lint target to have first the custom lint checks in checks/ +compiled, then wrapped into the library, and finally run lint on a +sample app module which has violations of the check enforced by sample +check in this project: +``` +$ ./gradlew :app:lint + +> Task :app:lintDebug + +Scanning app: ... +Wrote HTML report to file:///demo/android-custom-lint-rules/app/build/reports/lint-results-debug.html +Wrote SARIF report to file:///demo/android-custom-lint-rules/app/build/reports/lint-results-debug.sarif -For Android Studio 3.x and above, use the sample in `android-studio-3`. -If you are targeting Android Studio 2.x and older, use the sample in `android-studio-2`. +/demo/android-custom-lint-rules/app/src/main/java/com/android/example/Test.kt:8: Warning: This code mentions lint: Congratulations [ShortUniqueId] + val s = "lint" + ~~~~ + + Explanation for issues of type "ShortUniqueId": + This check highlights string literals in code which mentions the word lint. + Blah blah blah. + + Another paragraph here. + + Vendor: Android Open Source Project + Contact: https://github.com/googlesamples/android-custom-lint-rules + Feedback: https://github.com/googlesamples/android-custom-lint-rules/issues + +0 errors, 1 warnings + +BUILD SUCCESSFUL in 1s +``` ##### Lint Dependencies -When building your own rules, you will likely want to know which dependencies you should bring into your own project. -The below descriptions of the dependencies included within this project serve to help you make that decision: +When building your own rules, you will likely want to know which dependencies you should +bring into your own project. The below descriptions of the dependencies included within +this project serve to help you make that decision: Source Dependencies -- **com.android.tools.lint:lint-api**: The most important one; it contains things like `LintClient`, the `Detector` -base class, the `Issue` class, and everything else that Lint checks rely on in the Lint framework. -- **com.android.tools.lint:lint-checks**: Contains the built-in checks that are developed internally. Also contains -utilities that are sometimes useful for other lint checks, such as the `VersionChecks` class (which figures out whether -a given UAST element is known to only be called at a given API level, either by surrounding `if >= SDK-version` checks or -`if < SDK-version` early returns in the method). +- **com.android.tools.lint:lint-api**: The most important one; it contains things + like `LintClient`, the `Detector` base class, the `Issue` class, and everything else + that Lint checks rely on in the Lint framework. +- **com.android.tools.lint:lint-checks**: Contains the built-in checks that are developed + internally. Also contains utilities that are sometimes useful for other lint checks, + such as the `VersionChecks` class (which figures out whether a given UAST element is + known to only be called at a given API level, either by surrounding `if >= SDK-version` + checks or `if < SDK-version` early returns in the method). Test Dependencies -- **com.android.tools.lint:lint-tests**: Contains useful utilities for writing unit tests for Lint checks, -including the `LintDetectorTest` base class. -- **com.android.tools:testutils**: It's unlikely that you need to depend on this directly. The test infrastructure -depends on it indirectly though (the methods we use there were mostly for the older lint test infrastructure, -not the newer one). -- **com.android.tools.lint:lint**: Lint checks don't need to depend on this. It's a separate artifact used by tools -that want to integrate lint with the command line, such as the Gradle integration of lint. This is where things like -terminal output, HTML reporting, command line parsing etc is handled. +- **com.android.tools.lint:lint-tests**: Contains useful utilities for writing unit tests + for Lint checks, including the `LintDetectorTest` base class. +- **com.android.tools.lint:lint**: Lint checks don't need to depend on this. It's a + separate artifact used by tools that want to integrate lint with the command line, + such as the Gradle integration of lint. This is where things like terminal output, HTML + reporting, command line parsing etc is handled. +The APIs in all but the lint-api artifact are more likely to change incompatibly than +the lint-api artifact. Support ------- diff --git a/android-studio-2/README.md b/android-studio-2/README.md deleted file mode 100644 index ef3f7823..00000000 --- a/android-studio-2/README.md +++ /dev/null @@ -1,33 +0,0 @@ -See README.md in the parent directory for general information. - -Getting Started ---------------- - -##### Fetch code - -``` -git clone https://github.com/googlesamples/android-custom-lint-rules.git -cd android-custom-lint-rules -``` - -##### Build the validator - -`./gradlew build` - -##### Copy to the lint directory - -`cp ./build/libs/android-custom-lint-rules.jar ~/.android/lint/` - -##### Verify whether the issues are registered with lint - -`lint --show MainActivityDetector` - -##### Run lint - -`./gradlew lint` - -> Note: If you can't run `lint` directly, you may want to include android tools `PATH` in your - `~/.bash_profile`. -> (i.e. `PATH=$PATH:~/Library/Android/sdk/tools`) -> -> Then run `source ~/.bash_profile`. diff --git a/android-studio-2/build.gradle b/android-studio-2/build.gradle deleted file mode 100644 index 7ed6cbd3..00000000 --- a/android-studio-2/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -apply plugin: 'java' - -repositories { - google() - jcenter() -} - -ext { - lintVersion = '26.0.0-beta2' -} - -dependencies { - // For a description of the below dependencies, see the main project README - compile "com.android.tools.lint:lint-api:$lintVersion" - compile "com.android.tools.lint:lint-checks:$lintVersion" - testCompile "junit:junit:4.11" - testCompile "com.android.tools.lint:lint:$lintVersion" - testCompile "com.android.tools.lint:lint-tests:$lintVersion" - testCompile "com.android.tools:testutils:$lintVersion" -} - -jar { - manifest { - attributes("Lint-Registry": "com.example.google.lint.MyIssueRegistry") - } -} - -defaultTasks 'assemble' diff --git a/android-studio-2/gradle/wrapper/gradle-wrapper.properties b/android-studio-2/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9375478b..00000000 --- a/android-studio-2/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Wed Aug 09 14:33:29 PDT 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-rc-1-all.zip diff --git a/android-studio-2/gradlew b/android-studio-2/gradlew deleted file mode 100755 index 91a7e269..00000000 --- a/android-studio-2/gradlew +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android-studio-2/gradlew.bat b/android-studio-2/gradlew.bat deleted file mode 100644 index aec99730..00000000 --- a/android-studio-2/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android-studio-2/src/main/java/com/example/google/lint/MainActivityDetector.java b/android-studio-2/src/main/java/com/example/google/lint/MainActivityDetector.java deleted file mode 100644 index d74eb47a..00000000 --- a/android-studio-2/src/main/java/com/example/google/lint/MainActivityDetector.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.google.lint; - -import com.android.annotations.NonNull; -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.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.Collection; -import java.util.Collections; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_INTENT_FILTER; -import static com.android.xml.AndroidManifest.NODE_ACTION; -import static com.android.xml.AndroidManifest.NODE_CATEGORY; -import static com.example.google.lint.ManifestConstants.ACTION_NAME_MAIN; -import static com.example.google.lint.ManifestConstants.CATEGORY_NAME_LAUNCHER; - -/** - * Checks for an activity with a launcher intent in AndroidManifest.xml. - *

- * NOTE: This is not a final API; if you rely on this be prepared - * to adjust your code for the next tools release. - */ -public class MainActivityDetector extends ResourceXmlDetector implements Detector.XmlScanner { - public static final Issue ISSUE = Issue.create( - "MainActivityDetector", - "Missing Launcher Activities", - "This app should have an activity with a launcher intent.", - Category.CORRECTNESS, - 8, - Severity.ERROR, - new Implementation( - MainActivityDetector.class, - Scope.MANIFEST_SCOPE)); - - /** - * This will be true if the current file we're checking has at least one activity. - */ - private boolean mHasActivity; - /** - * This will be true if the file has an activity with a launcher intent. - */ - private boolean mHasLauncherActivity; - /** - * The manifest file location for the main project, null if there is no manifest. - */ - private Location mManifestLocation; - - /** - * No-args constructor used by the lint framework to instantiate the detector. - */ - public MainActivityDetector() { - } - - @Override - public Collection getApplicableElements() { - return Collections.singleton(TAG_ACTIVITY); - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - mHasActivity = false; - mHasLauncherActivity = false; - mManifestLocation = null; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - // Don't report issues on libraries that may not have a launcher activity - if (context.getProject() == context.getMainProject() - && !context.getMainProject().isLibrary() - && mManifestLocation != null) { - if (!mHasActivity) { - context.report(ISSUE, mManifestLocation, - "Expecting " + ANDROID_MANIFEST_XML + " to have an <" + TAG_ACTIVITY + - "> tag."); - } else if (!mHasLauncherActivity) { - // Report the issue if the manifest file has no activity with a launcher intent. - context.report(ISSUE, mManifestLocation, - "Expecting " + ANDROID_MANIFEST_XML + - " to have an activity with a launcher intent."); - } - } - } - - @Override - public void afterCheckFile(@NonNull Context context) { - // Store a reference to the manifest file in case we need to report it's location. - if (context.getProject() == context.getMainProject()) { - mManifestLocation = Location.create(context.file); - } - } - - @Override - public void visitElement(XmlContext context, Element activityElement) { - // Checks every activity and reports an error if there is no activity with a launcher - // intent. - mHasActivity = true; - if (isMainActivity(activityElement)) { - mHasLauncherActivity = true; - } - } - - /** - * Returns true if the XML node is an activity with a launcher intent. - * - * @param activityNode The node to check. - * @return true if the node is an activity with a launcher intent. - */ - private boolean isMainActivity(Node activityNode) { - if (TAG_ACTIVITY.equals(activityNode.getNodeName())) { - // Loop through all tags - for (Element activityChild : LintUtils.getChildren(activityNode)) { - if (TAG_INTENT_FILTER.equals(activityChild.getNodeName())) { - // Check for these children nodes: - // - // - // - boolean hasLauncherCategory = false; - boolean hasMainAction = false; - - for (Element intentFilterChild : LintUtils.getChildren(activityChild)) { - // Check for category tag) - if (NODE_CATEGORY.equals(intentFilterChild.getNodeName()) - && CATEGORY_NAME_LAUNCHER.equals( - intentFilterChild.getAttributeNS(ANDROID_URI, ATTR_NAME))) { - hasLauncherCategory = true; - } - // Check for action tag - if (NODE_ACTION.equals(intentFilterChild.getNodeName()) - && ACTION_NAME_MAIN.equals( - intentFilterChild.getAttributeNS(ANDROID_URI, ATTR_NAME))) { - hasMainAction = true; - } - } - - if (hasLauncherCategory && hasMainAction) { - return true; - } - } - } - } - return false; - } -} diff --git a/android-studio-2/src/main/java/com/example/google/lint/ManifestConstants.java b/android-studio-2/src/main/java/com/example/google/lint/ManifestConstants.java deleted file mode 100644 index de7c6154..00000000 --- a/android-studio-2/src/main/java/com/example/google/lint/ManifestConstants.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.google.lint; - -/** - * Manifest Constant definition class. - * Amends constants not found in {@link com.android.SdkConstants}. - */ -public final class ManifestConstants { - public static final String CATEGORY_NAME_LAUNCHER = "android.intent.category.LAUNCHER"; - public static final String ACTION_NAME_MAIN = "android.intent.action.MAIN"; -} diff --git a/android-studio-2/src/main/java/com/example/google/lint/MyIssueRegistry.java b/android-studio-2/src/main/java/com/example/google/lint/MyIssueRegistry.java deleted file mode 100644 index a607b1bb..00000000 --- a/android-studio-2/src/main/java/com/example/google/lint/MyIssueRegistry.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.google.lint; - -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Collections; -import java.util.List; - -/** - * The list of issues that will be checked when running lint. - */ -@SuppressWarnings("unused") -public class MyIssueRegistry extends IssueRegistry { - @Override - public List getIssues() { - return Collections.singletonList(MainActivityDetector.ISSUE); - } -} diff --git a/android-studio-2/src/test/java/com/example/google/lint/MainActivityDetectorTest.java b/android-studio-2/src/test/java/com/example/google/lint/MainActivityDetectorTest.java deleted file mode 100644 index 6df66364..00000000 --- a/android-studio-2/src/test/java/com/example/google/lint/MainActivityDetectorTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example.google.lint; - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Collections; -import java.util.List; - -import static com.android.SdkConstants.FN_ANDROID_MANIFEST_XML; - -/** - * NOTE: This is not a final API; if you rely on this be prepared - * to adjust your code for the next tools release. - */ -public class MainActivityDetectorTest extends LintDetectorTest { - @Override - protected Detector getDetector() { - return new MainActivityDetector(); - } - - @Override - protected List getIssues() { - return Collections.singletonList(MainActivityDetector.ISSUE); - } - - /** - * Test that a manifest with an activity with a launcher intent has no warnings. - */ - public void testHasMainActivity() { - lint().files( - xml(FN_ANDROID_MANIFEST_XML, "" + - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "")) - .run() - .expectClean(); - } - - /** - * Test that a manifest without an activity with a launcher intent reports an error. - */ - public void testMissingMainActivity() { - String expected = "AndroidManifest.xml: Error: Expecting AndroidManifest.xml to have an " + - "activity with a launcher intent. [MainActivityDetector]\n" + - "1 errors, 0 warnings\n"; - lint().files( - xml(FN_ANDROID_MANIFEST_XML, "" + - "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - "")) - .run() - .expect(expected); - } - - /** - * Test that a manifest without an <application> tag reports an error. - */ - public void testMissingApplication() { - String expected = "AndroidManifest.xml: Error: Expecting AndroidManifest.xml to have an " + - " tag. [MainActivityDetector]\n" + - "1 errors, 0 warnings\n"; - lint().files( - xml(FN_ANDROID_MANIFEST_XML, "" + - "\n" + - "\n" + - "")) - .run() - .expect(expected); - } -} diff --git a/android-studio-3/README.md b/android-studio-3/README.md deleted file mode 100644 index 1ddb7f6d..00000000 --- a/android-studio-3/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# Sample Lint Checks - -This project shows how Android Studio 3 (beta 5 and later) handles packaging -of lint rules. - -## Lint Check Jar Library - -First, there's the lint check implementation itself. That's done in the -"checks" project, which just applies the Gradle "java" plugin, and -that project produces a jar. Note that the dependencies for the lint -check project (other than its testing dependencies) must all be "compileOnly": - - dependencies { - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.lint:lint-checks:$lintVersion" - ... - -## Lint Check AAR Library - -Next, there's a separate Android library project, called "library". This -library doesn't have any code on its own (though it could). However, -in its build.gradle, it specifies this: - - dependencies { - lintChecks project(':checks') - } - -This tells the Gradle plugin to take the output from the "checks" project -and package that as a "lint.jar" payload inside this library's AAR file. -When that's done, any other projects that depends on this library will -automatically be using the lint checks. - -## App Modules - -Note that you don't have to go through the extra "library indirection" -if you have a lint check that you only want to apply to one or more -app modules. You can simply include the `lintChecks` dependency as shown -above there as well, and then lint will include these rules when analyzing -the project. - -## Lint Version - -The lint version of the libraries (specified in this project as the -`lintVersion` variable in build.gradle) should be the same version -that is used by the Gradle plugin. - -If the Gradle plugin version is *X*.*Y*.*Z*, then the Lint library -version is *X+23*.*Y*.*Z*. diff --git a/android-studio-3/build.gradle b/android-studio-3/build.gradle deleted file mode 100644 index f9e42121..00000000 --- a/android-studio-3/build.gradle +++ /dev/null @@ -1,24 +0,0 @@ -buildscript { - ext { - lintVersion = '26.5.0' - } - - repositories { - google() - jcenter() - } - dependencies { - classpath "com.android.tools.build:gradle:3.5.2" - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/android-studio-3/checks/build.gradle b/android-studio-3/checks/build.gradle deleted file mode 100644 index b8789f32..00000000 --- a/android-studio-3/checks/build.gradle +++ /dev/null @@ -1,22 +0,0 @@ -apply plugin: 'java-library' - -dependencies { - // For a description of the below dependencies, see the main project README - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.lint:lint-checks:$lintVersion" - testImplementation "junit:junit:4.12" - testImplementation "com.android.tools.lint:lint:$lintVersion" - testImplementation "com.android.tools.lint:lint-tests:$lintVersion" - testImplementation "com.android.tools:testutils:$lintVersion" -} - -sourceCompatibility = "1.8" -targetCompatibility = "1.8" - -jar { - manifest { - // Only use the "-v2" key here if your checks have been updated to the - // new 3.0 APIs (including UAST) - attributes("Lint-Registry-v2": "com.example.lint.checks.SampleIssueRegistry") - } -} diff --git a/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.java b/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.java deleted file mode 100644 index 45acc6ed..00000000 --- a/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.lint.checks; - -import com.android.tools.lint.client.api.UElementHandler; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.UastScanner; -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.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.jetbrains.uast.UElement; -import org.jetbrains.uast.ULiteralExpression; -import org.jetbrains.uast.UastLiteralUtils; - -import java.util.Collections; -import java.util.List; - -/** - * Sample detector showing how to analyze Kotlin/Java code. - * This example flags all string literals in the code that contain - * the word "lint". - */ -public class SampleCodeDetector extends Detector implements UastScanner { - /** Issue describing the problem and pointing to the detector implementation */ - public static final Issue ISSUE = Issue.create( - // ID: used in @SuppressLint warnings etc - "ShortUniqueId", - - // Title -- shown in the IDE's preference dialog, as category headers in the - // Analysis results window, etc - "Lint Mentions", - - // Full explanation of the issue; you can use some markdown markup such as - // `monospace`, *italic*, and **bold**. - "This check highlights string literals in code which mentions " + - "the word `lint`. Blah blah blah.\n" + - "\n" + - "Another paragraph here.\n", - Category.CORRECTNESS, - 6, - Severity.WARNING, - new Implementation( - SampleCodeDetector.class, - Scope.JAVA_FILE_SCOPE)); - - @Override - public List> getApplicableUastTypes() { - return Collections.singletonList(ULiteralExpression.class); - } - - @Override - public UElementHandler createUastHandler(JavaContext context) { - // Note: Visiting UAST nodes is a pretty general purpose mechanism; - // Lint has specialized support to do common things like "visit every class - // that extends a given super class or implements a given interface", and - // "visit every call site that calls a method by a given name" etc. - // Take a careful look at UastScanner and the various existing lint check - // implementations before doing things the "hard way". - // Also be aware of context.getJavaEvaluator() which provides a lot of - // utility functionality. - return new UElementHandler() { - @Override - public void visitLiteralExpression(ULiteralExpression expression) { - String string = UastLiteralUtils.getValueIfStringLiteral(expression); - if (string == null) { - return; - } - - if (string.contains("lint") && string.matches(".*\\blint\\b.*")) { - context.report(ISSUE, expression, context.getLocation(expression), - "This code mentions `lint`: **Congratulations**"); - } - } - }; - } -} diff --git a/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.java b/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.java deleted file mode 100644 index 92cd6b14..00000000 --- a/android-studio-3/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.lint.checks; - -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.detector.api.ApiKt; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Collections; -import java.util.List; - -/* - * The list of issues that will be checked when running lint. - */ -public class SampleIssueRegistry extends IssueRegistry { - @Override - public List getIssues() { - return Collections.singletonList(SampleCodeDetector.ISSUE); - } - - @Override - public int getApi() { - return ApiKt.CURRENT_API; - } -} - diff --git a/android-studio-3/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.java b/android-studio-3/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.java deleted file mode 100644 index e6c7e8a4..00000000 --- a/android-studio-3/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.example.lint.checks; - -import com.android.tools.lint.checks.infrastructure.LintDetectorTest; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; - -import java.util.Collections; -import java.util.List; - -public class SampleCodeDetectorTest extends LintDetectorTest { - public void testBasic() { - lint().files( - java("" + - "package test.pkg;\n" + - "public class TestClass1 {\n" + - " // In a comment, mentioning \"lint\" has no effect\n" + - " private static String s1 = \"Ignore non-word usages: linting\";\n" + - " private static String s2 = \"Let's say it: lint\";\n" + - "}")) - .run() - .expect("src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId]\n" + - " private static String s2 = \"Let's say it: lint\";\n" + - " ~~~~~~~~~~~~~~~~~~~~\n" + - "0 errors, 1 warnings\n"); - } - - @Override - protected Detector getDetector() { - return new SampleCodeDetector(); - } - - @Override - protected List getIssues() { - return Collections.singletonList(SampleCodeDetector.ISSUE); - } -} \ No newline at end of file diff --git a/android-studio-3/gradle/wrapper/gradle-wrapper.jar b/android-studio-3/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/android-studio-3/gradlew.bat b/android-studio-3/gradlew.bat deleted file mode 100644 index aec99730..00000000 --- a/android-studio-3/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android-studio-3/library/build.gradle b/android-studio-3/library/build.gradle deleted file mode 100644 index 90f7ba01..00000000 --- a/android-studio-3/library/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -apply plugin: 'com.android.library' - -android { - compileSdkVersion 29 - defaultConfig { - minSdkVersion 15 - targetSdkVersion 29 - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -/** Package the given lint checks library into this AAR */ -dependencies { - lintChecks project(':checks') -} diff --git a/android-studio-3/settings.gradle b/android-studio-3/settings.gradle deleted file mode 100644 index b613efff..00000000 --- a/android-studio-3/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':checks', ':library' diff --git a/android-studio-4/README.md b/android-studio-4/README.md deleted file mode 100644 index e025c26a..00000000 --- a/android-studio-4/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Sample Lint Checks - -This project shows how Android Studio 4 handles packaging of lint rules. - -## Lint Check Jar Library - -First, there's the lint check implementation itself. That's done in the -"checks" project, which just applies the Gradle "java" plugin, and -that project produces a jar. Note that the dependencies for the lint -check project (other than its testing dependencies) must all be "compileOnly": - - dependencies { - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.lint:lint-checks:$lintVersion" - ... - -## Lint Check AAR Library - -Next, there's a separate Android library project, called "library". This -library doesn't have any code on its own (though it could). However, -in its build.gradle, it specifies this: - - dependencies { - lintPublish project(':checks') - } - -This tells the Gradle plugin to take the output from the "checks" project -and package that as a "lint.jar" payload inside this library's AAR file. -When that's done, any other projects that depends on this library will -automatically be using the lint checks. - -## App Modules - -Note that you don't have to go through the extra "library indirection" -if you have a lint check that you only want to apply to one or more -app modules. You can simply include the `lintChecks` dependency as shown -above there as well, and then lint will include these rules when analyzing -the project. - -## Lint Version - -The lint version of the libraries (specified in this project as the -`lintVersion` variable in build.gradle) should be the same version -that is used by the Gradle plugin. - -If the Gradle plugin version is *X*.*Y*.*Z*, then the Lint library -version is *X+23*.*Y*.*Z*. diff --git a/android-studio-4/build.gradle b/android-studio-4/build.gradle deleted file mode 100644 index 061e7c28..00000000 --- a/android-studio-4/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -buildscript { - ext { - gradlePluginVersion = '4.1.0-alpha02' - lintVersion = '27.1.0-alpha02' - kotlinVersion = '1.3.70' - } - - repositories { - google() - jcenter() - } - dependencies { - classpath "com.android.tools.build:gradle:$gradlePluginVersion" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/android-studio-4/gradle.properties b/android-studio-4/gradle.properties deleted file mode 100644 index aac7c9b4..00000000 --- a/android-studio-4/gradle.properties +++ /dev/null @@ -1,17 +0,0 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true diff --git a/android-studio-4/gradle/wrapper/gradle-wrapper.jar b/android-studio-4/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5e24af05341d49695ee84e5f9b594659..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53636 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ⅅ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 H-+%o-(+5aQ diff --git a/android-studio-4/gradle/wrapper/gradle-wrapper.properties b/android-studio-4/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 17b347d4..00000000 --- a/android-studio-4/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Sep 01 06:52:38 PDT 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.1-all.zip diff --git a/android-studio-4/gradlew b/android-studio-4/gradlew deleted file mode 100755 index 9d82f789..00000000 --- a/android-studio-4/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android-studio-4/library/src/main/AndroidManifest.xml b/android-studio-4/library/src/main/AndroidManifest.xml deleted file mode 100644 index 843049f7..00000000 --- a/android-studio-4/library/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/android-studio-4/settings.gradle b/android-studio-4/settings.gradle deleted file mode 100644 index b613efff..00000000 --- a/android-studio-4/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':checks', ':library' diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..8db51f6c --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + + defaultConfig { + applicationId "com.android.example.lint_usage" + minSdkVersion 21 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + lintOptions { + textReport true + + // Produce report for CI: + // https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/sarif-support-for-code-scanning + sarifOutput file("../lint-results.sarif") + } +} + +dependencies { + implementation project(':library') +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..41bcadc5 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/java/com/android/example/Test.kt b/app/src/main/java/com/android/example/Test.kt new file mode 100644 index 00000000..4983aebe --- /dev/null +++ b/app/src/main/java/com/android/example/Test.kt @@ -0,0 +1,10 @@ +package com.android.example + +class Test { + // We have a custom lint check bundled with :library + // that this module depends on. The lint check looks + // for mentions of "lint", which should trigger an + // error + val s = "lint" + fun lint() { } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..ec8e81d2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,33 @@ +buildscript { + ext { + kotlinVersion = '1.4.31' + + // Current lint target: Studio 4.2 / AGP 7 + //gradlePluginVersion = '4.2.0-beta05' + //lintVersion = '27.2.0-beta05' + + // Upcoming lint target: Arctic Fox / AGP 7 + gradlePluginVersion = '7.0.0-alpha08' + lintVersion = '30.0.0-alpha08' // if gradle plugin was 4.1.2, you'd use 27.1.2 here + } + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android-studio-4/checks/.gitignore b/checks/.gitignore similarity index 100% rename from android-studio-4/checks/.gitignore rename to checks/.gitignore diff --git a/android-studio-4/checks/build.gradle b/checks/build.gradle similarity index 51% rename from android-studio-4/checks/build.gradle rename to checks/build.gradle index 592fe5bd..ae3de340 100644 --- a/android-studio-4/checks/build.gradle +++ b/checks/build.gradle @@ -15,7 +15,20 @@ dependencies { compileOnly "com.android.tools.lint:lint-api:$lintVersion" compileOnly "com.android.tools.lint:lint-checks:$lintVersion" compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - testImplementation "junit:junit:4.13" + + // TEMPORARY WORKAROUND + // Due to https://issuetracker.google.com/180889192, for some builds + // of 7.0 you may need to add one or more of the following dependencies, + // depending on which APIs your lint checks depend on. These were accidentally + // not exported by the lint-api artifact as api dependencies. This will + // be fixed by AGP 7.0.0-alpha10. + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.layoutlib:layoutlib-api:$lintVersion" + compileOnly "com.android.tools:common:$lintVersion" + compileOnly "com.android.tools.external.com-intellij:kotlin-compiler:$lintVersion" + compileOnly "org.ow2.asm:asm:7.0" + + testImplementation "junit:junit:4.13.2" testImplementation "com.android.tools.lint:lint:$lintVersion" testImplementation "com.android.tools.lint:lint-tests:$lintVersion" testImplementation "com.android.tools:testutils:$lintVersion" diff --git a/android-studio-4/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt b/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt similarity index 61% rename from android-studio-4/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt rename to checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt index 14603725..0c9ca416 100644 --- a/android-studio-4/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt +++ b/checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt @@ -16,24 +16,29 @@ package com.example.lint.checks import com.android.tools.lint.client.api.UElementHandler -import com.android.tools.lint.detector.api.* +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Detector.UastScanner +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.Scope +import com.android.tools.lint.detector.api.Severity import org.jetbrains.uast.UElement import org.jetbrains.uast.ULiteralExpression import org.jetbrains.uast.evaluateString /** - * Sample detector showing how to analyze Kotlin/Java code. - * This example flags all string literals in the code that contain - * the word "lint". + * Sample detector showing how to analyze Kotlin/Java code. This example + * flags all string literals in the code that contain the word "lint". */ @Suppress("UnstableApiUsage") class SampleCodeDetector : Detector(), UastScanner { - override fun getApplicableUastTypes(): List>? { + override fun getApplicableUastTypes(): List> { return listOf(ULiteralExpression::class.java) } - override fun createUastHandler(context: JavaContext): UElementHandler? { + override fun createUastHandler(context: JavaContext): UElementHandler { // Note: Visiting UAST nodes is a pretty general purpose mechanism; // Lint has specialized support to do common things like "visit every class // that extends a given super class or implements a given interface", and @@ -46,35 +51,42 @@ class SampleCodeDetector : Detector(), UastScanner { override fun visitLiteralExpression(node: ULiteralExpression) { val string = node.evaluateString() ?: return if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) { - context.report(ISSUE, node, context.getLocation(node), - "This code mentions `lint`: **Congratulations**") + context.report( + ISSUE, node, context.getLocation(node), + "This code mentions `lint`: **Congratulations**" + ) } } } } companion object { - /** Issue describing the problem and pointing to the detector implementation */ + /** + * Issue describing the problem and pointing to the detector + * implementation. + */ @JvmField val ISSUE: Issue = Issue.create( - // ID: used in @SuppressLint warnings etc - id = "ShortUniqueId", - // Title -- shown in the IDE's preference dialog, as category headers in the - // Analysis results window, etc - briefDescription = "Lint Mentions", - // Full explanation of the issue; you can use some markdown markup such as - // `monospace`, *italic*, and **bold**. - explanation = """ + // ID: used in @SuppressLint warnings etc + id = "ShortUniqueId", + // Title -- shown in the IDE's preference dialog, as category headers in the + // Analysis results window, etc + briefDescription = "Lint Mentions", + // Full explanation of the issue; you can use some markdown markup such as + // `monospace`, *italic*, and **bold**. + explanation = """ This check highlights string literals in code which mentions the word `lint`. \ Blah blah blah. Another paragraph here. """, // no need to .trimIndent(), lint does that automatically - category = Category.CORRECTNESS, - priority = 6, - severity = Severity.WARNING, - implementation = Implementation( - SampleCodeDetector::class.java, - Scope.JAVA_FILE_SCOPE)) + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + SampleCodeDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) } } diff --git a/android-studio-4/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt b/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt similarity index 61% rename from android-studio-4/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt rename to checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt index f4c4f723..aafaafa9 100644 --- a/android-studio-4/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt +++ b/checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 The Android Open Source Project + * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package com.example.lint.checks import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor import com.android.tools.lint.detector.api.CURRENT_API /* @@ -27,4 +28,15 @@ class SampleIssueRegistry : IssueRegistry() { override val api: Int get() = CURRENT_API -} \ No newline at end of file + + override val minApi: Int + get() = 8 // works with Studio 4.1 or later; see com.android.tools.lint.detector.api.Api / ApiKt + + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://github.com/googlesamples/android-custom-lint-rules/issues", + contact = "https://github.com/googlesamples/android-custom-lint-rules" + ) +} diff --git a/android-studio-4/checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry b/checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry similarity index 100% rename from android-studio-4/checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry rename to checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry diff --git a/android-studio-4/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt b/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt similarity index 72% rename from android-studio-4/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt rename to checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt index d5427cb7..30c86c63 100644 --- a/android-studio-4/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt +++ b/checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt @@ -15,15 +15,17 @@ */ package com.example.lint.checks -import com.android.tools.lint.checks.infrastructure.LintDetectorTest -import com.android.tools.lint.detector.api.Detector -import com.android.tools.lint.detector.api.Issue +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test @Suppress("UnstableApiUsage") -class SampleCodeDetectorTest : LintDetectorTest() { +class SampleCodeDetectorTest { + @Test fun testBasic() { lint().files( - java(""" + java( + """ package test.pkg; public class TestClass1 { // In a comment, mentioning "lint" has no effect @@ -31,22 +33,17 @@ class SampleCodeDetectorTest : LintDetectorTest() { private static String s2 = "Let's say it: lint"; } """ - ).indented()) - .run() - .expect(""" + ).indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId] private static String s2 = "Let's say it: lint"; ~~~~~~~~~~~~~~~~~~~~ 0 errors, 1 warnings """ - ) + ) } - - override fun getDetector(): Detector { - return SampleCodeDetector() - } - - override fun getIssues(): List { - return listOf(SampleCodeDetector.ISSUE) - } -} \ No newline at end of file +} diff --git a/android-studio-3/gradle.properties b/gradle.properties similarity index 100% rename from android-studio-3/gradle.properties rename to gradle.properties diff --git a/android-studio-2/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from android-studio-2/gradle/wrapper/gradle-wrapper.jar rename to gradle/wrapper/gradle-wrapper.jar diff --git a/android-studio-3/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties similarity index 93% rename from android-studio-3/gradle/wrapper/gradle-wrapper.properties rename to gradle/wrapper/gradle-wrapper.properties index f0f067c5..3393835d 100644 --- a/android-studio-3/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip diff --git a/android-studio-3/gradlew b/gradlew similarity index 100% rename from android-studio-3/gradlew rename to gradlew diff --git a/android-studio-4/gradlew.bat b/gradlew.bat similarity index 100% rename from android-studio-4/gradlew.bat rename to gradlew.bat diff --git a/android-studio-4/library/.gitignore b/library/.gitignore similarity index 100% rename from android-studio-4/library/.gitignore rename to library/.gitignore diff --git a/android-studio-4/library/build.gradle b/library/build.gradle similarity index 86% rename from android-studio-4/library/build.gradle rename to library/build.gradle index 2261372a..fa5ba221 100644 --- a/android-studio-4/library/build.gradle +++ b/library/build.gradle @@ -1,10 +1,10 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { minSdkVersion 15 - targetSdkVersion 29 + targetSdkVersion 30 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 diff --git a/android-studio-3/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml similarity index 100% rename from android-studio-3/library/src/main/AndroidManifest.xml rename to library/src/main/AndroidManifest.xml diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..802b0e45 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':checks', ':library', ':app' From 9452c0248df1732c1cb3a2b5c59a17eb5ddb9210 Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Mon, 15 Mar 2021 17:22:00 -0700 Subject: [PATCH 02/55] Remove temporary workaround for 180889192 --- build.gradle | 4 ++-- checks/build.gradle | 14 +------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/build.gradle b/build.gradle index ec8e81d2..b6f8958d 100644 --- a/build.gradle +++ b/build.gradle @@ -7,8 +7,8 @@ buildscript { //lintVersion = '27.2.0-beta05' // Upcoming lint target: Arctic Fox / AGP 7 - gradlePluginVersion = '7.0.0-alpha08' - lintVersion = '30.0.0-alpha08' // if gradle plugin was 4.1.2, you'd use 27.1.2 here + gradlePluginVersion = '7.0.0-alpha10' + lintVersion = '30.0.0-alpha10' // if gradle plugin was 4.1.2, you'd use 27.1.2 here } repositories { diff --git a/checks/build.gradle b/checks/build.gradle index ae3de340..a47ed9b8 100644 --- a/checks/build.gradle +++ b/checks/build.gradle @@ -13,25 +13,13 @@ lintOptions { dependencies { // For a description of the below dependencies, see the main project README compileOnly "com.android.tools.lint:lint-api:$lintVersion" + // You typically don't need this one: compileOnly "com.android.tools.lint:lint-checks:$lintVersion" compileOnly "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - // TEMPORARY WORKAROUND - // Due to https://issuetracker.google.com/180889192, for some builds - // of 7.0 you may need to add one or more of the following dependencies, - // depending on which APIs your lint checks depend on. These were accidentally - // not exported by the lint-api artifact as api dependencies. This will - // be fixed by AGP 7.0.0-alpha10. - compileOnly "com.android.tools.lint:lint-api:$lintVersion" - compileOnly "com.android.tools.layoutlib:layoutlib-api:$lintVersion" - compileOnly "com.android.tools:common:$lintVersion" - compileOnly "com.android.tools.external.com-intellij:kotlin-compiler:$lintVersion" - compileOnly "org.ow2.asm:asm:7.0" - testImplementation "junit:junit:4.13.2" testImplementation "com.android.tools.lint:lint:$lintVersion" testImplementation "com.android.tools.lint:lint-tests:$lintVersion" - testImplementation "com.android.tools:testutils:$lintVersion" } sourceCompatibility = "1.8" From c84628ac087b05095146521a343ff2836aab706d Mon Sep 17 00:00:00 2001 From: Tor Norbye Date: Sat, 20 Mar 2021 09:45:40 -0700 Subject: [PATCH 03/55] Check in snapshot of the lint API documentation. That way, we can point browsers directly to the HTML which isn't allowed from cs.android.com. --- README.md | 10 + docs/.gitignore | 5 + docs/README.md | 12 + docs/README.md.html | 42 + docs/api-guide/basics.md.html | 1070 ++++ docs/api-guide/changes.md.html | 75 + docs/api-guide/example.md.html | 362 ++ docs/api-guide/faq.md.html | 308 ++ docs/api-guide/nested-syntax-highlighting.png | Bin 0 -> 134730 bytes docs/api-guide/partial-analysis.md.html | 633 +++ docs/api-guide/publishing.md.html | 114 + docs/api-guide/quickfixes.md.html | 294 ++ docs/api-guide/terminology.md.html | 112 + docs/api-guide/unit-testing.md.html | 352 ++ docs/book.html | 4410 +++++++++++++++++ docs/book.md.html | 21 + docs/changes.md.html | 17 + docs/features.md.html | 140 + docs/internal/guidelines.md.html | 80 + docs/usage/baselines.md.html | 87 + docs/usage/changes.md.html | 35 + docs/usage/lintxml.md.html | 129 + docs/usage/suppressing.md.html | 71 + docs/usage/variables.md.html | 152 + 24 files changed, 8531 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/README.md.html create mode 100644 docs/api-guide/basics.md.html create mode 100644 docs/api-guide/changes.md.html create mode 100644 docs/api-guide/example.md.html create mode 100644 docs/api-guide/faq.md.html create mode 100644 docs/api-guide/nested-syntax-highlighting.png create mode 100644 docs/api-guide/partial-analysis.md.html create mode 100644 docs/api-guide/publishing.md.html create mode 100644 docs/api-guide/quickfixes.md.html create mode 100644 docs/api-guide/terminology.md.html create mode 100644 docs/api-guide/unit-testing.md.html create mode 100644 docs/book.html create mode 100644 docs/book.md.html create mode 100644 docs/changes.md.html create mode 100644 docs/features.md.html create mode 100644 docs/internal/guidelines.md.html create mode 100644 docs/usage/baselines.md.html create mode 100644 docs/usage/changes.md.html create mode 100644 docs/usage/lintxml.md.html create mode 100644 docs/usage/suppressing.md.html create mode 100644 docs/usage/variables.md.html diff --git a/README.md b/README.md index 6e988fb2..b7468d84 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ Custom Lint Rules ================= +The lint source code contains a lot of documentation on how to write +custom checks; this git repository contains a snapshot of this +documentation which you can read here: + +* [Full API Guide](https://google.github.io/android-custom-lint-rules/docs/book.html) +* [Other docs](https://google.github.io/android-custom-lint-rules/docs/README.md.html) + +Lint +---- + The [Android `lint` tool](http://developer.android.com/tools/help/lint.html) is a static code analysis tool that checks your project source files for potential bugs and optimization improvements for correctness, security, performance, usability, accessibility, and diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..92ba6dce --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,5 @@ +markdeep.min.js +todo.md +blank.md.html +spell-check.sh +spelling-ok.txt diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..5a5e6418 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +Android Lint +============ + +(Note that many files in this docs folder has the extension `.md.html` +instead of just `.md`. That means that they can be opened in a +browser for better visualization (such as syntax highlighting of code +snippets) but are readable and editable as markdown.) + +Let's go there right now: [Link](README.md.html) + +There is also a pre-rendered HTML version of the API Guide +[here](book.html) suitable for online hosting. diff --git a/docs/README.md.html b/docs/README.md.html new file mode 100644 index 00000000..ea8c9183 --- /dev/null +++ b/docs/README.md.html @@ -0,0 +1,42 @@ +Android Lint +============ + +Android Lint is a static analysis tool (which despite the name is not +limited to Android, and within Google for example is used to analyze +Java and Kotlin server side code as well as Android and even desktop +software like IDEs.) + +Lint's focus is on finding bugs (whether they are related to +correctness, performance, security, internationalization, usability and +so on); it's not a source code style checker. + +Available documentation: + +* [Lint Features](features.md.html) +* [Recent Changes](changes.md.html) +* Documents for users of lint, in the `usage` folder: + - [How to suppress incidents](usage/suppressing.md.html) + - [How to use baselines](usage/baselines.md.html) + - [How to use `lint.xml` files](usage/lintxml.md.html) + - [Environment variables and properties](usage/variables.md.html) +* Documents for authors of additional lint checks, in the + `api-guide` folder: + - [Complete Book](book.md.html), containing all of the below + documents as chapters, suitable for offline reading + - [Basics](api-guide/basics.md.html) + - [A Sample Lint Check](api-guide/example.md.html) + - [Publishing a Lint check](api-guide/publishing.md.html) + - [Unit Testing](api-guide/unit-testing.md.html) + - [Adding Quick Fixes](api-guide/quickfixes.md.html) + - [Terminology](api-guide/terminology.md.html) + - [Partial analysis](api-guide/partial-analysis.md.html) + - [Frequently Asked Questions](api-guide/faq.md.html) +* Documents for lint internals, intended for developers of lint + itself, in the `internal` folder: + - [Guidelines](internal/guidelines.md.html) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Documentation History: +* March 2021: Initial version + + diff --git a/docs/api-guide/basics.md.html b/docs/api-guide/basics.md.html new file mode 100644 index 00000000..f7683432 --- /dev/null +++ b/docs/api-guide/basics.md.html @@ -0,0 +1,1070 @@ + + +# Writing a Lint Check: Basics + +## Preliminaries + +(If you already know a lot of the basics but you're here because you've +run into a problem and you're consulting the docs, take a look at the +frequently asked questions chapter.) + +### “Lint?” + +The `lint` tool shipped with the C compiler and provided additional +static analysis of C code beyond what the compiler checked. + +Android Lint was named in honor of this tool, and with the Android +prefix to make it really clear that this is a static analysis tool +intended for analysis of Android code, provided by the Android Open +Source Project -- and to disambiguate it from the many other tools with +"lint“ in their names. + +However, since then, Android Lint has broadened its support and is no +longer intended only for Android code. In fact, within Google, it is +used to analyze all Java and Kotlin code. One of the reasons for this +is that it can easily analyze both Java and Kotlin code without having +to implement the checks twice. Additional features are described in the +[features](../features.html.md) chapter. + +We're planning to rename lint to reflect this new role, so we are +looking for good name suggestions. + +### API Stability + +Lint's APIs are still marked as @Beta, and we have made it very clear +all along that this is not a stable API, so custom lint checks may need +to be updated periodically to keep working. + +However, ”some APIs are more stable than others“. In particular, the +detector API (described below) is much less likely to change than the +client API (which is not intended for lint check authors but for tools +integrating lint to run within, such as IDEs and build systems). + +However, this doesn't mean the detector API won't change. A large part +of the API surface is external to lint; it's the AST libraries (PSI and +UAST) for Java and Kotlin from JetBrains; it's the bytecode library +(asm.ow2.io), it's the XML DOM library (org.w3c.dom), and so on. Lint +intentionally stays up to date with these, so any API or behavior +changes in these can affect your lint checks. + +Lint's own APIs may also change. The current API has grown organically +over the last 10 years (the first version of lint was released in 2011) +and there are a number of things we'd clean up and do differently if +starting over. Not to mention rename and clean up inconsistencies. + +However, lint has been pretty widely adopted, so at this point creating +a nicer API would probably cause more harm than good, so we're limiting +recent changes to just the necessary ones. An example of this is the +new [partial analysis](partial-analysis.md.html) architecture in 7.0 +which is there to allow much better CI and incremental analysis +performance. + +### Kotlin + +We recommend that you implement your checks in Kotlin. Part of +the reason for that is that the lint API uses a number of Kotlin +features: + +* **Named and default parameters**: Rather than using builders, some + construction methods, like `Issue.create()` have a lot of parameters + with default parameters. The API is cleaner to use if you just + specify what you need and rely on defaults for everything else. + +* **Compatibility**: We may add additional parameters over time. It + isn't practical to add @JvmOverloads on everything. + +* **Package-level functions**: Lint's API includes a number of package + level utility functions (in previous versions of the API these are all + thrown together in a `LintUtils` class). + +* **Deprecations**: Kotlin has support for simple API migrations. For + example, in the below example, the new `@Deprecated` annotation on + lines 1 through 7 will be added in an upcoming release, to ease + migration to a new API. IntelliJ can automatically quickfix these + deprecation replacements. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +@Deprecated( + "Use the new report(Incident) method instead, which is more future proof", + ReplaceWith( + "report(Incident(issue, message, location, null, quickfixData))", + "com.android.tools.lint.detector.api.Incident" + ) +) +@JvmOverloads +open fun report( + issue: Issue, + location: Location, + message: String, + quickfixData: LintFix? = null +) { + // ... +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As of 7.0, there is more Kotlin code in lint than remaining Java +code: + +Language | files | blank | comment | code +-------------|------:|--------:|--------:|------: +Kotlin | 420 | 14243 | 23239 | 130250 +Java | 289 | 8683 | 15205 | 101549 + [`$ cloc lint/`] + +And that's for all of lint, including many old lint detectors which +haven't been touched in years. In the Lint API library, +`lint/libs/lint-api`, the code is 78% Kotlin and 22% Java. + +## Concepts + +Lint will search your source code for problems. There are many types of +problems, and each one is called an `Issue`, which has associated +metadata like a unique id, a category, an explanation, and so on. + +Each instance that it finds is called an ”incident“. + +The actual responsibility of searching for and reporting incidents is +handled by detectors -- subclasses of `Detector`. Your lint check will +extend `Detector`, and when it has found a problem, it will ”report“ +the incident to lint. + +A `Detector` can analyze more than one `Issue`. For example, the +built-in `StringFormatDetector` analyzes formatting strings passed to +`String.format()` calls, and in the process of doing that discovers +multiple unrelated issues -- invalid formatting strings, formatting +strings which should probably use the plurals API instead, mismatched +types, and so on. The detector could simply have a single issue called +"StringFormatProblems” and report everything as a StringFormatProblem, +but that's not a good idea. Each of these individual types of String +format problems should have their own explanation, their own category, +their own severity, and most importantly should be individually +configurable by the user such that they can disable or promote one of +these issues separately from the others. + +A `Detector` can indicate which sets of files it cares about. These are +called “scopes”, and the way this works is that when you register your +`Issue`, you tell that issue which `Detector` class is responsible for +analyzing it, as well as which scopes the detector cares about. + +If for example a lint check wants to analyze Kotlin files, it can +include the `Scope.JAVA_FILE` scope, and now that detector will be +included when lint processes Java or Kotin files. + +!!! Tip + The name `Scope.JAVA_FILE` may make it sound like there should also + be a `Scope.KOTLIN_FILE`. However, `JAVA_FILE` here really refers to + both Java and Kotlin files since the analysis and APIs are identical + for both (using “UAST”, a universal abstract syntax tree). However, + at this point we don't want to rename it since it would break a lot + of existing checks. We might introduce an alias and deprecate this + one in the future. + +When detectors implement various callbacks, they can analyze the +code, and if they find a problematic pattern, they can “report” +the incident. This means computing an error message, as well as +a “location”. A “location” for an incident is really an error +range -- a file, and a starting offset and an ending offset. Locations +can also be linked together, so for example for a “duplicate +declaration” error, you can and should include both locations. + +Many detector methods will pass in a `Context`, or a more specific +subclass of `Context` such as `JavaContext` or `XmlContext`. This +allows lint to provide access to the detectors information they may +need, without passing in a lot of parameters (and allowing lint to add +additional data over time without breaking signatures). + +The `Context` classes also provide many convenience APIs. For example, +for `XmlContext` there are methods for creating locations for XML tags, +XML attributes, just the name part of an XML attribute and just the +value part of an XML attribute. For a `JavaContext` there are also +methods for creating locations, such as for a method call, including +whether to include the receiver and/or the argument list. + +When you report an `Incident` you can also provide a `LintFix`; this is +a quickfix which the IDE can use to offer actions to take on the +warning. In some cases, you can offer a complete and correct fix (such +as removing an unused element). In other cases the fix may be less +clear; for example, the `AccessibilityDetector` asks you to set a +description for images; the quickfix will set the content attribute, +but will leave the text value as TODO and will select the string such +that the user can just type to replace it. + +!!! Tip + When reporting incidents, make sure that the error messages are not + generic; try to be explicit and include specifics for the current + scenario. For example, instead of just “Duplicate declaration”, use + “`$name` has already been declared”. This isn't just for cosmetics; + it also makes lint's [baseline + mechanism](../usage/baselines.md.html) work better since it + currently matches by id + file + message, not by line numbers which + typically drift over time. + +## Client API versus Detector API + +Lint's API has two halves: + +- The **Client API**: “Integrate (and run) lint from within a tool”. + For example, both the IDE and the build system uses this API to embed + and invoke lint to analyze the code in the project or editor. + +- The **Detector API**: “Implement a new lint check”. This is the API + which lets checkers analyze code and report problems that they find. + +The class in the Client API which represents lint running in a tool is +called `LintClient`. This class is responsible for, among other things: + +* Reporting incidents found by detectors. For example, in the IDE, it + will place error markers into the source editor, and in a build + system, it may write warnings to the console or generate a report or + even fail the build. + +* Handling I/O. Detectors should never read files from disk directly. + This allows lint checks to work smoothly in for example the IDE. When + lint runs on the fly, and a lint check asks for the source file + contents (or other supporting files), the `LintClient` in the IDE + will implement the `readFile` method to first look in the open source + editors and if the requested file is being edited, it will return the + current (often unsaved!) contents. + +* Handling network traffic. Lint checks should never open + URLConnections themselves. By going through the lint API to request + data for a URL, not only can the LintClient for example use any + configured IDE proxy settings which is done in the IntelliJ + integration of lint, but even the lint check's own unit tests can + easily be tested because the special unit test implementation of a + `LintClient` provides a simple way to provide exact responses for + specific URLs: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +lint() + .files(...) + // Set up exactly the expected maven.google.com network output to + // ensure stable version suggestions in the tests + .networkData("https://maven.google.com/master-index.xml", "" + + "\n" + + "\n" + + " " + + "") + .networkData("https://maven.google.com/com/android/tools/build/group-index.xml", "" + + "\n" + + "\n" + + " \n" + + "") +.run() +.expect(...) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +And much, much, more. **However, most of the implementation of +`LintClient` is intended for integration of lint itself, and as a check +author you don't need to worry about it.** It's the detector API that +matters, and is also less likely to change than the client API. + +!!! Tip + The division between the two halves is not perfect; some classes + do not fit neatly in between the two or historically were put in + the wrong place, so this is a high level design to be aware of but + which is not absolute. + +Also, + +!!! Warning + Because of the division between two separate packages, which in + retrospect was a mistake, a number of APIs that are only intended + for internal lint usage have been made `public` such that lint's + code in one package can access it from the other. There's normally a + comment explaining that this is for internal use only, but be aware + that just because something is `public` or not `final` it's a good + idea to call or override it. + +## Creating an Issue + +For information on how to set up the project and to actually publish +your lint checks, see the [sample](example.md.html) and +[publishing](publishing.md.html) chapters. + +`Issue` is a final class, so unlike `Detector`, you don't subclass +it, you instantiate it via `Issue.create`. + +By convention, issues are registered inside the companion object of the +corresponding detector, but that is not required. + +Here's an example: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class SdCardDetector : Detector(), SourceCodeScanner { + companion object Issues { + @JvmField + val ISSUE = Issue.create( + id = "SdCardPath", + briefDescription = "Hardcoded reference to `/sdcard`", + explanation = """ + Your code should not reference the `/sdcard` path directly; \ + instead use `Environment.getExternalStorageDirectory().getPath()`. + + Similarly, do not reference the `/data/data/` path directly; it \ + can vary in multi-user scenarios. Instead, use \ + `Context.getFilesDir().getPath()`. + """, + moreInfo = "https://developer.android.com/training/data-storage#filesExternal", + category = Category.CORRECTNESS, + severity = Severity.WARNING, + androidSpecific = true, + implementation = Implementation( + SdCardDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } + ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are a number of things to note here. + +On line 4, we have the `Issue.create()` call. We store the issue into a +property such that we can reference this issue both from the +`IssueRegistry`, where we provide the `Issue` to lint, and also in the +`Detector` code where we report incidents of the issue. + +Note that `Issue.create` is a method with a lot of parameters (and we +will probably add more parameters in the future). Therefore, it's a +good practice to explicitly include the argument names (and therefore +to implement your code in Kotlin). + +The `Issue` provides metadata about a type of problem. + +The **`id`** is a short, unique identifier for this issue. By +convention it is a combination of words, capitalized camel case (though +you can also add your own package prefix as in Java packages). Note +that the id is “user visible”; it is included in text output when lint +runs in the build system, such as this: + +```shell +src/main/kotlin/test/pkg/MyTest.kt:4: Warning: Do not hardcode "/sdcard/"; + use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath] + val s: String = "/sdcard/mydir" + ~~~~~~~~~~~~~ +0 errors, 1 warnings +``` + +(Notice the `[SdCardPath]` suffix at the end of the error message.) + +The reason the id is made known to the user is that the ID is how +they'll configure and/or suppress issues. For example, to suppress the +warning in the current method, use + +``` +@Suppress("SdCardPath") +``` + +(or in Java, @SuppressWarnings). Note that there is an IDE quickfix to +suppress an incident which will automatically add these annotations, so +you don't need to know the ID in order to be able to suppress an +incident, but the ID will be visible in the annotation that it +generates, so it should be reasonably specific. + +Also, since the namespace is global, try to avoid picking generic names +that could clash with others, or seem to cover a larger set of issues +than intended. For example, “InvalidDeclaration” would be a poor id +since that can cover a lot of potential problems with declarations +across a number of languages and technologies. + +Next, we have the **`briefDescription`**. You can think of this as a +"category report header“; this is a static description for all +incidents of this type, so it cannot include any specifics. This string +is used for example as a header in HTML reports for all incidents of +this type, and in the IDE, if you open the Inspections UI, the various +issues are listed there using the brief descriptions. + +The **`explanation`** is a multi line, ideally multi-paragraph +explanation of what the problem is. In some cases, the problem is self +evident, as in the case of ”Unused declaration“, but in many cases, the +issue is more subtle and might require additional explanation, +particularly for what the developer should **do** to address the +problem. The explanation is included both in HTML reports and in the +IDE inspection results window. + +Note that even though we're using a raw string, and even though the +string is indented to be flush with the rest of the issue registration +for better readability, we don't need to call `trimIndent()` on +the raw string. Lint does that automatically. + +However, we do need to add line continuations -- those are the trailing +\'s at the end of the lines. + +Note also that we have a Markdown-like simple syntax, described in the +"TextFormat” section below. You can use asterisks for italics or double +asterisks for bold, you can use apostrophes for code font, and so on. +In terminal output this doesn't make a difference, but the IDE, +explanations, incident error messages, etc, are all formatted using +these styles. + +The **`category`** isn't super important; the main use is that category +names can be treated as id's when it comes to issue configuration; for +example, a user can turn off all internationalization issues, or run +lint against only the security related issues. The category is also +used for locating related issues in HTML reports. If none of the +built-in categories are appropriate you can also create your own. + +The **`severity`** property is very important. An issue can be either a +warning or an error. These are treated differently in the IDE (where +errors are red underlines and warnings are yellow highlights), and in +the build system (where errors can optionally break the build and +warnings do not). There are some other severities too; ”fatal“ is like +error except these checks are designated important enough (and have +very few false positives) such that we run them during release builds, +even if the user hasn't explicitly run a lint target. There's also +"informational” severity, which is only used in one or two places, and +finally the “ignore” severity. This is never the severity you register +for an issue, but it's part of the severities a developer can configure +for a particular issue, thereby turning off that particular check. + +You can also specify a **`moreInfo`** URL which will be included in the +issue explanation as a “More Info” link to open to read more details +about this issue or underlying problem. + +## TextFormat + +All error messages and issue metadata strings in lint are interpreted +using simple Markdown-like syntax: + +Raw text format | Renders To +-----------------------------|-------------------------- +This is a \`code symbol\` | This is a `code symbol` +This is `*italics*` | This is *italics* +This is `**bold**` | This is **bold** +http://, https:// | [](http://), [](https://) +`\*not italics*` | `\*not italics*` +\`\`\`language\n text\n\`\`\`| (preformatted text block) + [Supported markup in lint's markdown-like raw text format] + +This is useful when error messages and issue explanations are shown in +HTML reports generated by Lint, or in the IDE, where for example the +error message tooltips will use formatting. + +In the API, there is a `TextFormat` enum which encapsulates the +different text formats, and the above syntax is referred to as +`TextFormat.RAW`; it can be converted to `.TEXT` or `.HTML` for +example, which lint does when writing text reports to the console or +HTML reports to files respectively. As a lint check author you don't +need to know this (though you can for example with the unit testing +support decide which format you want to compare against in your +expected output), but the main point here is that your issue's brief +description, issue explanation, incident report messages etc, should +use the above “raw” syntax. Especially the first conversion; error +messages often refer to class names and method names, and these should +be surrounded by apostrophes. + +## Issue Implementation + +The last issue registration property is the **`implementation`**. This +is where we glue our metadata to our specific implementation of an +analyzer which can find instances of this issue. + +Normally, the `Implementation` provides two things: + +* The `.class` for our `Detector` which should be instantiated. In the + code sample above it was `SdCardDetector`. + +* The `Scope` that this issue's detector applies to. In the above + example it was `Scope.JAVA_FILE`, which means it will apply to Java + and Kotlin files. + +## Scopes + +The `Implementation` actually takes a **set** of scopes; we still refer +to this as a “scope”. Some lint checks want to analyze multiple types +of files. For example, the `StringFormatDetector` will analyze both the +resource files declaring the formatting strings across various locales, +as well as the Java and Kotlin files containing `String.format` calls +referencing the formatting strings. + +There are a number of pre-defined sets of scopes in the `Scope` +class. `Scope.JAVA_FILE_SCOPE` is the most common, which is a +singleton set containing exactly `Scope.JAVA_FILE`, but you +can always create your own, such as for example +``` + EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES) +``` + +When a lint issue requires multiple scopes, that means lint will +**only** run this detector if **all** the scopes are available in the +running tool. When lint runs a full batch run (such as a Gradle lint +target or a full “Inspect Code“ in the IDE), all scopes are available. + +However, when lint runs on the fly in the editor, it only has access to +the current file; it won't re-analyze *all* files in the project for +every few keystrokes. So in this case, the scope in the lint driver +only includes the current source file's type, and only lint checks +which specify a scope that is a subset would run. + +This is a common mistake for new lint check authors: the lint check +works just fine as a unit test, but they don't see working in the IDE +because the issue implementation requests multiple scopes, and **all** +have to be available. + +Often, a lint check looks at multiple source file types to work +correctly in all cases, but it can still identify *some* problems given +individual source files. In this case, the `Implementation` constructor +(which takes a vararg of scope sets) can be handed additional sets of +scopes, called ”analysis scopes“. If the current lint client's scope +matches or is a subset of any of the analysis scopes, then the check +will run after all. + +## Registering the Issue + +Once you've created your issue, you need to provide it from +an `IssueRegistry`. + +Here's an example `IssueRegistry`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +package com.example.lint.checks + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SdCardDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + // works with Studio 4.1 or later; see + // com.android.tools.lint.detector.api.Api / ApiKt + override val minApi: Int + get() = 8 + + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://com.example.lint.blah.blah/", + contact = "author@com.example.lint" + ) +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +On line 8, we're returning our issue. It's a list, so an +`IssueRegistry` can provide multiple issues. + +The **`api`** property should be written exactly like the way it +appears above in your own issue registry as well; this will record +which version of the lint API this issue registry was compiled against +(because this references a static final constant which will be copied +into the jar file instead of looked up dynamically when the jar is +loaded). + +The **`minApi`** property records the oldest lint API level this check +has been tested with. + +Both of these are used at issue loading time to make sure lint checks +are compatible, but in recent versions of lint (7.0) lint will more +aggressively try to load older detectors even if they have been +compiled against older APIs since there's a high likelihood that they +will work (it checks all the lint APIs in the bytecode and uses +reflection to verify that they're still there). + +The **`vendor`** property is new as of 7.0, and gives lint authors a +way to indicate where the lint check came from. When users use lint, +they're running hundreds and hundreds of checks, and sometimes it's not +clear who to contact with requests or bug reports. When a vendor has +been specified, lint will include this information in error output and +reports. + +The last step towards making the lint check available is to make +the `IssueRegistry` known via the service loader mechanism. + +Create a file named exactly +``` +src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +``` + +with the following contents (but where you substitute in your own +fully qualified class name for your issue registry): + +``` +com.example.lint.checks.SampleIssueRegistry +``` + +If you're not building your lint check using Gradle, you may not want +the `src/main/resources` prefix; the point is that your packaging of +the jar file should contain `META-INF/services/` at the root of the jar +file. + +## Implementing a Detector: Scanners + +We've finally come to the main task with writing a lint check: +implementing the **`Detector`**. + +Here's a trivial one: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class MyDetector : Detector() { + override fun run(context: Context) { + context.report(ISSUE, Location.create(context.file), + "I complain a lot") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This will just complain in every single file. Obviously, no real lint +detector does this; we want to do some analysis and **conditionally** +report incidents. + +In order to make it simpler to perform analysis, Lint has dedicated +support for analyzing various file types. The way this works is that +you register interest, and then various callbacks will be invoked. + +For example: + +* When implementing **`XmlScanner`**, in an XML element you can be + called back + - when any of a set of given tags are declared (`visitElement`) + - when any of a set of named attributes are declared + (`visitAttribute`) + - and you can perform your own document traversal via `visitDocument` + +* When implementing **`SourceCodeScanner`**, in Kotlin and Java files + you can be called back + - When a method of a given name is invoked (`getApplicableMethodNames` + and `visitMethodCall`) + - When a class of the given type is instantiated + (`getApplicableConstructorTypes` and `visitConstructor`) + - When a new class is declared which extends (possibly indirectly) + a given class or interface (`applicableSuperClasses` and + `visitClass`) + - When annotated elements are referenced or combined + (`applicableAnnotations` and `visitAnnotationUsage`) + - When any AST nodes of given types appear (`getApplicableUastTypes` + and `createUastHandler`) + +* When implementing a **`ClassScanner`**, in `.class` and `.jar` files + you can be called back + - when a method is invoked for a particular owner + (`getApplicableCallOwners` and `checkCall` + - when a given bytecode instruction occurs + (`getApplicableAsmNodeTypes` and `checkInstruction`) + - like with XmlScanner's `visitDocument`, you can perform your own + ASM bytecode iteration via `checkClass`. + +* There are various other scanners too, for example `GradleScanner` + which lets you visit `build.gradle` and `build.gradle.kts` DSL + closures, `BinaryFileScanner` which visits resource files such as + webp and png files, and `OtherFileScanner` which lets you visit + unknown files. + +!!! Note + Note that `Detector` already implements empty stub methods for all + of these interfaces, so if you for example implement + `SourceFileScanner` in your detector, you don't need to go and add + empty implementations for all the methods you aren't using. + +!!! Tip + None of Lint's APIs require you to call `super` when you override + methods; methods meant to be overridden are always empty so the + super-call is superfluous. + +## Detector Lifecycle + +Detector registration is done by detector class, not by detector +instance. Lint will instantiate detectors on your behalf. It will +instantiate the detector once per analysis, so you can stash state on +the detector in fields and accumulate information for analysis at the +end. + +There are some callbacks both before each individual file is analyzed +(`beforeCheckFile` and `afterCheckFile`), as well as before and after +analysis of all the modules (`beforeCheckRootProject` and +`afterCheckRootProject`). + +This is for example how the ”unused resources“ check works: we store +all the resource declarations and resource references we find in the +project as we process each file, and then in the +`afterCheckRootProject` method we analyze the resource graph and +compute any resource declarations that are not reachable in the +reference graph, and then we report each of these as unused. + +## Scanner Order + +Some lint checks involve multiple scanners. This is pretty common in +Android, where we want to cross check consistency between data in +resource files with the code usages. For example, the `String.format` +check makes sure that the arguments passed to `String.format` match the +formatting strings specified in all the translation XML files. + +Lint defines an exact order in which it processes scanners, and within +scanners, data. This makes it possible to write some detectors more +easily because you know that you'll encounter one type of data before +the other; you don't have to handle the opposite order. For example, in +our `String.format` example, we know that we'll always see the +formatting strings before we see the code with `String.format` calls, +so we can stash the formatting strings in a map, and when we process +the formatting calls in code, we can immediately issue reports; we +don't have to worry about encountering a formatting call for a +formatting string we haven't processed yet. + +Here's lint's defined order: + +1. Android Manifest +2. Android resources XML files (alphabetical by folder type, so for + example layouts are processed before value files like translations) +3. Kotlin and Java files +4. Bytecode (local `.class` files and library `.jar` files) +5. Gradle files +6. Other files +7. ProGuard files +8. Property Files + +Similarly, lint will always process libraries before the modules +that depend on them. + +!!! Tip + If you need to access something from later in the iteration order, + and it's not practical to store all the current data and instead + handle it when the later data is encountered, note that lint has + support for ”multi-pass analysis“: it can run multiple times over + the data. The way you invoke this is via + `context.driver.requestRepeat(this, …)`. This is actually how the + unused resource analysis works. Note however that this repeat is + only valid within the current module; you can't re-run the analysis + through the whole dependency graph. + +## Implementing a Detector: Services + +In addition to the scanners, lint provides a number of services +to make implementation simpler. These include + +* **`ConstantEvaluator`**: Performs evaluation of AST expressions, so + for example if we have the statements `x = 5; y = 2 * x`, the + constant evaluator can tell you that y is 10. This constant evaluator + can also be more permissive than a compiler's strict constant + evaluator; e.g. it can return concatenated strings where not all + parts are known, or it can use non-final initial values of fields. + This can help you find *possible* bugs instead of *certain* bugs. + +* **`TypeEvaluator`**: Attempts to provide the concrete type of an + expression. For example, for the Java statements `Object s = new + StringBuilder(); Object o = s`, the type evaluator can tell you that + the type of `o` at this point is really `StringBuilder`. + +* **`JavaEvaluator`**: Despite the unfortunate older name, this service + applies to both Kotlin and Java, and can for example provide + information about inheritance hierarchies, class lookup from fully + qualified names, etc. + +* **`DataFlowAnalyzer`**: Data flow analysis within a method. + +* For Android analysis, there are several other important services, + like the `ResourceRepository` and the `ResourceEvaluator`. + +* Finally, there are a number of utility methods; for example there is + an `editDistance` method used to find likely typos used by a number + of checks. + +## Scanner Example + +Let's create a `Detector` using one of the above scanners, +`XmlScanner`, which will look at all the XML files in the project and +if it encounters a `` tag it will report that `` should +be used instead: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.XmlScanner +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.XmlContext +import org.w3c.dom.Element + +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + val incident = Incident(context, ISSUE) + .message( "Use `` instead of ``") + .at(element) + context.report(incident)) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The above is using the new `Incident` API from Lint 7.0 and on; in +older versions you can use the following API, which still works in 7.0: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + context.report(ISSUE, context.getLocation(element), + "Use `` instead of ``") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second, older form, may seem simpler, but the new API allows a lot +more metadata to be attached to the report, such as an override +severity. You don't have to convert to the builder syntax to do this; +you could also have written the second form as + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +context.report(Incident(ISSUE, context.getLocation(element), + "Use `` instead of ``")) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Analyzing Kotlin and Java Code + +### UAST + +To analyze Kotlin and Java code, lint offers an abstract syntax tree, +or ”AST“, for the code. + +This AST is called ”UAST“, for ”Universal Abstract Syntax Tree“, which +represents multiple languages in the same way, hiding the language +specific details like whether there is a semicolon at the end of the +statements or whether the way an annotation class is declared is as +`@interface` or `annotation class`, and so on. + +This makes it possible to write a single analyzer which works +(”universally“) across all languages supported by UAST. And this is +very useful; most lint checks are doing something API or data-flow +specific, not something language specific. If however you do need to +implement something very language specific, see the next section, +"PSI”. + +In UAST, each element is called a **`UElement`**, and there are a +number of subclasses -- `UFile` for the compilation unit, `UClass` for +a class, `UMethod` for a method, `UExpression` for an expression, +`UIfExpression` for an `if`-expression, and so on. + +Here's a visualization of an AST in UAST for two equivalent programs +written in Kotlin and Java. These programs both result in the same +AST, shown on the right: a `UFile` compilation unit, containing +a `UClass` named `MyTest`, containing `UField` named s which has +an initializer setting the initial value to `hello`. + +************************************************************************ +* +* MyTest.kt: UAST: +* +---------------------------+ .-------. +* | package test.pkg | | UFile | +* | class MyTest { | '---+---' +* | private val s = “hello” | | +* | } | .------+------. +* +---------------------------+ | UClass MyTest | +* '------+------' +* MyTest.java: | +* +------------------------+ .---+----. +* | package test.pkg; | | UField s | +* | public class MyTest { | '+------+' +* | private String s = | / \ +* | “hello”; | / \ +* | } | / \ +* +------------------------+ / \ +* .-----------+. .--------+---------------. +* |UIdentifier s | | ULiteralExpression hello | +* '------------' '------------------------' +* +************************************************************************ + +!!! Tip + The name “UAST” is a bit misleading; it is not some sort of superset + of all possible syntax trees; instead, think of this as the “Java + view” of all code. So, for example, there isn’t a `UProperty` node + which represents Kotlin properties. Instead, the AST will look the + same as if the property had been implemented in Java: it will + contain a private field and a public getter and a public setter + (unless of course the Kotlin property specifies a private setter). + If you’ve written code in Kotlin and have tried to access that + Kotlin code from a Java file you will see the same thing -- the + “Java view” of Kotlin. The next section, “PSI“, will discuss how to + do more language specific analysis. + +### UAST Example + +Here's an example (from the built-in `AlarmDetector` for Android) which +shows all of the above in practice; this is a lint check which makes +sure that if anyone calls `AlarmManager.setRepeating`, the second +argument is at least 5,000 and the third argument is at least 60,000. + +Line 1 says we want to have line 3 called whenever lint comes across a +method to `setRepeating`. + +On lines 8-4 we make sure we're talking about the correct method on the +correct class with the correct signature. This uses the `JavaEvaluator` +to check that the called method is a member of the named class. This is +necessary because the callback would also be invoked if lint came +across a method call like `Unrelated.setRepeating`; the +`visitMethodCall` callback only matches by name, not receiver. + +On line 36 we use the `ConstantEvaluator` to compute the value of each +argument passed in. This will let this lint check not only handle cases +where you're specifying a specific value directly in the argument list, +but also for example referencing a constant from elsewhere. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +override fun getApplicableMethodNames(): List = listOf("setRepeating") + +override fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod +) { + val evaluator = context.evaluator + if (evaluator.isMemberInClass(method, "android.app.AlarmManager") && + evaluator.getParameterCount(method) == 4 + ) { + ensureAtLeast(context, node, 1, 5000L) + ensureAtLeast(context, node, 2, 60000L) + } +} + +private fun ensureAtLeast( + context: JavaContext, + node: UCallExpression, + parameter: Int, + min: Long +) { + val argument = node.valueArguments[parameter] + val value = getLongValue(context, argument) + if (value < min) { + val message = "Value will be forced up to $min as of Android 5.1; " + + "don't rely on this to be exact" + context.report(ISSUE, argument, context.getLocation(argument), message) + } +} + +private fun getLongValue( + context: JavaContext, + argument: UExpression +): Long { + val value = ConstantEvaluator.evaluate(context, argument) + if (value is Number) { + return value.toLong() + } + + return java.lang.Long.MAX_VALUE +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### Looking up UAST + +To write your detector's analysis, you need to know what the AST for +your code of interest looks like. Instead of trying to figure it out by +examining the elements under a debugger, a simple way to find out is to +”pretty print“ it, using the `UElement` extension method +**`asRecursiveLogString`**. + +For example, given the following unit test: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +lint().files( + kotlin("" + + "package test.pkg\n" + + "\n" + + "class MyTest {\n" + + " val s: String = \"hello\"\n" + + "}\n"), ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you evaluate `context.uastFile?.asRecursiveLogString()` from +one of the callbacks, it will print this: + +```text +UFile (package = test.pkg) + UClass (name = MyTest) + UField (name = s) + UAnnotation (fqName = org.jetbrains.annotations.NotNull) + ULiteralExpression (value = "hello") + UAnnotationMethod (name = getS) + UAnnotationMethod (name = MyTest) +``` + +(This also illustrates the earlier point about UAST representing the +Java view of the code; here the read-only public Kotlin property ”s“ is +represented by both a private field `s` and a public getter method, +`getS()`.) + +### Resolving + +When you have a method call, or a field reference, you may want to take +a look at the called method or field. This is called ”resolving“, and +UAST supports it directly; on a `UCallExpression` for example, call +`.resolve()`, which returns a `PsiMethod`, which is like a `UMethod`, +but may not represent a method we have source for (which for example +would be the case if you resolve a reference to the JDK or to a library +we do not have sources for). You can call `.toUElement()` on the +PSI element to try to convert it to UAST if source is available. + +!!! Warning + Resolving only works if lint has a correct classpath such that the + referenced method, field or class are actually present. If it is + not, resolve will return null, and various lint callbacks will not + be invoked. This is a common source of questions for lint checks + ”not working“; it frequently comes up in lint unit tests where a + test file will reference some API that isn't actually included in + the class path. The recommended approach for this is to declare + local stubs. See the [unit testing](unit-testing.md.html) chapter + for more details about this. + +### PSI + +PSI is short for ”Program Structure Interface“, and is IntelliJ's AST +abstraction used for all language modeling in the IDE. + +Note that there is a **different** PSI representation for each +language. Java and Kotlin have completely different PSI classes +involved. This means that writing a lint check using PSI would involve +writing a lot of logic twice; once for Java, and once for Kotlin. (And +the Kotlin PSI is a bit trickier to work with.) + +That's what UAST is for: there's a ”bridge“ from the Java PSI to UAST +and there's a bridge from the Kotlin PSI to UAST, and your lint check +just analyzes UAST. + +However, there are a few scenarios where we have to use PSI. + +The first, and most common one, is listed in the previous section on +resolving. UAST does not completely replace PSI; in fact, PSI leaks +through in part of the UAST API surface. For example, +`UMethod.resolve()` returns a `PsiMethod`. And more importantly, +`UMethod` **extends** `PsiMethod`. + +!!! Warning + For historical reasons, `PsiMethod` and other PSI classes contain + some unfortunate APIs that only work for Java, such as asking for + the method body. Because `UMethod` extends `PsiMethod`, you might be + tempted to call `getBody()` on it, but this will return null from + Kotlin. If your unit tests for your lint check only have test cases + written in Java, you may not realize that your check is doing the + wrong thing and won't work on Kotlin code. It should call `uastBody` + on the `UMethod` instead. Lint's special detector for lint detectors + looks for this and a few other scenarios (such as calling `parent` + instead of `uastParent`), so be sure to configure it for your + project. + +When you are dealing with ”signatures“ -- looking at classes and +class inheritance, methods, parameters and so on -- using PSI is +fine -- and unavoidable since UAST does not represent bytecode +(though in the future it potentially could, via a decompiler) +or any other JVM languages than Kotlin and Java. + +However, if you are looking at anything *inside* a method or class +or field initializer, you **must** use UAST. + +The **second** scenario where you may need to use PSI is where you have +to do something language specific which is not represented in UAST. For +example, if you are trying to look up the names or default values of a +parameter, or whether a given class is a companion object, then you'll +need to dip into Kotlin PSI. + +There is usually no need to look at Java PSI since UAST fully covers +it, unless you want to look at individual details like specific +whitespace between AST nodes, which is represented in PSI but not UAST. + +## Testing + +Writing unit tests for the lint check is important, and this is covered +in detail in the dedicated [unit testing](unit-testing.md.html) +chapter. + + diff --git a/docs/api-guide/changes.md.html b/docs/api-guide/changes.md.html new file mode 100644 index 00000000..6688813f --- /dev/null +++ b/docs/api-guide/changes.md.html @@ -0,0 +1,75 @@ +**Recent Changes** + +This chapter lists recent changes to lint that affect lint check +authors: new features, API and behavior changes, and so on. For +information about user visible changes to lint, see +[](../usage/changes.md.html). + +**7.0** + +* The API level has bumped to 10. + +* Partial analysis. Lint's architecture has changed to support better + scalability across large projects, where module results can be + cached, etc. See the api-guide's dedicated chapter for more details. + To opt in before it's turned on by default to test this on your full + Gradle projects rather than just the detector tests, add + + `android.experimental.useLintPartialAnalysis=true` + + to your `gradle.properties` file. If you want to debug your lint check + you may want to also set + + `android.experimental.runLintInProcess=true` + +* Issue registration now takes an optional `Vendor` property, where you + can specify information about which company or team provided this + lint check, which library it's associated with, contact information, + and so on. This will make it easier for users to figure out where to + send feedback or requests for 3rd party lint checks. + +* Bytecode verification: Instead of warning about 3rd party lint checks + being obsolete because they were not compiled against the latest Lint + API, lint now run its own bytecode verification against the lint jar + and will silently accept accept older (and newer!) lint checks if + they do not reference APIs that are not available. + +* Android Lint checks can now always access the resource repository for + random access to resources, instead of having to gather them in batch + mode. (Previously this was only available when lint checks were + running in the IDE.) + +* The lint unit testing library now provides a `TestMode` concept. You + can define setup and teardown methods, and lint will run unit tests + repeatedly for each test mode. There are a number of built-in test + modes already enabled; for example, all lint tests will run both in + global analysis mode and in partial analysis mode, and the results + compared to ensure they are the same. + +* Lint unit tests now include source contents for secondary locations + too. If the test fails, lint will retry without secondary source + locations and not report an error; this preserves backwards + compatibility. + +* There's a new `Incident` class which is used to hold information to + be reported to the user. Previously, there were a number of + overloaded methods to report issues, taking locations, error + messages, quick fixes, and so on. Each time we added another one we'd + have to add another overload. Now, you instead just report incidents. + This is critical to the new partial analysis architecture but is also + required if you for example want to override severities per incident + as described above. + +* Lint checks can now vary the severity on a per incident basis by + calling overrideSeverity on the incidents. This means that there is + no longer a need to create separate issues for flavors of the same + underlying problem with slightly different expectations around + warnings or errors. + +* There are additional modifier lookup methods for Kotlin modifiers + on `JavaEvaluator, like isReified(), isCompanion(), isTailRec(), and + so on. + +* API documentation is now available. + + diff --git a/docs/api-guide/example.md.html b/docs/api-guide/example.md.html new file mode 100644 index 00000000..a81452b7 --- /dev/null +++ b/docs/api-guide/example.md.html @@ -0,0 +1,362 @@ + + +# Example: Sample Lint Check GitHub Project + +The [](https://github.com/googlesamples/android-custom-lint-rules) +GitHub project provides a sample lint check which shows a working +skeleton. + +This chapter walks through that sample project and explains +what and why. + +## Project Layout + +Here's the project layout of the sample project: + +******************************************************************* +* * +* +----+ implementation +--------+ lintPublish +-------+ * +* |:app+----------------->|:library+-------------->|:checks| * +* +----+ +--------+ +-------+ * +* * +******************************************************************* + +We have an application module, `app`, which depends (via an +`implementation` dependency) on a `library`, and the library itself has +a `lintPublish` dependency on the `checks` project. + +## :checks + +The `checks` project is where the actual lint checks are implemented. +This project is a plain Kotlin or plain Java Gradle project: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +apply plugin: 'java-library' +apply plugin: 'kotlin' +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +!!! Tip + If you look at the sample project, you'll see a third plugin + applied: `apply plugin: 'com.android.lint'`. This pulls in the + standalone Lint Gradle plugin, which adds a lint target to this + Kotlin project. This means that you can run `./gradlew lint` on the + `:checks` project too. This is useful because lint ships with a + dozen lint checks that look for mistakes in lint detectors! This + includes warnings about using the wrong UAST methods, invalid id + formats, words in messages which look like code which should + probably be surrounded by apostrophes, etc. + +The Gradle file also declares the dependencies on lint APIs +that our detector needs: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +dependencies { + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.lint:lint-checks:$lintVersion" + testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second dependency is usually not necessary; you just need to depend +on the Lint API. However, the built-in checks define a lot of +additional infrastructure which it's sometimes convenient to depend on, +such as `ApiLookup` which lets you look up the required API level for a +given method, and so on. Don't add the dependency until you need it. + +## lintVersion? + +What is the `lintVersion` variable defined above? + +Here's the top level build.gradle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +buildscript { + ext { + kotlinVersion = '1.4.31' + + // Current lint target: Studio 4.2 / AGP 7 + //gradlePluginVersion = '4.2.0-beta06' + //lintVersion = '27.2.0-beta06' + + // Upcoming lint target: Arctic Fox / AGP 7 + gradlePluginVersion = '7.0.0-alpha10' + lintVersion = '30.0.0-alpha10' + } + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `$lintVersion` variable is defined on line 11. We don't technically +need to define the `$gradlePluginVersion` here or add it to the classpath on line 19, but that's done so that we can add the `lint` +plugin on the checks themselves, as well as for the other modules, +`:app` and `:library`, which do need it. + +When you build lint checks, you're compiling against the Lint APIs +distributed on maven.google.com (which is referenced via `google()` in +Gradle files). These follow the Gradle plugin version numbers. + +Therefore, you first pick which of lint's API you'd like to compile +against. You should use the latest available if possible. + +Once you know the Gradle plugin version number, say 4.2.0-beta06, you +can compute the lint version number by simply adding **23** to the +major version of the gradle plugin, and leave everything the same: + +**lintVersion = gradlePluginVersion + 23.0.0** + +For example, 7 + 23 = 30, so AGP version *7.something* corresponds to +Lint version *30.something*. As another example; as of this writing the +current stable version of AGP is 4.1.2, so the corresponding version of +the Lint API is 27.1.2. + +!!! Tip + Why this arbitrary numbering -- why can't lint just use the same + numbers? This is historical; lint (and various other sibling + libraries that lint depends on) was released earlier than the Gradle + plugin; it was up to version 22 or so. When we then shipped the + initial version of the Gradle plugin with Android Studio 1.0, we + wanted to start the numbering over from “1” for this brand new + artifact. However, some of the other libraries, like lint, couldn't + just start over at 1, so we continued incrementing their versions in + lockstep. Most users don't see this, but it's a wrinkle users of the + Lint API have to be aware of. + +## :library and :app + +The `library` project depends on the lint check project, and will +package the lint checks as part of its payload. The `app` project +then depends on the `library`, and has some code which triggers +the lint check. This is there to demonstrate how lint checks can +be published and consumed, and this is described in detail in the +[Publishing a Lint Check](publishing.md.html) chapter. + +## Lint Check Project Layout + +The lint checks source project is very simple + +``` +checks/build.gradle +checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt +checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt +checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt +``` + +First is the build file, which we've discussed above. + +## Service Registration + +Then there's the service registration file. Notice how this file is in +the source set `src/main/resources/`, which means that Gradle will +treat it as a resource and will package it into the output jar, in the +`META-INF/services` folder. This is using the service-provider loading facility in the JDK to register a service lint can look up. The +key is the fully qualified name for lint's `IssueRegistry` class. +And the **contents** of that file is a single line, the fully +qualified name of the issue registry: + +``` +$ cat checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry +com.example.lint.checks.SampleIssueRegistry +``` + +(The service loader mechanism is understood by IntelliJ, so it will +correctly update the service file contents if the issue registry is +renamed etc.) + +The service registration can contain more than one issue registry, +though there's usually no good reason for that, since a single issue +registry can provide multiple issues. + +## IssueRegistry + +Next we have the `IssueRegistry` linked from the service registration. +Lint will instantiate this class and ask it to provide a list of +issues. These are then merged with lint's other issues when lint +performs its analysis. + +In its simplest form we'd only need to have the following code +in that file: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +package com.example.lint.checks +import com.android.tools.lint.client.api.IssueRegistry +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SampleCodeDetector.ISSUE) +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +However, we're also providing some additional metadata about these lint +checks, such as the `Vendor`, which contains information about the +author and (optionally) contact address or bug tracker information, +displayed to users when an incident is found. + +We also provide some information about which version of lint's API the +check was compiled against, and the lowest version of the lint API that +this lint check has been tested with. (Note that the API versions are +not identical to the versions of lint itself; the idea and hope is that +the API may evolve at a slower pace than updates to lint delivering new +functionality). + +## Detector + +The `IssueRegistry` references the `SampleCodeDetector.ISSUE`, +so let's take a look at `SampleCodeDetector`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +class SampleCodeDetector : Detector(), UastScanner { + + // ... + + companion object { + /** + * Issue describing the problem and pointing to the detector + * implementation. + */ + @JvmField + val ISSUE: Issue = Issue.create( + // ID: used in @SuppressLint warnings etc + id = "ShortUniqueId", + // Title -- shown in the IDE's preference dialog, as category headers in the + // Analysis results window, etc + briefDescription = "Lint Mentions", + // Full explanation of the issue; you can use some markdown markup such as + // `monospace`, *italic*, and **bold**. + explanation = """ + This check highlights string literals in code which mentions the word `lint`. \ + Blah blah blah. + + Another paragraph here. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + SampleCodeDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `Issue` registration is pretty self-explanatory, and the details +about issue registration are covered in the [basics](basics.md.html) +chapter. The excessive comments here are there to explain the sample, +and there are usually no comments in issue registration code like this. + +Note how on line 29, the `Issue` registration names the `Detector` +class responsible for analyzing this issue: `SampleCodeDetector`. In +the above I deleted the body of that class; here it is now without the +issue registration at the end: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +package com.example.lint.checks + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.UastScanner +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.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UElement +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.evaluateString + +class SampleCodeDetector : Detector(), UastScanner { + override fun getApplicableUastTypes(): List> { + return listOf(ULiteralExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitLiteralExpression(node: ULiteralExpression) { + val string = node.evaluateString() ?: return + if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) { + context.report( + ISSUE, node, context.getLocation(node), + "This code mentions `lint`: **Congratulations**" + ) + } + } + } + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This lint check is very simple; for Kotlin and Java files, it visits +all the literal strings, and if the string contains the word “lint”, +then it issues a warning. + +This is using a very general mechanism of AST analysis; specifying the +relevant node types (literal expressions, on line 18) and visiting them +on line 23. Lint has a large number of convenience APIs for doing +higher level things, such as “call this callback when somebody extends +this class”, or “when somebody calls a method named ”foo“, and so on. +Explore the `SourceCodeScanner` and other `Detector` interfaces to see +what's possible. We'll hopefully also add more dedicated documentation +for this. + +## Detector Test + +Last but not least, let's not forget the unit test: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +package com.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test + +class SampleCodeDetectorTest { + @Test + fun testBasic() { + lint().files( + java( + """ + package test.pkg; + public class TestClass1 { + // In a comment, mentioning "lint" has no effect + private static String s1 = "Ignore non-word usages: linting"; + private static String s2 = "Let's say it: lint"; + } + """ + ).indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId] + private static String s2 = "Let's say it: lint"; + ∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼ + 0 errors, 1 warnings + """ + ) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As you can see, writing a lint unit test is very simple, because +lint ships with a dedicated testing library; this is what the + +``` + testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +``` + +dependency in build.gradle pulled in. + +Unit testing lint checks is covered in depth in the +[unit testing chapter](unit-testing.md.html), so we'll cut the +explanation of the above test short here. + + diff --git a/docs/api-guide/faq.md.html b/docs/api-guide/faq.md.html new file mode 100644 index 00000000..df679609 --- /dev/null +++ b/docs/api-guide/faq.md.html @@ -0,0 +1,308 @@ + + +# Frequently Asked Questions + +This chapter contains a random collection of questions people +have asked in the past. + +### My detector callbacks aren't invoked + +If you've for example implemented the Detector callback for visiting +method calls, `visitMethodCall`, notice how the third parameter is a +`PsiMethod`, and that it is not nullable: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + open fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod + ) { +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This passes in the method that has been called. When lint is visiting +the AST, it will resolve calls, and if the called method cannot be +resolved, the callback won't be called. + +This happens when the classpath that lint has been configured with does +not contain everything needed. When lint is running from Gradle, this +shouldn't happen; the build system should have a complete classpath and +pass it to Lint (or the build wouldn't have succeeded in the first +place). + +This usually comes up in unit tests for lint, where you've added a test +case which is referencing some API for some library, but the library +itself isn't part of the test. The solution for this is to create stubs +for the part of the API you care about. This is discussed in more +detail in the [unit testing](unit-testing.md.html) chapter. + +### My lint check works from the unit test but not in the IDE + +There are several things to check if you have a lint check which +works correctly from your unit test but not in the IDE. + +1. First check that the lint jar is packaged correctly; use `jar tvf + lint.jar` to look at the jar file to make sure it contains the + service loader registration of your issue registry, and `javap + -classpath lint.jar com.example.YourIssueRegistry` to inspect your + issue registry. + +2. If that's correct, the next thing to check is that lint is actually + loading your issue registry. First look in the IDE log (from the + Help menu) to make sure there aren't log messages from lint + explaining why it can't load the registry, for example because it + does not specify a valid applicable API range. + +3. If there's no relevant warning in the log, try setting the + `$ANDROID_LINT_JARS` environment variable to point directly to your + lint jar file and restart Studio to make sure that that works. + +4. Next, try running **Analyze | Inspect Code...**. This runs lint on + the whole project. If that works, then the issue is that your lint + check isn't eligible to run “on the fly”; the reason for this is + that your implementation scope registers more than one scope, which + says that your lint check can only run if lint gets to look at both + types of files, and in the editor, only the current file is analyzed + by lint. However, you can still make the check work on the fly by + specifying additional analysis scopes; see the API guide for more + information about this. + +### `visitAnnotationUsage` isn't called for annotations + +If you want to just visit any annotation declarations (e.g. `@Foo` on +method `foo`), don't use the `applicableAnnotations` and +`visitAnnotationUsage` machinery. The purpose of that facility is to +look at *elements* that are being combined with annotated elements, +such as a method call to a method whose return value has been +annotated, or an argument to a method a method parameter that has been +annotated, or assigning an assigned value to an annotated variable, etc. + +If you just want to look at annotations, use `getApplicableUastTypes` +with `UAnnotation::class.java`, and a `UElementHandler` which overrides +`visitAnnotation`. + +### How do I check if a UAST or PSI element is for Java or Kotlin? + +To check whether an element is in Java or Kotlin, call one +of the package level methods in the detector API (and from +Java, you can access them as utility methods on the “Lint” +class) : + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +package com.android.tools.lint.detector.api + +/** Returns true if the given element is written in Java. */ +fun isJava(element: PsiElement?): Boolean { /* ... */ } + +/** Returns true if the given language is Kotlin. */ +fun isKotlin(language: Language?): Boolean { /* ... */ } + +/** Returns true if the given language is Java. */ +fun isJava(language: Language?): Boolean { /* ... */ } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have a `UElement` and need a `PsiElement` for the above method, +see the next question. + +### What if I need a `PsiElement` and I have a `UElement` ? + +If you have a `UElement`, you can get the underlying source PSI element +by calling `element.sourcePsi`. + +### How do I get the `UMethod` for a `PsiMethod` ? + +Call `psiMethod.toUElementOfType()`. Note that this may return +null if UAST cannot find valid Java or Kotlin source code for the +method. + +For `PsiField` and `PsiClass` instances use the equivalent +`toUElementOfType` type arguments. + +### How do get a `JavaEvaluator` ? + +The `Context` passed into most of the `Detector` callback methods +relevant to Kotlin and Java analysis is of type `JavaContext`, and it +has a public `evaluator` property which provides a `JavaEvaluator` you +can use in your analysis. + +If you need one outside of that scenario (this is not common) you can +construct one directly by instantiating a `DefaultJavaEvaluator`; the +constructor parameters are nullable, and are only needed for a couple +of operations on the evaluator. + +### How do I check whether an element is internal? + +First get a `JavaEvaluator` as explained above, then call +this evaluator method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +open fun isInternal(owner: PsiModifierListOwner?): Boolean { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +(Note that a `PsiModifierListOwner` is an interface which includes +`PsiMethod`, `PsiClass`, `PsiField`, `PsiMember`, `PsiVariable`, etc.) + +### Is element inline, sealed, operator, infix, suspend, data? + +Get the `JavaEvaluator` as explained above, and then call one of these +evaluator method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +open fun isData(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isInline(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isLateInit(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isSealed(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isOperator(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isInfix(owner: PsiModifierListOwner?): Boolean { /* ... */ +open fun isSuspend(owner: PsiModifierListOwner?): Boolean { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up a class if I have its fully qualified name? + +Get the `JavaEvaluator` as explained above, then call +`evaluator.findClass(qualifiedName: String)`. Note that the result is +nullable. + +### How do I look up a class if I have a PsiType? + +Get the `JavaEvaluator` as explained above, then call +`evaluator.getTypeClass`. To go from a class to its type, +use `getClassType`. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + abstract fun getClassType(psiClass: PsiClass?): PsiClassType? + abstract fun getTypeClass(psiType: PsiType?): PsiClass? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up hierarhcy annotations for an element? + +You can directly look up annotations via the modified list +of PsiElement or the annotations for a `UAnnotated` element, +but if you want to search the inheritance hierarchy for +annotations (e.g. if a method is overriding another, get +any annotations specified on super implementations), use +one of these two evaluator methods: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + abstract fun getAllAnnotations( + owner: UAnnotated, + inHierarchy: Boolean + ): List + + abstract fun getAllAnnotations( + owner: PsiModifierListOwner, + inHierarchy: Boolean + ): Array +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I look up if a class is a subclass of another? + +To see if a method is a direct member of a particular +named class, use the following method in `JavaEvaluator`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +fun isMemberInClass(member: PsiMember?, className: String): Boolean { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To see if a method is a member in any *subclass* of a named class, use + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + open fun isMemberInSubClassOf( + member: PsiMember, + className: String, + strict: Boolean = false + ): Boolean { /* ... */ } + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, use `strict = true` if you don't want to include members in the +named class itself as a match. + +To see if a class extends another or implements an interface, use one +of these methods. Again, `strict` controls whether we include the super +class or super interface itself as a match. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + abstract fun extendsClass( + cls: PsiClass?, + className: String, + strict: Boolean = false + ): Boolean + + abstract fun implementsInterface( + cls: PsiClass, + interfaceName: String, + strict: Boolean = false + ): Boolean +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +### How do I know which parameter a call argument corresponds to? + +In Java, matching up the arguments in a call with the parameters in the +called method is easy: the first argument corresponds to the first +parameter, the second argument corresponds to the second parameter and +so on. If there are more arguments than parameters, the last arguments +are all vararg arguments to the last parameter. + +In Kotlin, it's much more complicated. With named parameters, but +arguments can appear in any order, and with default parameters, only +some of them may be specified. And if it's an extension method, the +first argument passed to a `PsiMethod` is actually the instance itself. + +Lint has a utility method to help with this on the `JavaEvaluator`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + open fun computeArgumentMapping( + call: UCallExpression, + method: PsiMethod + ): Map { /* ... */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This returns a map from UAST expressions (each argument to a UAST call +is a `UExpression`, and these are the `valueArguments` property on the +`UCallExpression`) to each corresponding `PsiParameter` on the +`PsiMethod` that the method calls. + +### How can my lint checks target two different versions of lint? + +If you need to ship different versions of your lint checks to target +different versions of lint (because perhaps you need to work both with +an older version of lint, and a newer version that has a different +API), the way to do this (as of Lint 7.0) is to use the `maxApi` +property on the `IssueRegistry`. In the service loader registration +(`META-INF/services`), register *two* issue registries; one for each +implementation, and mark the older one with the right `minApi` to +`maxApi` range, and the newer one with `minApi` following the previous +registry's `maxApi`. (Both `minApi` and `maxApi` are inclusive). When +lint loads the issue registries it will ignore registries with a range +outside of the current API level. + +### How do I check out the current lint source code? + +```shell +$ git clone --branch=mirror-goog-studio-master-dev --single-branch \ + https://android.googlesource.com/platform/tools/base +Cloning into 'base'... +remote: Total 648820 (delta 325442), reused 635137 (delta 325442) +Receiving objects: 100% (648820/648820), 1.26 GiB | 15.52 MiB/s, done. +Resolving deltas: 100% (325442/325442), done. +Updating files: 100% (14416/14416), done. + +$ du -sh base +1.8G base +$ cd base/lint +$ ls +.editorconfig BUILD build.gradle libs/ +.gitignore MODULE_LICENSE_APACHE2 cli/ +$ ls libs/ +intellij-core/ kotlin-compiler/ lint-api/ lint-checks/ lint-gradle/ lint-model/ lint-tests/ uast/ +``` + +### Where do I find examples of lint checks? + +The built-in lint checks are a good source. Check out the source code +as shown above and look in +`lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/` or +browse sources online: +[](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/) + + diff --git a/docs/api-guide/nested-syntax-highlighting.png b/docs/api-guide/nested-syntax-highlighting.png new file mode 100644 index 0000000000000000000000000000000000000000..d9238676d3cabff5020f14c5ce60bb47e171c4f7 GIT binary patch literal 134730 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

{cjgI(#4eBcm2Pi&Q-PWVam|S1Ts< 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 z�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 HYq$RgSQ#lA literal 0 HcmV?d00001 diff --git a/docs/api-guide/partial-analysis.md.html b/docs/api-guide/partial-analysis.md.html new file mode 100644 index 00000000..c70fe66d --- /dev/null +++ b/docs/api-guide/partial-analysis.md.html @@ -0,0 +1,633 @@ + + +# Partial Analysis + +## About + +This chapter describes Lint's “partial analysis”; its architecture and +APIs for allowing lint results to be cached. + +This focuses on how to write or update existing lint checks such that +they work correctly under partial analysis. For other details about +partial analysis, such as the client side implemented by the build +system, see the lint internal docs folder. + +!!! Note + Note that while lint has this architecture, and all lint detectors + must support it, the checks may not run in partial analysis mode; + they may instead run in “global analysis mode”, which is how lint + has worked up until this point. + + This is because coordinating partial results and merging is + performed by the `LintClient`; e.g. in the IDE, there's no good + reason to do all this extra work (because all sources are generally + available, including “downstream” module info like the + `minSdkVersion`). + + Right now, only the Android Gradle Plugin turns on partial analysis + mode. But that's a very important client, since it's usually how + lint checks are performed on continuous integration servers to + validate code reviews. + +## The Problem + +Many lint checks require “global” analysis. For example you can't +determine whether a particular string defined in a library module is +unused unless you look at all modules transitively consuming this +library as well. + +However, many developers run lint as part of their continuous +integration. Particularly in large projects, analyzing all modules for +every check-in is too costly. + +This chapter describes lint's architecture for handling this, such +that module results can be cached. + +## Overview + +Briefly stated, lint's architecture for this is “map reduce”: lint now +has two separate phases, analyze and report (map and reduce +respectively): + +* **analyze** - where lint analyzes source code of a single module in + isolation, and stores some intermediate partial results (map) + +* **report** - where lint reads in the previously stored module results, + and performs some post-processing on this data to generate an actual + lint report. + +Crucially, the individual module results can be cached, such that if +nothing has changed in a module, the module results continue to be +valid (unless signatures have changed in libraries it depends on.) + +Making this work requires some modifications to any `Detector` which +considers data from outside the current module. However, there are some +very common scenarios that lint has special support for to make this +easier. + +Detectors fit into one of the following categories (and these +categories will be explained in subsequent sessions) : + +1. Local analysis which doesn't depend on anything else. For example, a + lint check check which flags typos can report incidents immediately. + Lint calls these “definite incidents”. + +2. Local analysis which depends on a few, common conditions. For + example, in Android, a check may only apply if the `minSdkVersion < + 21`. Lint has special support for this; you basically report an + incident and attach a “constraint” to it. Lint calls these, and + incidents reported as part of #3 below, as “provisional incidents”. + +3. Analysis which depends on some conditions of downstream modules that + are not part of the built-in constraints. For example, a lint check + may only apply if the consuming module depends on a certain version + of a networking library. In this case, the detector will report the + incident and attach a map to it, with whatever data it needs to + consult later to decide if the incident actually should be reported. + When the detector reports incidents this way, it has to also + override a callback method. Lint will record these incidents, and + during reporting, call the detector and pass it back its data map + and provisional incidents such that it can decide whether the + incidents should indeed be reported. + +4. Last, and least, there are some scenarios where you cannot compute + provisional incidents up front and filter them later (or doing so + would be very costly). For example, unused resources fit into this + category. We don't want to report every single resource declaration + as unused and then filter later. Instead, we compute the resource + usage graph within the module analysis. And in the reporting task, + we then load all the partial usage graphs, and merge them together + and walk the graph to report all the unused resources. To support + this, lint provides a map per module for detectors to put their data + into, and you can put maps into the map to model structured data. + Lint will persist these, and in the reporting task the lint + detectors will be passed their data to do their post-processing and + reporting based on their data. + +These are listed in increasing order of effort, and thankfully, they're +also listed in order of frequency. For lint's built-in checks (~385), + +* 89% needed no work at all. +* 6% were updated to report incidents with constraints +* 4% were updated to report incidents with data for later filtering +* 1% were updated to perform map recording and later reduce filtering + +## Does my Detector Need Work? + +At this point you're probably wondering whether your checks are in the +89% category where you don't need to do anything, or in the remaining +11%. How do you know? + +Lint has several built-in mechanisms to try to catch problems. There +are a few scenarios it cannot detect, and these are described below, +but for the vast majority, simply running your unit tests (which are +comprehensive, right?) should create unit test failures if your +detector is doing something it shouldn't. + +### Catching Mistakes: Blocking Access to Main Project + +In Android checks, it's very common to try to access the main (“app”) +project, to see what the real `minSdkVersion` is, since the app +`minSdkVersion` can be higher than the one in the library. For the +`targetSdkVersion` it's even more important, since the library +`targetSdkVersion` has no meaningful relationship to the app one. + +When you run lint unit tests, as of 7.0, it will now run your tests +twice -- once with global analysis (the previous behavior), and once +with partial analysis. When lint is running in partial analysis, a +number of calls, such as looking up the main project, or consulting the +merged manifest, is not allowed during the analysis phase. Attempting +to do so will generate an error: + +```none + SdCardTest.java: Error: The lint detector + com.android.tools.lint.checks.SdCardDetector + called context.getMainProject() during module analysis. + + This does not work correctly when running in Lint Unit Tests. + + In particular, there may be false positives or false negatives because + the lint check may be using the minSdkVersion or manifest information + from the library instead of any consuming app module. + + Contact the vendor of the lint issue to get it fixed/updated (if + known, listed below), and in the meantime you can try to work around + this by disabling the following issues: + + "SdCardPath" + + Issue Vendor: + Vendor: Android Open Source Project + Contact: https://groups.google.com/g/lint-dev + Feedback: https://issuetracker.google.com/issues/new?component=192708 + + Call stack: Context.getMainProject(Context.kt:117)←SdCardDetector$createUastHandler$1.visitLiteralExpression(SdCardDetector.kt:66) + ←UElementVisitor$DispatchPsiVisitor.visitLiteralExpression(UElementVisitor.kt:791) + ←ULiteralExpression$DefaultImpls.accept(ULiteralExpression.kt:38) + ←JavaULiteralExpression.accept(JavaULiteralExpression.kt:24)←UVariableKt.visitContents(UVariable.kt:64) + ←UVariableKt.access$visitContents(UVariable.kt:1)←UField$DefaultImpls.accept(UVariable.kt:92) + ... +``` + +Specific examples of information many lint checks look at in this +category: +* `minSdkVersion` and `targetSdkVersion` +* The merged manifest +* The resource repository +* Whether the main module is an Android project + +### Catching Mistakes: Simulated App Module + +Lint will also modify the unit test when running the test in partial +analysis mode. In particular, let's say your test has a manifest which +sets `minSdkVersion` to 21. + +Lint will instead run the analysis task on a modified test project +where the `minSdkVersion` is set to 1, and then run the reporting task +where `minSdkVersion` is set back to 21. This ensures that lint checks +will correctly use the `minSdkVersion` from the main project, not the +library. + +### Catching Mistakes: Diffing Results + +Lint will also diff the report output from running the same unit tests +both in global analysis mode and in partial analysis mode. We expect +the results to always be identical, and in some cases if the module +analysis is not written correctly, they're not. + +### Catching Mistakes: Remaining Issues + +The above three mechanisms will catch most problems related to partial +analysis. However, there are a few remaining scenarios to be aware of: + +* Resolving into library source code. If you have a Kotlin or Java + function call AST node (`UCallExpression`) you can call `resolve()` + on it to find the called `PsiMethod`, and from there you can look at + its source code, to make some decisions. + + For example, lint's API Check uses this to see if a given method is a + version-check utility (“`SDK_INT > 21`?”); it resolves the method + call in `if (isOnLollipop()) { ... }` and looks at its method body to + see if the return value corresponds to a proper `SDK_INT` check. + + In partial analysis mode, you cannot look at source files from + libraries you depend on; they will only be provided in binary + (bytecode inside a jar file) form. + + This means that instead, you need to aggregate data along the way. + For example, the way lint handles the version check method lookup is + to look for SDK_INT comparisons, and if found, stores a reference to + the method in ther partial results map which it can later consult + from downstream modules. + +* Multiple passes across the modules (lint has a way to request + multiple passes; this was used by a few lint checks like the unused + resource detector; the multiple passes now only apply to the local + module) + +In order to test for correct operation of your check, you should add +your own individual unit test for a multi-module project. + +Lint's unit test infrastructure makes this easy; just use relative +paths in the test file descriptions. + +For example, if you have the following unit test declaration: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + lint().files( + manifest().minSdk(15), + manifest().to("../app/AndroidManifest.xml").minSdk(21), + xml( + "res/layout/linear.xml", + "" + ... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second `manifest()` call here on line 3 does all the heavy lifting: +the fact that you're referencing `../app` means it will create another +module named “app”, and it will add a dependency from that module on +this one. It will also mark the current module as a library. This is +based on the name patterns; if you for example reference say `../lib1`, +it will assume the current module is an app module and the dependency +will go from here to the library. + +Finally, to test a multi-module setup where the code in the other +module is only available as binary, lint has a new special test file +type. The `CompiledSourceFile` can be constructed via either +`compiled()`, if you want to make both the source code and the class +file available in the project, or `bytecode()` if you want to only +provide the bytecode. In both cases you include the source code in the +test file declaration, and the first time you run your test it will try +to run compilation and emit the extra base64 string to include the test +file. By having the sources included for the binary it's easy to +regenerate bytecode tests later (this was an issue with some of lint's +older unit tests; we recently decompiled them and created new test +files using this mechanism to make the code more maintainable. + +Lint's partial analysis testing support will automatically only use +binaries for the dependencies (even if using `CompiledSourceFile` with +sources). + +!!! Note + Lint's testing infrastructure may try to automate this testing at + some point; e.g. by looking at the error locations from a global + analysis, it can then create a new project where only the source + file with the warnings is provided as source, and all the other test + files are placed in a separate module, and then represented only as + binaries (through a lint AST to PsiCompiled pretty printer.) + +## Incidents + +In the past, you would typically report problems like this: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At some point, we added support for quickfixes, so the +report method took an additional parameter, line 6: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers + context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image", + fix().set().todo(ANDROID_URI, ATTR_CONTENT_DESCRIPTION).build() +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we need to attach various additional data (like constraints +and maps), we don't really want to just add more parameters. + +Instead, this tuple of data about a particular occurrence of a problem +is called an “incident”, and there is a new `Incident` class which +represents it. To report an incident you simply call +`context.report(incident)`. There are several ways to create these +incidents. The easiest is to simply edit your existing call above by +adding `Incident(` (or from Java, `new Incident(`) inside the +`context.report` block like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report(Incident( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + )) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +and then reformatting the source code: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report( + Incident( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + ) +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Incident` has a number of overloaded constructors to make it easy to +construct it from existing report calls. + +There are other ways to construct it too, for example like the +following: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + Incident(context) + .issue(ISSUE) + .scope(node) + .location(context.getLocation(node)) + .message("Do not hardcode \"/sdcard/\"").report() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +That are additional methods you can fall too, like `fix()`, and +conveniently, `at()` which specifies not only the scope node but +automatically computes and records the location of that scope node too, +such that the following is equivalent: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + Incident(context) + .issue(ISSUE) + .at(node) + .message("Do not hardcode \"/sdcard/\"").report() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +So step one to partial analysis is to convert your code to report +incidents instead of the passing in all the individual properties of an +incident. Note that for backwards compatibility, if your check doesn't +need any work for partial analysis, you can keep calling the older +report methods; they will be redirected to an `Incident` call +internally, but since you don't need to attach data you don't have to +make any changes + +## Constraints + +If your check needs to be conditional, perhaps on the `minSdkVersion`, +you need to attach a “constraint” to your report call. + +All the constraints are built in; there isn't a way to implement your +own. For custom logic, see the next section: LintMaps. + +Here are the current constraints, though this list may grow over time: + +* minSdkAtLeast(Int) +* minSdkLessThan(Int) +* targetSdkAtLeast(Int) +* targetSdkLessThan(Int) +* isLibraryProject() +* isAndroidProject() +* notLibraryProject() +* notAndroidProject() + +These are package-level functions, though from Java you can access them +from the `Constraints` class. + +Recording an incident with a constraint is easy; first construct the +`Incident` as before, and then report them via +`context.report(incident, constraint)`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~java + String message = + "One or more images in this project can be converted to " + + "the WebP format which typically results in smaller file sizes, " + + "even for lossless conversion"; + Incident incident = new Incident(WEBP_ELIGIBLE, location, message); + context.report(incident, minSdkAtLeast(18)); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, note that you can combine constraints; there are both “and” +and “or” operators defined for the `Constraint` class. so the following +is valid: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + val constraint = targetSdkAtLeast(23) and notLibraryProject() + context.report(incident, constraint) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +That's all you have to do. Lint will record this provisional incident, +and when it is performing reporting, it will evaluate these constraints +on its own and only report incidents that meet the constraint. + +## Incident LintMaps + +In some cases, you cannot use one of the built-in constraints; you have +to do your own “filtering” from the reporting task, where you have +access to the main module. + +In that case, you call `context.report(incident, map)` instead. + +Like `Incident`, `LintMap` is a new data holder class in lint which +makes it convenient to pass around (and more importantly, persist) +data. All the set methods return the map itself, so you can easily +chain property calls. + +Here's an example: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + context.report( + incident, + map() + .put(KEY_OVERRIDES, overrides) + .put(KEY_IMPLICIT, implicitlyExportedPreS) + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here, `map()` is a method defined by `Detector` to create a new +`LintMap`, similar to how `fix()` constructs a new `LintFix`. + +Note however that when reporting data, you need to do the post +processing yourself. To do this, you need to override this method: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Filter which looks at incidents previously reported via + * [Context.report] with a [LintMap], and returns false if the issue + * does not apply in the current reporting project context, or true + * if the issue should be reported. For issues that are accepted, + * the detector is also allowed to mutate the issue, such as + * customizing the error message further. + */ + open fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For example, for the above report call, the corresponding +implementation of `filterIncident` looks like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { + if (context.mainProject.targetSdk < 19) return true + if (map.getBoolean(KEY_IMPLICIT, false) == true && context.mainProject.targetSdk >= 31) return true + return map.getBoolean(KEY_OVERRIDES, false) == false + } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Note also that you are allowed to modify incidents here before +reporting them. The most common reason scenario for this is changing +the incident message, perhaps to reflect data not known at module +analysis time. For example, lint's API check creates messages like this: + +*Error: Cast from AudioFormat to Parcelable requires API level 24 (current min is 21)* + +At module analysis time when the incident was created, the minSdk being +21 was not known (and in fact can vary if this library is consumed by +many different app modules!) + +!!! WARNING + You must store state in the lint map; don't try to store it in the + detector itself as instance state. That won't work because the + detector instance that `filterInstance` is called on is not the same + instance as the one which originally reported it. If you think about + it, that makes sense; when module results are cached, the same + reported data can be used over and over again for repeated builds, + each time for new detector instances in the reporting task. + +## Module LintMaps + +The last (and most involved) scenario for partial analysis is one where +you cannot just create incidents and filter or customize them later. + +The most complicated example of this is lint's built-in +UnusedResourceDetector, which locates unused resources. This “requires” +global analysis, since we want to include all resources in the entire +project. We also cannot just store lists of “resources declared” and +"resources referenced“ since we really want to treat this as a graph. +For example if `@layout/main` is including `@drawable/icon`, then a +naive approach would see the icon as referenced (by main) and therefore +mark it as not unused. But what we want is that if the icon is **only** +referenced from main, and if main is unused, then so is the icon. + +To handle this, we model the resources as a graph, with edges +representing references. + +When analyzing individual modules, we create the resource graph for +just that model, and we store that in the results. That means we store +it in the module's `LintMap`. This is a map for the whole module +maintained by lint, so you can access it repeatedly and add to it. +(This is also where lint's API check stores the `SDK_INT` comparison +functions as described earlier in this chapter). + +The unused resource detector creates a persistence string for the +graph, and records that in the map. + +Then, during reporting, it is given access to *all* the lint maps for +all the modules that the reporting module depends on, including itself. +It then merges all the graphs into a single reference graph. + +For example, let's say in module 1 we have layout A which includes +drawables B and D, and B in turn depends on color C. We get a resource +graph like the following: + +********************************************** +* * +* .-. .-. .-. * +* | A +----->| B +----->| C | * +* '+' '-' '-' * +* | * +* | * +* | .-. * +* +--->| D | * +* '-' * +* * +********************************************** + +Then in another module, we have the following resource reference graph: + +********************************************** +* * +* .-. .-. .-. * +* | E +----->| B +----->| D | * +* '-' '-' '-' * +* * +********************************************** + +In the reporting task, we merge the two graphs like the following: + +********************************************** +* * +* .-. * +* | E | * +* '+' * +* | * +* v * +* .-. .-. .-. * +* | A +----->| B +----->| C | * +* '+' '+' '-' * +* | | * +* | v * +* | .-. * +* +------->| D | * +* '-' * +* * +********************************************** + +Once that's done, it can proceed precisely as before: analyze the graph +and report all the resources that are not reachable from the reference +roots (e.g. manifest and used code). + +The way this works in code is that you report data into the module by +first looking up the module data map, by calling this method on the +`Context`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Returns a [PartialResult] where state can be stored for later + * analysis. This is a more general mechanism for reporting + * provisional issues when you need to collect a lot of data and do + * some post processing before figuring out what to report and you + * can't enumerate out specific [Incident] occurrences up front. + * + * Note that in this case, the lint infrastructure will not + * automatically look up the error location (since there isn't one + * yet) to see if the issue has been suppressed (via annotations, + * lint.xml and other mechanisms), so you should do this + * yourself, via the various [LintDriver.isSuppressed] methods. + */ + fun getPartialResults(issue: Issue): PartialResult { ... } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Then you put whatever data you want, such as the resource usage model +encoded as a string. + +!!! + Note that you don't have to worry about clashes in key names; each + issue (and therefore detector) is given its own map. + +And then your detector should also override the following method, where +you can walk through the map contents, compute incidents and report +them: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + /** + * Callback to detectors that add partial results (by adding entries + * to the map returned by [LintClient.getPartialResults]). This is + * where the data should be analyzed and merged and results reported + * (via [Context.report]) to lint. + */ + open fun checkPartialResults(context: Context, partialResults: PartialResult) { ... } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Optimizations + +Most lint checks run on the fly in the IDE editor as well. In some +cases, if all the map computations are expensive, you can check whether +partial analysis is in effect, and if not, just directly access (for +example) the main project. + +Do this by calling `isGlobalAnalysis()`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin + if (context.isGlobalAnalysis()) { + // shortcut + } else { + // partial analysis code path + } +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + diff --git a/docs/api-guide/publishing.md.html b/docs/api-guide/publishing.md.html new file mode 100644 index 00000000..04d64328 --- /dev/null +++ b/docs/api-guide/publishing.md.html @@ -0,0 +1,114 @@ + + +# Publishing a Lint Check + +Lint will look for jar files with a service registry key for issue +registries. + +You can manually point it to your custom lint checks jar files by using +the environment variable `ANDROID_LINT_JARS`: + +```shell +$ export ANDROID_LINT_JARS=/path/to/first.jar:/path/to/second.jar +``` +(On Windows, use `;` instead of `:` as the path separator) + +However, that is only intended for development and as a workaround for +build systems that do not have direct support for lint or embedded lint +libraries, such as the internal Google build system. + +## Android + +### AAR Support + +Android libraries are shipped as `.aar` files instead of `.jar` files. +This means that they can carry more than just the code payload. Under +the hood, `.aar` files are just zip files which contain many other +nested files, including api and implementation jars, resources, +proguard/r8 rules, and yes, lint jars. + +For example, if we look at the contents of the timber logging library's +AAR file, we can see the lint.jar with several lint checks within as +part of the payload: + +```shell +$ jar tvf ~/.gradle/caches/.../jakewharton.timber/timber/4.5.1/?/timber-4.5.1.aar + 216 Fri Jan 20 14:45:28 PST 2017 AndroidManifest.xml + 8533 Fri Jan 20 14:45:28 PST 2017 classes.jar + 10111 Fri Jan 20 14:45:28 PST 2017 lint.jar + 39 Fri Jan 20 14:45:28 PST 2017 proguard.txt + 0 Fri Jan 20 14:45:24 PST 2017 aidl/ + 0 Fri Jan 20 14:45:28 PST 2017 assets/ + 0 Fri Jan 20 14:45:28 PST 2017 jni/ + 0 Fri Jan 20 14:45:28 PST 2017 res/ + 0 Fri Jan 20 14:45:28 PST 2017 libs/ +``` + +The advantage of this approach is that when lint notices that you +depend on a library, and that library contains custom lint checks, then +lint will pull in those checks and apply them. This gives library +authors a way to provide their own additional checks enforcing usage. + +### lintPublish Configuration + +The Android Gradle library plugin provides some special configurations, +`lintConfig` and `lintPublish`. + +The `lintPublish` configuration lets you reference another project, and +it will take that project's output jar and package it as a `lint.jar` +inside the AAR file. + +The [](https://github.com/googlesamples/android-custom-lint-rules) +sample project demonstrates this setup. + +The `:checks` project is a pure Kotlin library which depends on the +Lint APIs, implements a `Detector`, and provides an `IssueRegistry` +which is linked from `META-INF/services`. + +Then in the Android library, the `:library` project applies the Android +Gradle library plugin. It then specifies a `lintPublish` configuration +referencing the checks lint project: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +apply plugin: 'com.android.library' +dependencies { + lintPublish project(':checks') + // other dependencies +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Finally, the sample `:app` project is an example of an Android app +which depends on the library, and the source code in the app contains a +violation of the lint check defined in the `:checks` project. If you +run `./gradlew :app:lint` to analyze the app, the build will fail +emitting the custom lint check. + +### Local Checks + +What if you aren't publishing a library, but you'd like to apply +some checks locally for your own codebase? + +You can use a similar approach to `lintPublish`: In your app +module, specify + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +apply plugin: 'com.android.application' +dependencies { + lintConfig project(':checks') + // other dependencies +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now, when lint runs on this application, it will apply the checks +provided from the given project. + +!!! Warning + This mechanism works well on the CI server for enforcing local code + conventions, and it also works for developers on your team; the + errors should be flagged in the IDE (providing they are analyzing + single-file scopes). However, there have been various bugs and + difficulties around the lint checks getting rebuilt after changes or + clean builds. There are some bugs in the Android Gradle Plugin issue + tracker for this. + + diff --git a/docs/api-guide/quickfixes.md.html b/docs/api-guide/quickfixes.md.html new file mode 100644 index 00000000..8bd22855 --- /dev/null +++ b/docs/api-guide/quickfixes.md.html @@ -0,0 +1,294 @@ + + +# Adding Quick Fixes + +## Introduction + +When your detector reports an incident, it can also provide one or more +"quick fixes“, which are actions the users can invoke in the IDE (or, +for safe fixes, in batch mode) to address the reported incident. + +For example, if the lint check reports an unused resource, a quick fix +could offer to remove the unused resource. + +In some cases, quick fixes can take partial steps towards fixing the +problem, but not fully. For example, the accessibility lint check which +makes sure that for images you set a content description, the quickfix +can offer to add it -- but obviously it doesn't know what description +to put. In that case, the lint fix will go ahead and add the attribute +declaration with the correct namespace and attribute name, but will +leave the value up to the user (so it uses a special quick fix provided +by lint to place a TODO marker as the value, along with selecting just +that TODO string such that the user can type to replace without having +to manually delete the TODO string first.) + +## The LintFix builder class + +The class in lint which represents a quick fix is `LintFix`. + +Note that `LintFix` is **not** a class you can subclass and then for +example implement your own arbitrary code in something like a +`perform()` method. + +Instead, `LintFix` has a number of builders where you *describe* the +action that you would like the quickfix to take. Then, lint will offer +that quickfix in the IDE, and when the user invokes it, lint runs its +own implementation of the various descriptors. + +The historical reason for this is that many of the quickfixes in lint +depended on machinery in the IDE (such as code and import cleanup after +an edit operation) that isn't available in lint itself, along with +other concepts that only make sense in the IDE, such as moving the +caret, opening files, selecting text, and so on. + +More recently, this is also used to persist quickfixes properly for +later reuse; this is required for [partial +analysis](partial-analysis.md.html). + +## Creating a LintFix + +Lint fixes use a ”fluent API“; you first construct a `LintFix`, and on +that method you call various available type methods, which will then +further direct you to the allowed options. + +For example, to create a lint fix to set an XML attribute of a given +name to ”true“, use something like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +LintFix fix = fix().set(null, "singleLine", "true").build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here the `fix()` method is provided by the `Detector` super class, but +that's just a utility method for `LintFix.fix()` (or in older versions, +`LintFix.create()`). + +There are a number of additional, common methods you can set on +the `fix()` object: + +* `name`: Sets the description of the lint fix. This should be brief; + it's in the quickfix popup shown to the user. + +* `sharedName`: This sets the ”shared“ or ”family“ name: all fixes in + the file will with the same name can be applied in a single + invocation by the user. For example, if you register 500 ”Remove + unused import“ quickfixes in a file, you don't want to force the user + to have to invoke each and every one. By setting the shared name, the + user will be offered to **Fix All *$family name* problems in the + current file**, which they can then perform to have all 500 + individual fixes applied in one go. + +* `autoFix`: If you get a lint report and you notice there are a lot of + incidents that lint can fix automatically, you don't want to have to + go and open each and every file and all the fixes in the file. + Therefore, lint can apply the fixes in batch mode; the Gradle + integration has a `lintFix` target to perform this, and the `lint` + command has an `--apply-suggestions` option. + + However, many quick fixes require user intervention. Not just the + ones where the user has to choose among alternatives, and not just + the ones where the quick fix inserts a placeholder value like TODO. + Take for example lint's built-in check which requires overrides of a + method annotated with `@CallSuper` to invoke `super.` on the + overridden method. Where should we insert the call -- at the + beginning? At the end? + + Therefore, lint has the `autoFix` property you can set on a quickfix. + This indicates that this fix is ”safe“ and can be performed in batch + mode. When the `lintFix` target runs, it will only apply fixes marked + safe in this way. + +## Available Fixes + +The current set of available quick fix types are: + +* `fix().replace`: String replacements. This is the most general + mechanism, and allows you to perform arbitrary edits to the source + code. In addition to the obvious ”replace old string with new“, the + old string can use a different location range than the incident + range, you can match with regular expressions (and perform + replacements on a specific group within the regular expression), and + so on. + + This fix is also the most straightforward way to **delete** text. + + It offers some useful cleanup operations: + + - Source code cleanup, which will run the IDE's code formatter on the + modified source code range. This will apply the user's code + preferences, such as whether there should be a space between a cast + and the expression, and so on. + + - Import cleanup. That means that if you are referencing a new type, + you don't have to worry about checking whether it is imported and + if not adding an import statement; you can simply write your string + replacements using the fully qualified names, and then tag the + quickfix with the import cleanup option, and when the quickfix is + performed the import will be added if necessary and all the fully + qualified references replaced with simple names. And this will also + correctly handle the scenario where the symbols cannot be replaced + with simple names because there is a conflicting import of the same + name from a different package. + +* `fix().annotate`: Annotating an element. This will add (or optionally + replace) an annotation on a source element such as a method. It will + also handle import management. + +* `fix().set`: Add XML attributes. This will insert an attribute into + the given element, applying the user's code style preferences for + where to insert the attribute. (In Android XML for example there's a + specific sorting convention which is generally alphabetical, except + layout params go before other attributes, and width goes before + height.) + + You can either set the value to something specific, or place the + caret inside the newly created empty attribute value, or set it + to TODO and select that text for easy type-to-replace. + +!!! Tip + If you use the `todo()` quickfix, it's a good idea to special case + your lint check to deliberately not accept ”TODO“ as a valid value. + For example, for lint's accessibility check which makes sure you set + a content description, it will complain both when you haven't set + the content description attribute, **and** if the text is set to + ”TODO“. That way, if the user applies the quickfix, which creates + the attribute in the right place and moves the focus to the right + place, the editor is still showing a warning that the content + description should be set. + +* `fix().unset`: Remove XML attribute. This is a special case of add + attribute. + +* `fix().url`: Show URL. In some cases, you can't ”fix“ or do anything + local to address the problem, but you really want to direct the + user's attention to additional documentation. In that case, you can + attach a ”show this URL“ quick fix to the incident which will open + the browser with the given URL when invoked. For example, in a + complicated deprecation where you want users to migrate from one + approach to a completely different one that you cannot automate, you + could use something like this: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val message = "Job scheduling with `GcmNetworkManager` is deprecated: Use AndroidX `WorkManager` instead" +val fix = fix() +.url("https://developer.android.com/topic/libraries/architecture/workmanager/migrating-gcm") +.build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Combining Fixes + +You might notice that lint's APIs to report incidents only takes a +**single** quick fix instead of a list of fixes. + +But let's say that it *did* take a list of quick fixes. + +- Should they *all* be performed as a single unit? That makes sense if + you're trying to write a quickfix which performs multiple string + replacements. + +- Or should they be offered as separate alternatives for the user to + choose between? That makes sense if the incident says for example + that you must set at least one attribute among three possibilities; + in this case we may want to add quickfixes for setting each attribute. + +Both scenarios have their uses, so lint makes this explicit: + +- `fix().composite`: create a ”composite“ fix, which composes the fix + out of multiple individual fixes, or + +- `fix().alternatives`: create an ”alternatives“ fix, which holds a + number of individual fixes, which lint will present as separate + options to the user. + +Here's an example of how to create a composite fix, which will be +performed as a unit; here we're both setting a new attribute and +deleting a previous attribute: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val fix = fix().name("Replace with singleLine=\"true\"") + .composite( + fix().set(ANDROID_URI, "singleLine", "true").build(), + fix().unset(namespace, oldAttributeName).build() + ) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +And here's an example of how to create an alternatives fix, which are +offered to the user as separate options; this is from our earlier +example of the accessibility check which requires you to set a content +description, which can be set either on the ”text“ attribute or the +"contentDescription” attribute: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +val fix = fix().alternatives( + fix().set().todo(ANDROID_URI, "text").build(), + fix().set().todo(ANDROID_URI, "contentDescription") + .build()) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Refactoring Java and Kotlin code + +It would be nice if there was an AST manipulation API, similar to UAST +for visiting ASTs, that quickfixes could use to implement refactorings, +but we don't have a library like that. And it's unlikely it would work +well; when you rewrite the user's code you typically have to take +language specific conventions into account. + +Therefore, today, when you create quickfixes for Kotlin and Java code, +if the quickfix isn't something simple which would work for both +languages, then you need to conditionally create either the Kotlin +version or the Java version of the quickfix based on whether the source +file it applies to is in Kotlin or Java. (For an easy way to check you +can use the `isKotlin` or `isJava` package level methods in +`com.android.tools.lint.detector.api`.) + +However, it's often the case that the quickfix is something simple +which would work for both; that's true for most of the built-in lint +checks with quickfixes for Kotlin and Java. + +## Regular Expressions and Back References + +The `replace` string quick fix allows you to match the text to +with regular expressions. + +You can also use back references in the regular expression such +that the quick fix replacement text includes portions from the +original string. + +Here's an example from lint's `AssertDetector`: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +val fix = fix().name("Surround with desiredAssertionStatus() check") + .replace() + .range(context.getLocation(assertCall)) + .pattern("(.*)") + .with("if (javaClass.desiredAssertionStatus()) { \\k<1> }") + .reformat(true) + .build() +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The replacement string's back reference above, on line 5, is \k<1>. If +there were multiple regular expression groups in the replacement +string, this could have been \k<2>, \k<3>, and so on. + +Here's how this looks when applied, from its unit test: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin +lint().files().run().expectFixDiffs( + """ + Fix for src/test/pkg/AssertTest.kt line 18: Surround with desiredAssertionStatus() check: + @@ -18 +18 + - assert(expensive()) // WARN + + if (javaClass.desiredAssertionStatus()) { assert(expensive()) } // WARN + """ +) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +## Emitting quick fix XML to apply on CI + +Note that the `lint` has an option (`--describe-suggestions`) to emit +an XML file which describes all the edits to perform on documents to +apply a fix. This maps all quick fixes into chapter edits (including +XML logic operations). This can be (and is, within Google) used to +integrate with code review tools such that the user can choose whether +to auto-fix a suggestion right from within the code review tool. + + diff --git a/docs/api-guide/terminology.md.html b/docs/api-guide/terminology.md.html new file mode 100644 index 00000000..be09e8c7 --- /dev/null +++ b/docs/api-guide/terminology.md.html @@ -0,0 +1,112 @@ + + +# Terminology + +You don't need to read this up front and understand everything, but +this is hopefully a handy reference to return to. + +In alphabetical order: + +Configuration +: A configuration provides extra information or parameters to lint on a + per project, or even per directory basis. For example, the `lint.xml` + files can change the severity for issues, or list incidents to ignore + (matched for example by a regular expression), or even provide values + for options read by a specific detector. + +Context +: An object passed into detectors in many APIs, providing data about + (for example) which file is being analyzed (and in which project), + and for specific types of analysis additional information; for + example, an XmlContext points to the DOM document, a JavaContext + includes the AST, and so on. + +Detector +: The implementation of the lint check which registers Issues, analyzes + the code, and reports Incidents. + +Implementation +: An `Implementation` tells lint how a given issue is actually + analyzed, such as which detector class to instantiate, as well as + which scopes the detector applies to. + +Incident +: A specific occurrence of the issue at a specific location. + An example of an incident is: + ```text + Warning: In file IoUtils.kt, line 140, the field download folder + is "/sdcard/downloads"; do not hardcode the path to `/sdcard`. + ``` + +Issue +: A type or class of problem that your lint check identifies. An issue + has an associated severity (error, warning or info), a priority, a + category, an explanation, and so on. + + An example of an issue is “Don't hardcode paths to /sdcard”. + +IssueRegistry +: An `IssueRegistry` provides a list of issues to lint. When you write + one or more lint checks, you'll register these in an `IssueRegistry` + and point to it using the `META-INF` service loader mechanism. + +LintClient +: The `LintClient` represents the specific tool the detector is running + in. For example, when running in the IDE there is a LintClient which + (when incidents are reported) will show highlights in the editor, + whereas when lint is running as part of the Gradle plugin, incidents + are instead accumulated into HTML (and XML and text) reports, and + the build interrupted on error. + +Location +: A “location” refers to a place where an incident is reported. + Typically this refers to a text range within a source file, but a + location can also point to a binary file such as a `png` file. + Locations can also be linked together, along with descriptions. + Therefore, if you for example are reporting a duplicate declaration, + you can include **both** Locations, and in the IDE, both locations + (if they're in the same file) will be highlighted. A location linked + from another is called a “secondary” location, but the chaining can + be as long as you want (and lint's unit testing infrastructure will + make sure there are no cycles.) + +Partial Analysis +: A “map reduce” architecture in lint which makes it possible to + analyze individual modules in isolation and then later filter and + customize the partial results based on conditions outside of these + modules. This is explained in greater detail in the + [partial analysis](partial-analysis.md.html) chapter. + +Platform +: The `Platform` abstraction allows lint issues to indicate where they + apply (such as “Android”, or “Server”, and so on). This means that an + Android-specific check won't trigger warnings on non-Android code. + +Scope +: `Scope` is an enum which lists various types of files that a detector + may want to analyze. + + For example, there is a scope for XML files, there is a scope for + Java and Kotlin files, there is a scope for .class files, and so on. + + Typically lint cares about which **set** of scopes apply, + so most of the APIs take an `EnumSet< Scope>`, but we'll often + refer to this as just “the scope” instead of the “scope set”. + +Severity +: For an issue, whether the incident should be an error, or just a + warning, or neither (just an FYI highlight). There is also a special + type of error severity, “fatal”, discussed later. + +TextFormat +: An enum describing various text formats lint understands. Lint checks + will typically only operate with the “raw” format, which is + markdown-like (e.g. you can surround words with an asterisk to make + it italics or two to make it bold, and so on). + +Vendor +: A `Vendor` is a simple data class which provides information about + the provenance of a lint check: who wrote it, where to file issues, + and so on. + + diff --git a/docs/api-guide/unit-testing.md.html b/docs/api-guide/unit-testing.md.html new file mode 100644 index 00000000..9c3f3bce --- /dev/null +++ b/docs/api-guide/unit-testing.md.html @@ -0,0 +1,352 @@ + + +# Lint Check Unit Testing + +Lint has a dedicated testing library for lint checks. To use it, +add this dependency to your lint check Gradle project: + +``` +testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +``` + +This lends itself nicely to test-driven development. When we get bug +reports of a false positive, we typically start by adding the text for +the repro case, ensure that the test is failing, and then work on the +bug fix (often setting breakpoints and debugging through the unit test) +until it passes. + +## Creating a Unit Test + +Here's a sample lint unit test for a simple, sample lint check which +just issues warnings whenever it sees the word “lint” mentioned +in a string: + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~kotlin linenumbers +package com.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test + +class SampleCodeDetectorTest { + @Test + fun testBasic() { + lint().files( + java( + """ + package test.pkg; + public class TestClass1 { + // In a comment, mentioning "lint" has no effect + private static String s1 = "Ignore non-word usages: linting"; + private static String s2 = "Let's say it: lint"; + } + """ + ).indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId] + private static String s2 = "Let's say it: lint"; + ∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼ + 0 errors, 1 warnings + """ + ) + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Lint's testing API is a “fluent API”; you chain method calls together, +and the return objects determine what is allowed next. + +Notice how we construct a test object here on line 10 with the `lint()` +call. This is a “lint test task”, which has a number of setup methods +on it (such as the set of source files we want to analyze), the issues +it should consider, etc. + +Then, on line 23, the `run()` method. This runs the lint unit test, and +then it returns a result object. On the result object we have a number +of methods to verify that the test succeeded. For a test making sure we +don't have false positives, you can just call `expectClean()`. But the +most common operation is to call `expect(output)`. + +!!! Tip + Notice how we're including the whole text output here; including not + just the error message and line number, but lint's output of the + relevant line and the error range (using ~~~~ characters). + + This is the recommended practice for lint checks. It may be tempting + to avoid “duplication” of repeating error messages in the tests + (“DRY”), so some developers have written tests where they just + assert that a given test has say “2 warnings”. But this isn't + testing that the error range is exactly what you expect (which + matters a lot when users are seeing the lint check from the IDE, + since that's the underlined region), and it could also continue to + pass even if the errors flagged are no longer what you intended. + + Finally, even if the location is correct today, it may not be + correct tomorrow. Several times in the past, some unit tests in + lint's built-in checks have started failing after an update to the + Kotlin compiler because of some changes to the AST which required + tweaks here and there. + +## Computing the Expected Output + +You may wonder how we knew what to paste into our `expect` call +to begin with. + +We didn't. When you write a test, simply start with +`expect("")`, and run the test. It will fail. You can now +copy the actual output into the `expect` call as the expected +output, provided of course that it's correct! + +## Test Files + +On line 11, we construct a Java test file. We call `java(...)` and pass +in the source file contents. This constructs a `TestFile`, and there +are a number of different types of test source files, such as for +Kotlin files, manifest files, icons, property files, and so on. + +Using test file descriptors like this has a number of advantages +over the traditional approach of checking in test files as sources: + +* Everything is kept together, so it's easier to look at a test and see + what it analyzes and what the expected results are. This is + particularly important for complex lint checks which test a lot of + scenarios. As of this writing, `ApiDetectorTest` has 157 individual + unit tests. + +* Lint can provide a DSL to construct test files easily. For example, + `projectProperties().compileSdk(17)` and + `manifest().minSdk(5).targetSdk(17)` construct a `project.properties` + and an `AndroidManifest.xml` file with the correct contents to + specify for example the right element setting up the + `minSdkVersion` and `targetSdkVersion`. + + For icons, we can construct bitmaps like this: +``` + image("res/mipmap-hdpi/my_launcher2_round.png", 50, 50) + .fillOval(0, 0, 50, 50, 0xFFFFFFFF) + .text(5, 5, "x", 0xFFFFFFFF)) +``` + +* Similarly, when we construct `java()` or `kotlin()` test sources, we + don't have to name the files, because lint will analyze the source + code and figure out what the class file should be named and where to + place it. + +* We can easily “parameterize” our test files. For example, if you + want to run your unit test against a 100K json file, you can + construct it programmatically; you don't have to check one in. + +* Since test sources often (deliberately!) have errors in them, this + sometimes causes problems with the tooling; for example, some code + review tools will flag “disallowed” constructs or things like tabs or + trailing spaces, which may be deliberate in a lint unit test. + +* Lint originally checked in test sources as individual files. + Unfortunately over time, source files ended up getting reused by + multiple tests. And that made it harder to make changes, or figure + out whether test sources are still in use, and so on. + +* Last but not least, because all the test construction methods + specify the correct mime type for their string parameters, IntelliJ + will actually syntax highlight the test source declarations! Here's + how this looks: + + ![Screenshot of nested highlighting](nested-syntax-highlighting.png) + +## Trimming indents? + +Notice how in the above Kotlin unit tests we used raw strings, **and** +we indented the sources to be flush with the opening “”“ string +delimiter. + +You might be tempted to call `.trimIndent()` on the raw string. +However, doing that would break the above nested syntax highlighting +method (or at least it used to). Therefore, instead, call `.indented()` +on the test file itself, not the string, as shown on line 20. + +Note that we don't need to do anything with the `expect` call; lint +will automatically call `trimIndent()` on the string passed in to it. + +## Dollars in Raw Strings + +Kotlin requires that raw strings have to escape the dollar ($) +character. That's normally not a problem, but for some source files, it +makes the source code look **really** messy and unreadable. + +For that reason, lint will actually convert $ into $ (a unicode wide +dollar sign). Lint lets you use this character in test sources, and it +always converts the test output to use it (though it will convert in +the opposite direction when creating the test sources on disk). + +## Quickfixes + +If your lint check registers quickfixes with the reported incidents, +it's trivial to test these as well. + +For example, for a lint check result which flags two incidents, with a +single quickfix, the unit test looks like this: + +``` +lint().files(...) + .run() + .expect(expected) + .expectFixDiffs( + "" + + "Fix for res/layout/textsize.xml line 10: Replace with sp:\n" + + "@@ -11 +11\n" + + "- android:textSize=\"14dp\" />\n" + + "+ android:textSize=\"14sp\" />\n" + + "Fix for res/layout/textsize.xml line 15: Replace with sp:\n" + + "@@ -16 +16\n" + + "- android:textSize=\"14dip\" />\n" + + "+ android:textSize=\"14sp\" />\n"); +``` + +The `expectFixDiffs` method will iterate over all the incidents it +found, and in succession, apply the fix, diff the two sources, and +append this diff along with the fix message into the log. + +When there are multiple fixes offered for a single incident, it will +iterate through all of these too: + +``` +lint().files(...) + .run() + .expect(expected) + .expectFixDiffs( + + "Fix for res/layout/autofill.xml line 7: Set autofillHints:\n" + + "@@ -12 +12\n" + + " android:layout_width=\"match_parent\"\n" + + " android:layout_height=\"wrap_content\"\n" + + "+ android:autofillHints=\"|\"\n" + + " android:hint=\"hint\"\n" + + " android:inputType=\"password\" >\n" + + "Fix for res/layout/autofill.xml line 7: Set importantForAutofill=\"no\":\n" + + "@@ -13 +13\n" + + " android:layout_height=\"wrap_content\"\n" + + " android:hint=\"hint\"\n" + + "+ android:importantForAutofill=\"no\"\n" + + " android:inputType=\"password\" >\n" + + " \n"); +``` + +## Library Dependencies and Stubs + +Let's say you're writing a lint check for something like the +AndroidX library's `RecyclerView` widget. + +In this case, it's highly likely that your unit test will reference +`RecyclerView`. But how does lint know what `RecyclerView` is? If it +doesn't, type resolve won't work, and as a result the detector won't. + +You could make your test ”depend“ on the recyclerview. This is +possible, using the `LibraryReferenceTestFile`, but is not recommended. + +Instead, the recommended approach is to just use ”stubs“; create +skeleton classes which represent only the **signatures** of the +library, and in particular, only the subset that your lint check +cares about. + +For example, for lint's own recycler view test, the unit test +declares a field holding the recycler view stub: + +``` +private val recyclerViewStub = java( + """ + package android.support.v7.widget; + + import android.content.Context; + import android.util.AttributeSet; + import android.view.View; + import java.util.List; + + // Just a stub for lint unit tests + public class RecyclerView extends View { + public RecyclerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public abstract static class ViewHolder { + public ViewHolder(View itemView) { + } + } + + public abstract static class Adapter { + public abstract void onBindViewHolder(VH holder, int position); + public void onBindViewHolder(VH holder, int position, List payloads) { + } + public void notifyDataSetChanged() { } + } + } + """ +).indented() +``` + +And now, all the other unit tests simply include `recyclerViewStub` +as one of the test files. For a larger example, see +[this test](https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/SliceDetectorTest.kt). + +## Binary and Compiled Source Files + +If you need to use binaries in your unit tests, there is +a special test file type for that: base64gzip. Here's an +example from a lint check which tries to recognize usage +of Cordova in the bytecode: + +``` +fun testVulnerableCordovaVersionInClasses() { + lint().files( + base64gzip( + "bin/classes/org/apache/cordova/Device.class", + "" + + "yv66vgAAADIAFAoABQAPCAAQCQAEABEHABIHABMBAA5jb3Jkb3ZhVmVyc2lv" + + "bgEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABjxpbml0PgEAAygpVgEABENvZGUB" + + "AA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAtE" + + "ZXZpY2UuamF2YQwACAAJAQAFMi43LjAMAAYABwEAGW9yZy9hcGFjaGUvY29y" + + "ZG92YS9EZXZpY2UBABBqYXZhL2xhbmcvT2JqZWN0ACEABAAFAAAAAQAJAAYA" + + "BwAAAAIAAQAIAAkAAQAKAAAAHQABAAEAAAAFKrcAAbEAAAABAAsAAAAGAAEA" + + "AAAEAAgADAAJAAEACgAAAB4AAQAAAAAABhICswADsQAAAAEACwAAAAYAAQAA" + + "AAUAAQANAAAAAgAO" + )` + ).run().expect( +``` + +Here, ”base64gzip“ means that the file is gzipped and then base64 +encoded. + +If you want to compute the base64gzip string for a given file, a simple +way to do it is to add this statement at the beginning of your test: + +``` +assertEquals("", TestFiles.toBase64gzip(File("/tmp/mybinary.bin"))) +``` + +The test will fail, and now you have your output to copy/paste into the +test. + +However, if you're writing byte-code based tests, don't just hard code +in the .class file or .jar file contents like this. Lint's own unit +tests did that, and it's hard to later reconstruct what the byte code +was later if you need to make changes or extend it to other bytecode +formats. + +Instead, use the new `compiled` or `bytecode` test files. The key here +is that they automate a bit of the above process: the test file +provides a source test file, as well as a set of corresponding binary +files (since a single source file can create multiple class files, and +for Kotlin, some META-INF data). + +Initially, you just specify the sources, and when no binary data +has been provided, lint will instead attempt to compile the sources +and emit the full test file registration. + +This isn't just a convenience; lint's test infrastructure also uses +this to test some additional scenarios (for example, in a multi- module +project it will only provide the binaries, not the sources, for +upstream modules.) + + diff --git a/docs/book.html b/docs/book.html new file mode 100644 index 00000000..2a776034 --- /dev/null +++ b/docs/book.html @@ -0,0 +1,4410 @@ + + + + +

Android Lint API Guide

Android Lint API Guide
+ +
+ +

+ +This chapter inlines all the API documentation into a single +long chapter, suitable for printing or reading on a tablet. + +

+
Contents

(Top)
+Terminology
+Writing a Lint Check: Basics
+  2.1  Preliminaries
+    2.1.1  “Lint?”
+    2.1.2  API Stability
+    2.1.3  Kotlin
+  2.2  Concepts
+  2.3  Client API versus Detector API
+  2.4  Creating an Issue
+  2.5  TextFormat
+  2.6  Issue Implementation
+  2.7  Scopes
+  2.8  Registering the Issue
+  2.9  Implementing a Detector: Scanners
+  2.10  Detector Lifecycle
+  2.11  Scanner Order
+  2.12  Implementing a Detector: Services
+  2.13  Scanner Example
+  2.14  Analyzing Kotlin and Java Code
+    2.14.1  UAST
+    2.14.2  UAST Example
+    2.14.3  Looking up UAST
+    2.14.4  Resolving
+    2.14.5  PSI
+  2.15  Testing
+Example: Sample Lint Check GitHub Project
+  3.1  Project Layout
+  3.2  :checks
+  3.3  lintVersion?
+  3.4  :library and :app
+  3.5  Lint Check Project Layout
+  3.6  Service Registration
+  3.7  IssueRegistry
+  3.8  Detector
+  3.9  Detector Test
+Publishing a Lint Check
+  4.1  Android
+    4.1.1  AAR Support
+    4.1.2  lintPublish Configuration
+    4.1.3  Local Checks
+Lint Check Unit Testing
+  5.1  Creating a Unit Test
+  5.2  Computing the Expected Output
+  5.3  Test Files
+  5.4  Trimming indents?
+  5.5  Dollars in Raw Strings
+  5.6  Quickfixes
+  5.7  Library Dependencies and Stubs
+  5.8  Binary and Compiled Source Files
+Adding Quick Fixes
+  6.1  Introduction
+  6.2  The LintFix builder class
+  6.3  Creating a LintFix
+  6.4  Available Fixes
+  6.5  Combining Fixes
+  6.6  Refactoring Java and Kotlin code
+  6.7  Regular Expressions and Back References
+  6.8  Emitting quick fix XML to apply on CI
+Partial Analysis
+  7.1  About
+  7.2  The Problem
+  7.3  Overview
+  7.4  Does my Detector Need Work?
+    7.4.1  Catching Mistakes: Blocking Access to Main Project
+    7.4.2  Catching Mistakes: Simulated App Module
+    7.4.3  Catching Mistakes: Diffing Results
+    7.4.4  Catching Mistakes: Remaining Issues
+  7.5  Incidents
+  7.6  Constraints
+  7.7  Incident LintMaps
+  7.8  Module LintMaps
+  7.9  Optimizations
+Frequently Asked Questions
+    8.0.1  My detector callbacks aren't invoked
+    8.0.2  My lint check works from the unit test but not in the IDE
+    8.0.3  visitAnnotationUsage isn't called for annotations
+    8.0.4  How do I check if a UAST or PSI element is for Java or Kotlin?
+    8.0.5  What if I need a PsiElement and I have a UElement ?
+    8.0.6  How do I get the UMethod for a PsiMethod ?
+    8.0.7  How do get a JavaEvaluator ?
+    8.0.8  How do I check whether an element is internal?
+    8.0.9  Is element inline, sealed, operator, infix, suspend, data?
+    8.0.10  How do I look up a class if I have its fully qualified name?
+    8.0.11  How do I look up a class if I have a PsiType?
+    8.0.12  How do I look up hierarhcy annotations for an element?
+    8.0.13  How do I look up if a class is a subclass of another?
+    8.0.14  How do I know which parameter a call argument corresponds to?
+    8.0.15  How can my lint checks target two different versions of lint?
+    8.0.16  How do I check out the current lint source code?
+    8.0.17  Where do I find examples of lint checks?
+Appendix: Recent Changes
+10  Appendix: Environment Variables and System Properties
+  10.1  Environment Variables
+    10.1.1  Detector Configuration Variables
+    10.1.2  Lint Configuration Variables
+    10.1.3  Lint Development Variables
+  10.2  System Properties
+

   

Terminology

+

+ + +You don't need to read this up front and understand everything, but +this is hopefully a handy reference to return to. + +

+ +In alphabetical order: + +

+ +

Configuration

A configuration provides extra information or parameters to lint on a + per project, or even per directory basis. For example, the lint.xml + files can change the severity for issues, or list incidents to ignore + (matched for example by a regular expression), or even provide values + for options read by a specific detector. + +

Context

An object passed into detectors in many APIs, providing data about + (for example) which file is being analyzed (and in which project), + and for specific types of analysis additional information; for + example, an XmlContext points to the DOM document, a JavaContext + includes the AST, and so on. + +

Detector

The implementation of the lint check which registers Issues, analyzes + the code, and reports Incidents. + +

Implementation

An Implementation tells lint how a given issue is actually + analyzed, such as which detector class to instantiate, as well as + which scopes the detector applies to. + +

Incident

A specific occurrence of the issue at a specific location. + An example of an incident is: +

    Warning: In file IoUtils.kt, line 140, the field download folder
+    is "/sdcard/downloads"; do not hardcode the path to `/sdcard`.
Issue

A type or class of problem that your lint check identifies. An issue + has an associated severity (error, warning or info), a priority, a + category, an explanation, and so on. + +

+ + An example of an issue is “Don't hardcode paths to /sdcard”. + +

IssueRegistry

An IssueRegistry provides a list of issues to lint. When you write + one or more lint checks, you'll register these in an IssueRegistry + and point to it using the META-INF service loader mechanism. + +

LintClient

The LintClient represents the specific tool the detector is running + in. For example, when running in the IDE there is a LintClient which + (when incidents are reported) will show highlights in the editor, + whereas when lint is running as part of the Gradle plugin, incidents + are instead accumulated into HTML (and XML and text) reports, and + the build interrupted on error. + +

Location

A “location” refers to a place where an incident is reported. + Typically this refers to a text range within a source file, but a + location can also point to a binary file such as a png file. + Locations can also be linked together, along with descriptions. + Therefore, if you for example are reporting a duplicate declaration, + you can include both Locations, and in the IDE, both locations + (if they're in the same file) will be highlighted. A location linked + from another is called a “secondary” location, but the chaining can + be as long as you want (and lint's unit testing infrastructure will + make sure there are no cycles.) + +

Partial Analysis

A “map reduce” architecture in lint which makes it possible to + analyze individual modules in isolation and then later filter and + customize the partial results based on conditions outside of these + modules. This is explained in greater detail in the + partial analysis chapter. + +

Platform

The Platform abstraction allows lint issues to indicate where they + apply (such as “Android”, or “Server”, and so on). This means that an + Android-specific check won't trigger warnings on non-Android code. + +

Scope

Scope is an enum which lists various types of files that a detector + may want to analyze. + +

+ + For example, there is a scope for XML files, there is a scope for + Java and Kotlin files, there is a scope for .class files, and so on. + +

+ + Typically lint cares about which set of scopes apply, + so most of the APIs take an EnumSet< Scope>, but we'll often + refer to this as just “the scope” instead of the “scope set”. + +

Severity

For an issue, whether the incident should be an error, or just a + warning, or neither (just an FYI highlight). There is also a special + type of error severity, “fatal”, discussed later. + +

TextFormat

An enum describing various text formats lint understands. Lint checks + will typically only operate with the “raw” format, which is + markdown-like (e.g. you can surround words with an asterisk to make + it italics or two to make it bold, and so on). + +

Vendor

A Vendor is a simple data class which provides information about + the provenance of a lint check: who wrote it, where to file issues, + and so on. + +

+   

Writing a Lint Check: Basics

+ +   

Preliminaries

+

+ + +(If you already know a lot of the basics but you're here because you've +run into a problem and you're consulting the docs, take a look at the +frequently asked questions chapter.) + +

+   

“Lint?”

+

+ + +The lint tool shipped with the C compiler and provided additional +static analysis of C code beyond what the compiler checked. + +

+ +Android Lint was named in honor of this tool, and with the Android +prefix to make it really clear that this is a static analysis tool +intended for analysis of Android code, provided by the Android Open +Source Project — and to disambiguate it from the many other tools with +“lint“ in their names. + +

+ +However, since then, Android Lint has broadened its support and is no +longer intended only for Android code. In fact, within Google, it is +used to analyze all Java and Kotlin code. One of the reasons for this +is that it can easily analyze both Java and Kotlin code without having +to implement the checks twice. Additional features are described in the +features chapter. + +

+ +We're planning to rename lint to reflect this new role, so we are +looking for good name suggestions. + +

+   

API Stability

+

+ + +Lint's APIs are still marked as @Beta, and we have made it very clear +all along that this is not a stable API, so custom lint checks may need +to be updated periodically to keep working. + +

+ +However, ”some APIs are more stable than others“. In particular, the +detector API (described below) is much less likely to change than the +client API (which is not intended for lint check authors but for tools +integrating lint to run within, such as IDEs and build systems). + +

+ +However, this doesn't mean the detector API won't change. A large part +of the API surface is external to lint; it's the AST libraries (PSI and +UAST) for Java and Kotlin from JetBrains; it's the bytecode library +(asm.ow2.io), it's the XML DOM library (org.w3c.dom), and so on. Lint +intentionally stays up to date with these, so any API or behavior +changes in these can affect your lint checks. + +

+ +Lint's own APIs may also change. The current API has grown organically +over the last 10 years (the first version of lint was released in 2011) +and there are a number of things we'd clean up and do differently if +starting over. Not to mention rename and clean up inconsistencies. + +

+ +However, lint has been pretty widely adopted, so at this point creating +a nicer API would probably cause more harm than good, so we're limiting +recent changes to just the necessary ones. An example of this is the +new partial analysis architecture in 7.0 +which is there to allow much better CI and incremental analysis +performance. + +

+   

Kotlin

+

+ + +We recommend that you implement your checks in Kotlin. Part of +the reason for that is that the lint API uses a number of Kotlin +features: + +

+ +

    +
  • Named and default parameters: Rather than using builders, some + construction methods, like Issue.create() have a lot of parameters + with default parameters. The API is cleaner to use if you just + specify what you need and rely on defaults for everything else. + +

    + +

  • +
  • Compatibility: We may add additional parameters over time. It + isn't practical to add @JvmOverloads on everything. + +

    + +

  • +
  • Package-level functions: Lint's API includes a number of package + level utility functions (in previous versions of the API these are all + thrown together in a LintUtils class). + +

    + +

  • +
  • Deprecations: Kotlin has support for simple API migrations. For + example, in the below example, the new @Deprecated annotation on + lines 1 through 7 will be added in an upcoming release, to ease + migration to a new API. IntelliJ can automatically quickfix these + deprecation replacements.
+ +

@Deprecated( + "Use the new report(Incident) method instead, which is more future proof", + ReplaceWith( + "report(Incident(issue, message, location, null, quickfixData))", + "com.android.tools.lint.detector.api.Incident" + ) +) +@JvmOverloads +open fun report( + issue: Issue, + location: Location, + message: String, + quickfixData: LintFix? = null +) { + // ... +}

+ +As of 7.0, there is more Kotlin code in lint than remaining Java +code: +

+ + + +
Language files blank comment code
Kotlin 420 14243 23239 130250
Java 289 8683 15205 101549
$ cloc lint/
+ +

+ +And that's for all of lint, including many old lint detectors which +haven't been touched in years. In the Lint API library, +lint/libs/lint-api, the code is 78% Kotlin and 22% Java. + +

+   

Concepts

+

+ + +Lint will search your source code for problems. There are many types of +problems, and each one is called an Issue, which has associated +metadata like a unique id, a category, an explanation, and so on. + +

+ +Each instance that it finds is called an ”incident“. + +

+ +The actual responsibility of searching for and reporting incidents is +handled by detectors — subclasses of Detector. Your lint check will +extend Detector, and when it has found a problem, it will ”report“ +the incident to lint. + +

+ +A Detector can analyze more than one Issue. For example, the +built-in StringFormatDetector analyzes formatting strings passed to +String.format() calls, and in the process of doing that discovers +multiple unrelated issues — invalid formatting strings, formatting +strings which should probably use the plurals API instead, mismatched +types, and so on. The detector could simply have a single issue called +“StringFormatProblems” and report everything as a StringFormatProblem, +but that's not a good idea. Each of these individual types of String +format problems should have their own explanation, their own category, +their own severity, and most importantly should be individually +configurable by the user such that they can disable or promote one of +these issues separately from the others. + +

+ +A Detector can indicate which sets of files it cares about. These are +called “scopes”, and the way this works is that when you register your +Issue, you tell that issue which Detector class is responsible for +analyzing it, as well as which scopes the detector cares about. + +

+ +If for example a lint check wants to analyze Kotlin files, it can +include the Scope.JAVA_FILE scope, and now that detector will be +included when lint processes Java or Kotin files. + +

+ +

The name Scope.JAVA_FILE may make it sound like there should also + be a Scope.KOTLIN_FILE. However, JAVA_FILE here really refers to + both Java and Kotlin files since the analysis and APIs are identical + for both (using “UAST”, a universal abstract syntax tree). However, + at this point we don't want to rename it since it would break a lot + of existing checks. We might introduce an alias and deprecate this + one in the future.
+ +

+ +When detectors implement various callbacks, they can analyze the +code, and if they find a problematic pattern, they can “report” +the incident. This means computing an error message, as well as +a “location”. A “location” for an incident is really an error +range — a file, and a starting offset and an ending offset. Locations +can also be linked together, so for example for a “duplicate +declaration” error, you can and should include both locations. + +

+ +Many detector methods will pass in a Context, or a more specific +subclass of Context such as JavaContext or XmlContext. This +allows lint to provide access to the detectors information they may +need, without passing in a lot of parameters (and allowing lint to add +additional data over time without breaking signatures). + +

+ +The Context classes also provide many convenience APIs. For example, +for XmlContext there are methods for creating locations for XML tags, +XML attributes, just the name part of an XML attribute and just the +value part of an XML attribute. For a JavaContext there are also +methods for creating locations, such as for a method call, including +whether to include the receiver and/or the argument list. + +

+ +When you report an Incident you can also provide a LintFix; this is +a quickfix which the IDE can use to offer actions to take on the +warning. In some cases, you can offer a complete and correct fix (such +as removing an unused element). In other cases the fix may be less +clear; for example, the AccessibilityDetector asks you to set a +description for images; the quickfix will set the content attribute, +but will leave the text value as TODO and will select the string such +that the user can just type to replace it. + +

+ +

When reporting incidents, make sure that the error messages are not + generic; try to be explicit and include specifics for the current + scenario. For example, instead of just “Duplicate declaration”, use + “$name has already been declared”. This isn't just for cosmetics; + it also makes lint's baseline + mechanism work better since it + currently matches by id + file + message, not by line numbers which + typically drift over time.
+ +

+   

Client API versus Detector API

+

+ + +Lint's API has two halves: + +

+ +

    +
  • The Client API: “Integrate (and run) lint from within a tool”. + For example, both the IDE and the build system uses this API to embed + and invoke lint to analyze the code in the project or editor. + +

    + +

  • +
  • The Detector API: “Implement a new lint check”. This is the API + which lets checkers analyze code and report problems that they find.
+ +

+ +The class in the Client API which represents lint running in a tool is +called LintClient. This class is responsible for, among other things: + +

+ +

    +
  • Reporting incidents found by detectors. For example, in the IDE, it + will place error markers into the source editor, and in a build + system, it may write warnings to the console or generate a report or + even fail the build. + +

    + +

  • +
  • Handling I/O. Detectors should never read files from disk directly. + This allows lint checks to work smoothly in for example the IDE. When + lint runs on the fly, and a lint check asks for the source file + contents (or other supporting files), the LintClient in the IDE + will implement the readFile method to first look in the open source + editors and if the requested file is being edited, it will return the + current (often unsaved!) contents. + +

    + +

  • +
  • Handling network traffic. Lint checks should never open + URLConnections themselves. By going through the lint API to request + data for a URL, not only can the LintClient for example use any + configured IDE proxy settings which is done in the IntelliJ + integration of lint, but even the lint check's own unit tests can + easily be tested because the special unit test implementation of a + LintClient provides a simple way to provide exact responses for + specific URLs:
+ +

lint()
+  .files(...)
+  // Set up exactly the expected maven.google.com network output to
+  // ensure stable version suggestions in the tests
+  .networkData("https://maven.google.com/master-index.xml", ""
+       + "<!--?xml version='1.0' encoding='UTF-8'?-->\n"
+       + "<metadata>\n"
+       + "  <com.android.tools.build>"
+       + "</com.android.tools.build></metadata>")
+  .networkData("https://maven.google.com/com/android/tools/build/group-index.xml", ""
+       + "<!--?xml version='1.0' encoding='UTF-8'?-->\n"
+       + "<com.android.tools.build>\n"
+       + "  <gradle versions="\"2.3.3,3.0.0-alpha1\"/">\n"
+       + "</gradle></com.android.tools.build>")
+.run()
+.expect(...)

+ +And much, much, more. However, most of the implementation of +LintClient is intended for integration of lint itself, and as a check +author you don't need to worry about it. It's the detector API that +matters, and is also less likely to change than the client API. + +

+ +

The division between the two halves is not perfect; some classes + do not fit neatly in between the two or historically were put in + the wrong place, so this is a high level design to be aware of but + which is not absolute.
+ +

+ +Also, + +

+ +

Because of the division between two separate packages, which in + retrospect was a mistake, a number of APIs that are only intended + for internal lint usage have been made public such that lint's + code in one package can access it from the other. There's normally a + comment explaining that this is for internal use only, but be aware + that just because something is public or not final it's a good + idea to call or override it.
+ +

+   

Creating an Issue

+

+ + +For information on how to set up the project and to actually publish +your lint checks, see the sample and +publishing chapters. + +

+ +Issue is a final class, so unlike Detector, you don't subclass +it, you instantiate it via Issue.create. + +

+ +By convention, issues are registered inside the companion object of the +corresponding detector, but that is not required. + +

+ +Here's an example: + +

class SdCardDetector : Detector(), SourceCodeScanner { + companion object Issues { + @JvmField + val ISSUE = Issue.create( + id = "SdCardPath", + briefDescription = "Hardcoded reference to `/sdcard`", + explanation = """ + Your code should not reference the `/sdcard` path directly; \ + instead use `Environment.getExternalStorageDirectory().getPath()`. + + Similarly, do not reference the `/data/data/` path directly; it \ + can vary in multi-user scenarios. Instead, use \ + `Context.getFilesDir().getPath()`. + """, + moreInfo = "https://developer.android.com/training/data-storage#filesExternal", + category = Category.CORRECTNESS, + severity = Severity.WARNING, + androidSpecific = true, + implementation = Implementation( + SdCardDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } + ...

+ +There are a number of things to note here. + +

+ +On line 4, we have the Issue.create() call. We store the issue into a +property such that we can reference this issue both from the +IssueRegistry, where we provide the Issue to lint, and also in the +Detector code where we report incidents of the issue. + +

+ +Note that Issue.create is a method with a lot of parameters (and we +will probably add more parameters in the future). Therefore, it's a +good practice to explicitly include the argument names (and therefore +to implement your code in Kotlin). + +

+ +The Issue provides metadata about a type of problem. + +

+ +The id is a short, unique identifier for this issue. By +convention it is a combination of words, capitalized camel case (though +you can also add your own package prefix as in Java packages). Note +that the id is “user visible”; it is included in text output when lint +runs in the build system, such as this: + +

src/main/kotlin/test/pkg/MyTest.kt:4: Warning: Do not hardcode "/sdcard/";
+      use Environment.getExternalStorageDirectory().getPath() instead [SdCardPath]
+    val s: String = "/sdcard/mydir"
+                     ~~~~~~~~~~~~~
+0 errors, 1 warnings

+ +(Notice the [SdCardPath] suffix at the end of the error message.) + +

+ +The reason the id is made known to the user is that the ID is how +they'll configure and/or suppress issues. For example, to suppress the +warning in the current method, use + +

@Suppress("SdCardPath")

+ +(or in Java, @SuppressWarnings). Note that there is an IDE quickfix to +suppress an incident which will automatically add these annotations, so +you don't need to know the ID in order to be able to suppress an +incident, but the ID will be visible in the annotation that it +generates, so it should be reasonably specific. + +

+ +Also, since the namespace is global, try to avoid picking generic names +that could clash with others, or seem to cover a larger set of issues +than intended. For example, “InvalidDeclaration” would be a poor id +since that can cover a lot of potential problems with declarations +across a number of languages and technologies. + +

+ +Next, we have the briefDescription. You can think of this as a +“category report header“; this is a static description for all +incidents of this type, so it cannot include any specifics. This string +is used for example as a header in HTML reports for all incidents of +this type, and in the IDE, if you open the Inspections UI, the various +issues are listed there using the brief descriptions. + +

+ +The explanation is a multi line, ideally multi-paragraph +explanation of what the problem is. In some cases, the problem is self +evident, as in the case of ”Unused declaration“, but in many cases, the +issue is more subtle and might require additional explanation, +particularly for what the developer should do to address the +problem. The explanation is included both in HTML reports and in the +IDE inspection results window. + +

+ +Note that even though we're using a raw string, and even though the +string is indented to be flush with the rest of the issue registration +for better readability, we don't need to call trimIndent() on +the raw string. Lint does that automatically. + +

+ +However, we do need to add line continuations — those are the trailing +\'s at the end of the lines. + +

+ +Note also that we have a Markdown-like simple syntax, described in the +“TextFormat” section below. You can use asterisks for italics or double +asterisks for bold, you can use apostrophes for code font, and so on. +In terminal output this doesn't make a difference, but the IDE, +explanations, incident error messages, etc, are all formatted using +these styles. + +

+ +The category isn't super important; the main use is that category +names can be treated as id's when it comes to issue configuration; for +example, a user can turn off all internationalization issues, or run +lint against only the security related issues. The category is also +used for locating related issues in HTML reports. If none of the +built-in categories are appropriate you can also create your own. + +

+ +The severity property is very important. An issue can be either a +warning or an error. These are treated differently in the IDE (where +errors are red underlines and warnings are yellow highlights), and in +the build system (where errors can optionally break the build and +warnings do not). There are some other severities too; ”fatal“ is like +error except these checks are designated important enough (and have +very few false positives) such that we run them during release builds, +even if the user hasn't explicitly run a lint target. There's also +“informational” severity, which is only used in one or two places, and +finally the “ignore” severity. This is never the severity you register +for an issue, but it's part of the severities a developer can configure +for a particular issue, thereby turning off that particular check. + +

+ +You can also specify a moreInfo URL which will be included in the +issue explanation as a “More Info” link to open to read more details +about this issue or underlying problem. + +

+   

TextFormat

+

+ + +All error messages and issue metadata strings in lint are interpreted +using simple Markdown-like syntax: +

+ + + + + + + +
Raw text format Renders To
This is a `code symbol` This is a code symbol
This is *italics* This is italics
This is **bold** This is bold
http://, https:// http://, https://
\*not italics* \*not italics*
```language\n text\n``` (preformatted text block)
Supported markup in lint's markdown-like raw text format
+ +

+ +This is useful when error messages and issue explanations are shown in +HTML reports generated by Lint, or in the IDE, where for example the +error message tooltips will use formatting. + +

+ +In the API, there is a TextFormat enum which encapsulates the +different text formats, and the above syntax is referred to as +TextFormat.RAW; it can be converted to .TEXT or .HTML for +example, which lint does when writing text reports to the console or +HTML reports to files respectively. As a lint check author you don't +need to know this (though you can for example with the unit testing +support decide which format you want to compare against in your +expected output), but the main point here is that your issue's brief +description, issue explanation, incident report messages etc, should +use the above “raw” syntax. Especially the first conversion; error +messages often refer to class names and method names, and these should +be surrounded by apostrophes. + +

+   

Issue Implementation

+

+ + +The last issue registration property is the implementation. This +is where we glue our metadata to our specific implementation of an +analyzer which can find instances of this issue. + +

+ +Normally, the Implementation provides two things: + +

+ +

    +
  • The .class for our Detector which should be instantiated. In the + code sample above it was SdCardDetector. + +

    + +

  • +
  • The Scope that this issue's detector applies to. In the above + example it was Scope.JAVA_FILE, which means it will apply to Java + and Kotlin files.
+ +

+   

Scopes

+

+ + +The Implementation actually takes a set of scopes; we still refer +to this as a “scope”. Some lint checks want to analyze multiple types +of files. For example, the StringFormatDetector will analyze both the +resource files declaring the formatting strings across various locales, +as well as the Java and Kotlin files containing String.format calls +referencing the formatting strings. + +

+ +There are a number of pre-defined sets of scopes in the Scope +class. Scope.JAVA_FILE_SCOPE is the most common, which is a +singleton set containing exactly Scope.JAVA_FILE, but you +can always create your own, such as for example +

    EnumSet.of(Scope.CLASS_FILE, Scope.JAVA_LIBRARIES)

+ +When a lint issue requires multiple scopes, that means lint will +only run this detector if all the scopes are available in the +running tool. When lint runs a full batch run (such as a Gradle lint +target or a full “Inspect Code“ in the IDE), all scopes are available. + +

+ +However, when lint runs on the fly in the editor, it only has access to +the current file; it won't re-analyze all files in the project for +every few keystrokes. So in this case, the scope in the lint driver +only includes the current source file's type, and only lint checks +which specify a scope that is a subset would run. + +

+ +This is a common mistake for new lint check authors: the lint check +works just fine as a unit test, but they don't see working in the IDE +because the issue implementation requests multiple scopes, and all +have to be available. + +

+ +Often, a lint check looks at multiple source file types to work +correctly in all cases, but it can still identify some problems given +individual source files. In this case, the Implementation constructor +(which takes a vararg of scope sets) can be handed additional sets of +scopes, called ”analysis scopes“. If the current lint client's scope +matches or is a subset of any of the analysis scopes, then the check +will run after all. + +

+   

Registering the Issue

+

+ + +Once you've created your issue, you need to provide it from +an IssueRegistry. + +

+ +Here's an example IssueRegistry: + +

package com.example.lint.checks + +import com.android.tools.lint.client.api.IssueRegistry +import com.android.tools.lint.client.api.Vendor +import com.android.tools.lint.detector.api.CURRENT_API + +class SampleIssueRegistry : IssueRegistry() { + override val issues = listOf(SdCardDetector.ISSUE) + + override val api: Int + get() = CURRENT_API + + // works with Studio 4.1 or later; see + // com.android.tools.lint.detector.api.Api / ApiKt + override val minApi: Int + get() = 8 + + // Requires lint API 30.0+; if you're still building for something + // older, just remove this property. + override val vendor: Vendor = Vendor( + vendorName = "Android Open Source Project", + feedbackUrl = "https://com.example.lint.blah.blah/", + contact = "author@com.example.lint" + ) +}

+ +On line 8, we're returning our issue. It's a list, so an +IssueRegistry can provide multiple issues. + +

+ +The api property should be written exactly like the way it +appears above in your own issue registry as well; this will record +which version of the lint API this issue registry was compiled against +(because this references a static final constant which will be copied +into the jar file instead of looked up dynamically when the jar is +loaded). + +

+ +The minApi property records the oldest lint API level this check +has been tested with. + +

+ +Both of these are used at issue loading time to make sure lint checks +are compatible, but in recent versions of lint (7.0) lint will more +aggressively try to load older detectors even if they have been +compiled against older APIs since there's a high likelihood that they +will work (it checks all the lint APIs in the bytecode and uses +reflection to verify that they're still there). + +

+ +The vendor property is new as of 7.0, and gives lint authors a +way to indicate where the lint check came from. When users use lint, +they're running hundreds and hundreds of checks, and sometimes it's not +clear who to contact with requests or bug reports. When a vendor has +been specified, lint will include this information in error output and +reports. + +

+ +The last step towards making the lint check available is to make +the IssueRegistry known via the service loader mechanism. + +

+ +Create a file named exactly +

src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry

+ +with the following contents (but where you substitute in your own +fully qualified class name for your issue registry): + +

com.example.lint.checks.SampleIssueRegistry

+ +If you're not building your lint check using Gradle, you may not want +the src/main/resources prefix; the point is that your packaging of +the jar file should contain META-INF/services/ at the root of the jar +file. + +

+   

Implementing a Detector: Scanners

+

+ + +We've finally come to the main task with writing a lint check: +implementing the Detector. + +

+ +Here's a trivial one: +

class MyDetector : Detector() { + override fun run(context: Context) { + context.report(ISSUE, Location.create(context.file), + "I complain a lot") + } +}

+ +This will just complain in every single file. Obviously, no real lint +detector does this; we want to do some analysis and conditionally +report incidents. + +

+ +In order to make it simpler to perform analysis, Lint has dedicated +support for analyzing various file types. The way this works is that +you register interest, and then various callbacks will be invoked. + +

+ +For example: + +

+ +

    +
  • When implementing XmlScanner, in an XML element you can be + called back +
      +
    • when any of a set of given tags are declared (visitElement) +
    • +
    • when any of a set of named attributes are declared + (visitAttribute) +
    • +
    • and you can perform your own document traversal via visitDocument + +

      + +

    +
  • When implementing SourceCodeScanner, in Kotlin and Java files + you can be called back +
      +
    • When a method of a given name is invoked (getApplicableMethodNames + and visitMethodCall) +
    • +
    • When a class of the given type is instantiated + (getApplicableConstructorTypes and visitConstructor) +
    • +
    • When a new class is declared which extends (possibly indirectly) + a given class or interface (applicableSuperClasses and + visitClass) +
    • +
    • When annotated elements are referenced or combined + (applicableAnnotations and visitAnnotationUsage) +
    • +
    • When any AST nodes of given types appear (getApplicableUastTypes + and createUastHandler) + +

      + +

    +
  • When implementing a ClassScanner, in .class and .jar files + you can be called back +
      +
    • when a method is invoked for a particular owner + (getApplicableCallOwners and checkCall +
    • +
    • when a given bytecode instruction occurs + (getApplicableAsmNodeTypes and checkInstruction) +
    • +
    • like with XmlScanner's visitDocument, you can perform your own + ASM bytecode iteration via checkClass. + +

      + +

    +
  • There are various other scanners too, for example GradleScanner + which lets you visit build.gradle and build.gradle.kts DSL + closures, BinaryFileScanner which visits resource files such as + webp and png files, and OtherFileScanner which lets you visit + unknown files.
+ +

+ +

Note that Detector already implements empty stub methods for all + of these interfaces, so if you for example implement + SourceFileScanner in your detector, you don't need to go and add + empty implementations for all the methods you aren't using.
+ +

+ +

None of Lint's APIs require you to call super when you override + methods; methods meant to be overridden are always empty so the + super-call is superfluous.
+ +

+   

Detector Lifecycle

+

+ + +Detector registration is done by detector class, not by detector +instance. Lint will instantiate detectors on your behalf. It will +instantiate the detector once per analysis, so you can stash state on +the detector in fields and accumulate information for analysis at the +end. + +

+ +There are some callbacks both before each individual file is analyzed +(beforeCheckFile and afterCheckFile), as well as before and after +analysis of all the modules (beforeCheckRootProject and +afterCheckRootProject). + +

+ +This is for example how the ”unused resources“ check works: we store +all the resource declarations and resource references we find in the +project as we process each file, and then in the +afterCheckRootProject method we analyze the resource graph and +compute any resource declarations that are not reachable in the +reference graph, and then we report each of these as unused. + +

+   

Scanner Order

+

+ + +Some lint checks involve multiple scanners. This is pretty common in +Android, where we want to cross check consistency between data in +resource files with the code usages. For example, the String.format +check makes sure that the arguments passed to String.format match the +formatting strings specified in all the translation XML files. + +

+ +Lint defines an exact order in which it processes scanners, and within +scanners, data. This makes it possible to write some detectors more +easily because you know that you'll encounter one type of data before +the other; you don't have to handle the opposite order. For example, in +our String.format example, we know that we'll always see the +formatting strings before we see the code with String.format calls, +so we can stash the formatting strings in a map, and when we process +the formatting calls in code, we can immediately issue reports; we +don't have to worry about encountering a formatting call for a +formatting string we haven't processed yet. + +

+ +Here's lint's defined order: + +

+ +

    +
  1. Android Manifest +
  2. +
  3. Android resources XML files (alphabetical by folder type, so for + example layouts are processed before value files like translations) +
  4. +
  5. Kotlin and Java files +
  6. +
  7. Bytecode (local .class files and library .jar files) +
  8. +
  9. Gradle files +
  10. +
  11. Other files +
  12. +
  13. ProGuard files +
  14. +
  15. Property Files
+ +

+ +Similarly, lint will always process libraries before the modules +that depend on them. + +

+ +

If you need to access something from later in the iteration order, + and it's not practical to store all the current data and instead + handle it when the later data is encountered, note that lint has + support for ”multi-pass analysis“: it can run multiple times over + the data. The way you invoke this is via + context.driver.requestRepeat(this, …). This is actually how the + unused resource analysis works. Note however that this repeat is + only valid within the current module; you can't re-run the analysis + through the whole dependency graph.
+ +

+   

Implementing a Detector: Services

+

+ + +In addition to the scanners, lint provides a number of services +to make implementation simpler. These include + +

+ +

    +
  • ConstantEvaluator: Performs evaluation of AST expressions, so + for example if we have the statements x = 5; y = 2 * x, the + constant evaluator can tell you that y is 10. This constant evaluator + can also be more permissive than a compiler's strict constant + evaluator; e.g. it can return concatenated strings where not all + parts are known, or it can use non-final initial values of fields. + This can help you find possible bugs instead of certain bugs. + +

    + +

  • +
  • TypeEvaluator: Attempts to provide the concrete type of an + expression. For example, for the Java statements Object s = new + StringBuilder(); Object o = s, the type evaluator can tell you that + the type of o at this point is really StringBuilder. + +

    + +

  • +
  • JavaEvaluator: Despite the unfortunate older name, this service + applies to both Kotlin and Java, and can for example provide + information about inheritance hierarchies, class lookup from fully + qualified names, etc. + +

    + +

  • +
  • DataFlowAnalyzer: Data flow analysis within a method. + +

    + +

  • +
  • For Android analysis, there are several other important services, + like the ResourceRepository and the ResourceEvaluator. + +

    + +

  • +
  • Finally, there are a number of utility methods; for example there is + an editDistance method used to find likely typos used by a number + of checks.
+ +

+   

Scanner Example

+

+ + +Let's create a Detector using one of the above scanners, +XmlScanner, which will look at all the XML files in the project and +if it encounters a <bitmap> tag it will report that <vector> should +be used instead: + +

import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.XmlScanner +import com.android.tools.lint.detector.api.Location +import com.android.tools.lint.detector.api.XmlContext +import org.w3c.dom.Element + +class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + val incident = Incident(context, ISSUE) + .message( "Use `<vector>` instead of `<bitmap>`") + .at(element) + context.report(incident)) + } +}

+ +The above is using the new Incident API from Lint 7.0 and on; in +older versions you can use the following API, which still works in 7.0: + +

class MyDetector : Detector(), XmlScanner { + override fun getApplicableElements() = listOf("bitmap") + + override fun visitElement(context: XmlContext, element: Element) { + context.report(ISSUE, context.getLocation(element), + "Use `<vector>` instead of `<bitmap>`") + } +}

+ +The second, older form, may seem simpler, but the new API allows a lot +more metadata to be attached to the report, such as an override +severity. You don't have to convert to the builder syntax to do this; +you could also have written the second form as + +

context.report(Incident(ISSUE, context.getLocation(element), + "Use `<vector>` instead of `<bitmap>`"))
+   

Analyzing Kotlin and Java Code

+ +   

UAST

+

+ + +To analyze Kotlin and Java code, lint offers an abstract syntax tree, +or ”AST“, for the code. + +

+ +This AST is called ”UAST“, for ”Universal Abstract Syntax Tree“, which +represents multiple languages in the same way, hiding the language +specific details like whether there is a semicolon at the end of the +statements or whether the way an annotation class is declared is as +@interface or annotation class, and so on. + +

+ +This makes it possible to write a single analyzer which works +(”universally“) across all languages supported by UAST. And this is +very useful; most lint checks are doing something API or data-flow +specific, not something language specific. If however you do need to +implement something very language specific, see the next section, +“PSI”. + +

+ +In UAST, each element is called a UElement, and there are a +number of subclasses — UFile for the compilation unit, UClass for +a class, UMethod for a method, UExpression for an expression, +UIfExpression for an if-expression, and so on. + +

+ +Here's a visualization of an AST in UAST for two equivalent programs +written in Kotlin and Java. These programs both result in the same +AST, shown on the right: a UFile compilation unit, containing +a UClass named MyTest, containing UField named s which has +an initializer setting the initial value to hello. + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +MyTest.kt:UAST:packagetest.pkgUFileclassMyTest{privatevals=hello}UClassMyTestMyTest.java:packagetest.pkg;UFieldspublicclassMyTest{privateStrings=hello;}UIdentifiersULiteralExpressionhello + +

+ +

The name “UAST” is a bit misleading; it is not some sort of superset + of all possible syntax trees; instead, think of this as the “Java + view” of all code. So, for example, there isn’t a UProperty node + which represents Kotlin properties. Instead, the AST will look the + same as if the property had been implemented in Java: it will + contain a private field and a public getter and a public setter + (unless of course the Kotlin property specifies a private setter). + If you’ve written code in Kotlin and have tried to access that + Kotlin code from a Java file you will see the same thing — the + “Java view” of Kotlin. The next section, “PSI“, will discuss how to + do more language specific analysis.
+ +

+   

UAST Example

+

+ + +Here's an example (from the built-in AlarmDetector for Android) which +shows all of the above in practice; this is a lint check which makes +sure that if anyone calls AlarmManager.setRepeating, the second +argument is at least 5,000 and the third argument is at least 60,000. + +

+ +Line 1 says we want to have line 3 called whenever lint comes across a +method to setRepeating. + +

+ +On lines 8-4 we make sure we're talking about the correct method on the +correct class with the correct signature. This uses the JavaEvaluator +to check that the called method is a member of the named class. This is +necessary because the callback would also be invoked if lint came +across a method call like Unrelated.setRepeating; the +visitMethodCall callback only matches by name, not receiver. + +

+ +On line 36 we use the ConstantEvaluator to compute the value of each +argument passed in. This will let this lint check not only handle cases +where you're specifying a specific value directly in the argument list, +but also for example referencing a constant from elsewhere. + +

override fun getApplicableMethodNames(): List<string> = listOf("setRepeating") + +override fun visitMethodCall( + context: JavaContext, + node: UCallExpression, + method: PsiMethod +) { + val evaluator = context.evaluator + if (evaluator.isMemberInClass(method, "android.app.AlarmManager") && + evaluator.getParameterCount(method) == 4 + ) { + ensureAtLeast(context, node, 1, 5000L) + ensureAtLeast(context, node, 2, 60000L) + } +} + +private fun ensureAtLeast( + context: JavaContext, + node: UCallExpression, + parameter: Int, + min: Long +) { + val argument = node.valueArguments[parameter] + val value = getLongValue(context, argument) + if (value < min) { + val message = "Value will be forced up to $min as of Android 5.1; " + + "don't rely on this to be exact" + context.report(ISSUE, argument, context.getLocation(argument), message) + } +} + +private fun getLongValue( + context: JavaContext, + argument: UExpression +): Long { + val value = ConstantEvaluator.evaluate(context, argument) + if (value is Number) { + return value.toLong() + } + + return java.lang.Long.MAX_VALUE +}
+   

Looking up UAST

+

+ + +To write your detector's analysis, you need to know what the AST for +your code of interest looks like. Instead of trying to figure it out by +examining the elements under a debugger, a simple way to find out is to +”pretty print“ it, using the UElement extension method +asRecursiveLogString. + +

+ +For example, given the following unit test: + +

lint().files(
+       kotlin(""
+               + "package test.pkg\n"
+               + "\n"
+               + "class MyTest {\n"
+               + "    val s: String = \"hello\"\n"
+               + "}\n"), ...

+ +If you evaluate context.uastFile?.asRecursiveLogString() from +one of the callbacks, it will print this: + +

UFile (package = test.pkg)
+    UClass (name = MyTest)
+        UField (name = s)
+            UAnnotation (fqName = org.jetbrains.annotations.NotNull)
+            ULiteralExpression (value = "hello")
+        UAnnotationMethod (name = getS)
+        UAnnotationMethod (name = MyTest)

+ +(This also illustrates the earlier point about UAST representing the +Java view of the code; here the read-only public Kotlin property ”s“ is +represented by both a private field s and a public getter method, +getS().) + +

+   

Resolving

+

+ + +When you have a method call, or a field reference, you may want to take +a look at the called method or field. This is called ”resolving“, and +UAST supports it directly; on a UCallExpression for example, call +.resolve(), which returns a PsiMethod, which is like a UMethod, +but may not represent a method we have source for (which for example +would be the case if you resolve a reference to the JDK or to a library +we do not have sources for). You can call .toUElement() on the +PSI element to try to convert it to UAST if source is available. + +

+ +

Resolving only works if lint has a correct classpath such that the + referenced method, field or class are actually present. If it is + not, resolve will return null, and various lint callbacks will not + be invoked. This is a common source of questions for lint checks + ”not working“; it frequently comes up in lint unit tests where a + test file will reference some API that isn't actually included in + the class path. The recommended approach for this is to declare + local stubs. See the unit testing chapter + for more details about this.
+ +

+   

PSI

+

+ + +PSI is short for ”Program Structure Interface“, and is IntelliJ's AST +abstraction used for all language modeling in the IDE. + +

+ +Note that there is a different PSI representation for each +language. Java and Kotlin have completely different PSI classes +involved. This means that writing a lint check using PSI would involve +writing a lot of logic twice; once for Java, and once for Kotlin. (And +the Kotlin PSI is a bit trickier to work with.) + +

+ +That's what UAST is for: there's a ”bridge“ from the Java PSI to UAST +and there's a bridge from the Kotlin PSI to UAST, and your lint check +just analyzes UAST. + +

+ +However, there are a few scenarios where we have to use PSI. + +

+ +The first, and most common one, is listed in the previous section on +resolving. UAST does not completely replace PSI; in fact, PSI leaks +through in part of the UAST API surface. For example, +UMethod.resolve() returns a PsiMethod. And more importantly, +UMethod extends PsiMethod. + +

+ +

For historical reasons, PsiMethod and other PSI classes contain + some unfortunate APIs that only work for Java, such as asking for + the method body. Because UMethod extends PsiMethod, you might be + tempted to call getBody() on it, but this will return null from + Kotlin. If your unit tests for your lint check only have test cases + written in Java, you may not realize that your check is doing the + wrong thing and won't work on Kotlin code. It should call uastBody + on the UMethod instead. Lint's special detector for lint detectors + looks for this and a few other scenarios (such as calling parent + instead of uastParent), so be sure to configure it for your + project.
+ +

+ +When you are dealing with ”signatures“ — looking at classes and +class inheritance, methods, parameters and so on — using PSI is +fine — and unavoidable since UAST does not represent bytecode +(though in the future it potentially could, via a decompiler) +or any other JVM languages than Kotlin and Java. + +

+ +However, if you are looking at anything inside a method or class +or field initializer, you must use UAST. + +

+ +The second scenario where you may need to use PSI is where you have +to do something language specific which is not represented in UAST. For +example, if you are trying to look up the names or default values of a +parameter, or whether a given class is a companion object, then you'll +need to dip into Kotlin PSI. + +

+ +There is usually no need to look at Java PSI since UAST fully covers +it, unless you want to look at individual details like specific +whitespace between AST nodes, which is represented in PSI but not UAST. + +

+   

Testing

+

+ + +Writing unit tests for the lint check is important, and this is covered +in detail in the dedicated unit testing +chapter. + +

+ + + +

+   

Example: Sample Lint Check GitHub Project

+

+ + +The https://github.com/googlesamples/android-custom-lint-rules +GitHub project provides a sample lint check which shows a working +skeleton. + +

+ +This chapter walks through that sample project and explains +what and why. + +

+   

Project Layout

+

+ + +Here's the project layout of the sample project: + +

+ + + + + + + + + + + + + + + + + + +implementationlintPublish:app:library:checks + +

+ +We have an application module, app, which depends (via an +implementation dependency) on a library, and the library itself has +a lintPublish dependency on the checks project. + +

+   

:checks

+

+ + +The checks project is where the actual lint checks are implemented. +This project is a plain Kotlin or plain Java Gradle project: + +

apply plugin: 'java-library'
+apply plugin: 'kotlin'

+ + + +

If you look at the sample project, you'll see a third plugin + applied: apply plugin: 'com.android.lint'. This pulls in the + standalone Lint Gradle plugin, which adds a lint target to this + Kotlin project. This means that you can run ./gradlew lint on the + :checks project too. This is useful because lint ships with a + dozen lint checks that look for mistakes in lint detectors! This + includes warnings about using the wrong UAST methods, invalid id + formats, words in messages which look like code which should + probably be surrounded by apostrophes, etc.
+ +

+ +The Gradle file also declares the dependencies on lint APIs +that our detector needs: + +

dependencies { + compileOnly "com.android.tools.lint:lint-api:$lintVersion" + compileOnly "com.android.tools.lint:lint-checks:$lintVersion" + testImplementation "com.android.tools.lint:lint-tests:$lintVersion" +}

+ +The second dependency is usually not necessary; you just need to depend +on the Lint API. However, the built-in checks define a lot of +additional infrastructure which it's sometimes convenient to depend on, +such as ApiLookup which lets you look up the required API level for a +given method, and so on. Don't add the dependency until you need it. + +

+   

lintVersion?

+

+ + +What is the lintVersion variable defined above? + +

+ +Here's the top level build.gradle +

buildscript { + ext { + kotlinVersion = '1.4.31' + + // Current lint target: Studio 4.2 / AGP 7 + //gradlePluginVersion = '4.2.0-beta06' + //lintVersion = '27.2.0-beta06' + + // Upcoming lint target: Arctic Fox / AGP 7 + gradlePluginVersion = '7.0.0-alpha10' + lintVersion = '30.0.0-alpha10' + } + + repositories { + google() + mavenCentral() + } + dependencies { + classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +}

+ +The $lintVersion variable is defined on line 11. We don't technically +need to define the $gradlePluginVersion here or add it to the classpath on line 19, but that's done so that we can add the lint +plugin on the checks themselves, as well as for the other modules, +:app and :library, which do need it. + +

+ +When you build lint checks, you're compiling against the Lint APIs +distributed on maven.google.com (which is referenced via google() in +Gradle files). These follow the Gradle plugin version numbers. + +

+ +Therefore, you first pick which of lint's API you'd like to compile +against. You should use the latest available if possible. + +

+ +Once you know the Gradle plugin version number, say 4.2.0-beta06, you +can compute the lint version number by simply adding 23 to the +major version of the gradle plugin, and leave everything the same: + +

+ +lintVersion = gradlePluginVersion + 23.0.0 + +

+ +For example, 7 + 23 = 30, so AGP version 7.something corresponds to +Lint version 30.something. As another example; as of this writing the +current stable version of AGP is 4.1.2, so the corresponding version of +the Lint API is 27.1.2. + +

+ +

Why this arbitrary numbering — why can't lint just use the same + numbers? This is historical; lint (and various other sibling + libraries that lint depends on) was released earlier than the Gradle + plugin; it was up to version 22 or so. When we then shipped the + initial version of the Gradle plugin with Android Studio 1.0, we + wanted to start the numbering over from “1” for this brand new + artifact. However, some of the other libraries, like lint, couldn't + just start over at 1, so we continued incrementing their versions in + lockstep. Most users don't see this, but it's a wrinkle users of the + Lint API have to be aware of.
+ +

+   

:library and :app

+

+ + +The library project depends on the lint check project, and will +package the lint checks as part of its payload. The app project +then depends on the library, and has some code which triggers +the lint check. This is there to demonstrate how lint checks can +be published and consumed, and this is described in detail in the +Publishing a Lint Check chapter. + +

+   

Lint Check Project Layout

+

+ + +The lint checks source project is very simple + +

checks/build.gradle
+checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
+checks/src/main/java/com/example/lint/checks/SampleIssueRegistry.kt
+checks/src/main/java/com/example/lint/checks/SampleCodeDetector.kt
+checks/src/test/java/com/example/lint/checks/SampleCodeDetectorTest.kt

+ +First is the build file, which we've discussed above. + +

+   

Service Registration

+

+ + +Then there's the service registration file. Notice how this file is in +the source set src/main/resources/, which means that Gradle will +treat it as a resource and will package it into the output jar, in the +META-INF/services folder. This is using the service-provider loading facility in the JDK to register a service lint can look up. The +key is the fully qualified name for lint's IssueRegistry class. +And the contents of that file is a single line, the fully +qualified name of the issue registry: + +

$ cat checks/src/main/resources/META-INF/services/com.android.tools.lint.client.api.IssueRegistry
+com.example.lint.checks.SampleIssueRegistry

+ +(The service loader mechanism is understood by IntelliJ, so it will +correctly update the service file contents if the issue registry is +renamed etc.) + +

+ +The service registration can contain more than one issue registry, +though there's usually no good reason for that, since a single issue +registry can provide multiple issues. + +

+   

IssueRegistry

+

+ + +Next we have the IssueRegistry linked from the service registration. +Lint will instantiate this class and ask it to provide a list of +issues. These are then merged with lint's other issues when lint +performs its analysis. + +

+ +In its simplest form we'd only need to have the following code +in that file: + +

package com.example.lint.checks
+import com.android.tools.lint.client.api.IssueRegistry
+class SampleIssueRegistry : IssueRegistry() {
+    override val issues = listOf(SampleCodeDetector.ISSUE)
+}

+ +However, we're also providing some additional metadata about these lint +checks, such as the Vendor, which contains information about the +author and (optionally) contact address or bug tracker information, +displayed to users when an incident is found. + +

+ +We also provide some information about which version of lint's API the +check was compiled against, and the lowest version of the lint API that +this lint check has been tested with. (Note that the API versions are +not identical to the versions of lint itself; the idea and hope is that +the API may evolve at a slower pace than updates to lint delivering new +functionality). + +

+   

Detector

+

+ + +The IssueRegistry references the SampleCodeDetector.ISSUE, +so let's take a look at SampleCodeDetector: + +

class SampleCodeDetector : Detector(), UastScanner { + + // ... + + companion object { + /** + * Issue describing the problem and pointing to the detector + * implementation. + */ + @JvmField + val ISSUE: Issue = Issue.create( + // ID: used in @SuppressLint warnings etc + id = "ShortUniqueId", + // Title -- shown in the IDE's preference dialog, as category headers in the + // Analysis results window, etc + briefDescription = "Lint Mentions", + // Full explanation of the issue; you can use some markdown markup such as + // `monospace`, *italic*, and **bold**. + explanation = """ + This check highlights string literals in code which mentions the word `lint`. \ + Blah blah blah. + + Another paragraph here. + """, + category = Category.CORRECTNESS, + priority = 6, + severity = Severity.WARNING, + implementation = Implementation( + SampleCodeDetector::class.java, + Scope.JAVA_FILE_SCOPE + ) + ) + } +}

+ +The Issue registration is pretty self-explanatory, and the details +about issue registration are covered in the basics +chapter. The excessive comments here are there to explain the sample, +and there are usually no comments in issue registration code like this. + +

+ +Note how on line 29, the Issue registration names the Detector +class responsible for analyzing this issue: SampleCodeDetector. In +the above I deleted the body of that class; here it is now without the +issue registration at the end: + +

package com.example.lint.checks + +import com.android.tools.lint.client.api.UElementHandler +import com.android.tools.lint.detector.api.Category +import com.android.tools.lint.detector.api.Detector +import com.android.tools.lint.detector.api.Detector.UastScanner +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.Scope +import com.android.tools.lint.detector.api.Severity +import org.jetbrains.uast.UElement +import org.jetbrains.uast.ULiteralExpression +import org.jetbrains.uast.evaluateString + +class SampleCodeDetector : Detector(), UastScanner { + override fun getApplicableUastTypes(): List<class<out uelement?="">> { + return listOf(ULiteralExpression::class.java) + } + + override fun createUastHandler(context: JavaContext): UElementHandler { + return object : UElementHandler() { + override fun visitLiteralExpression(node: ULiteralExpression) { + val string = node.evaluateString() ?: return + if (string.contains("lint") && string.matches(Regex(".*\\blint\\b.*"))) { + context.report( + ISSUE, node, context.getLocation(node), + "This code mentions `lint`: **Congratulations**" + ) + } + } + } + } +}

+ +This lint check is very simple; for Kotlin and Java files, it visits +all the literal strings, and if the string contains the word “lint”, +then it issues a warning. + +

+ +This is using a very general mechanism of AST analysis; specifying the +relevant node types (literal expressions, on line 18) and visiting them +on line 23. Lint has a large number of convenience APIs for doing +higher level things, such as “call this callback when somebody extends +this class”, or “when somebody calls a method named ”foo“, and so on. +Explore the SourceCodeScanner and other Detector interfaces to see +what's possible. We'll hopefully also add more dedicated documentation +for this. + +

+   

Detector Test

+

+ + +Last but not least, let's not forget the unit test: + +

package com.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test + +class SampleCodeDetectorTest { + @Test + fun testBasic() { + lint().files( + java( + """ + package test.pkg; + public class TestClass1 { + // In a comment, mentioning "lint" has no effect + private static String s1 = "Ignore non-word usages: linting"; + private static String s2 = "Let's say it: lint"; + } + """ + ).indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId] + private static String s2 = "Let's say it: lint"; + ∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼ + 0 errors, 1 warnings + """ + ) + } +}

+ +As you can see, writing a lint unit test is very simple, because +lint ships with a dedicated testing library; this is what the + +

    testImplementation "com.android.tools.lint:lint-tests:$lintVersion"

+ +dependency in build.gradle pulled in. + +

+ +Unit testing lint checks is covered in depth in the +unit testing chapter, so we'll cut the +explanation of the above test short here. + +

+ + + +

+   

Publishing a Lint Check

+

+ + +Lint will look for jar files with a service registry key for issue +registries. + +

+ +You can manually point it to your custom lint checks jar files by using +the environment variable ANDROID_LINT_JARS: + +

$ export ANDROID_LINT_JARS=/path/to/first.jar:/path/to/second.jar

+(On Windows, use ; instead of : as the path separator) + +

+ +However, that is only intended for development and as a workaround for +build systems that do not have direct support for lint or embedded lint +libraries, such as the internal Google build system. + +

+   

Android

+ +   

AAR Support

+

+ + +Android libraries are shipped as .aar files instead of .jar files. +This means that they can carry more than just the code payload. Under +the hood, .aar files are just zip files which contain many other +nested files, including api and implementation jars, resources, +proguard/r8 rules, and yes, lint jars. + +

+ +For example, if we look at the contents of the timber logging library's +AAR file, we can see the lint.jar with several lint checks within as +part of the payload: + +

$ jar tvf ~/.gradle/caches/.../jakewharton.timber/timber/4.5.1/?/timber-4.5.1.aar
+   216 Fri Jan 20 14:45:28 PST 2017 AndroidManifest.xml
+  8533 Fri Jan 20 14:45:28 PST 2017 classes.jar
+ 10111 Fri Jan 20 14:45:28 PST 2017 lint.jar
+    39 Fri Jan 20 14:45:28 PST 2017 proguard.txt
+     0 Fri Jan 20 14:45:24 PST 2017 aidl/
+     0 Fri Jan 20 14:45:28 PST 2017 assets/
+     0 Fri Jan 20 14:45:28 PST 2017 jni/
+     0 Fri Jan 20 14:45:28 PST 2017 res/
+     0 Fri Jan 20 14:45:28 PST 2017 libs/

+ +The advantage of this approach is that when lint notices that you +depend on a library, and that library contains custom lint checks, then +lint will pull in those checks and apply them. This gives library +authors a way to provide their own additional checks enforcing usage. + +

+   

lintPublish Configuration

+

+ + +The Android Gradle library plugin provides some special configurations, +lintConfig and lintPublish. + +

+ +The lintPublish configuration lets you reference another project, and +it will take that project's output jar and package it as a lint.jar +inside the AAR file. + +

+ +The https://github.com/googlesamples/android-custom-lint-rules +sample project demonstrates this setup. + +

+ +The :checks project is a pure Kotlin library which depends on the +Lint APIs, implements a Detector, and provides an IssueRegistry +which is linked from META-INF/services. + +

+ +Then in the Android library, the :library project applies the Android +Gradle library plugin. It then specifies a lintPublish configuration +referencing the checks lint project: + +

apply plugin: 'com.android.library'
+dependencies {
+    lintPublish project(':checks')
+    // other dependencies
+}

+ +Finally, the sample :app project is an example of an Android app +which depends on the library, and the source code in the app contains a +violation of the lint check defined in the :checks project. If you +run ./gradlew :app:lint to analyze the app, the build will fail +emitting the custom lint check. + +

+   

Local Checks

+

+ + +What if you aren't publishing a library, but you'd like to apply +some checks locally for your own codebase? + +

+ +You can use a similar approach to lintPublish: In your app +module, specify + +

apply plugin: 'com.android.application'
+dependencies {
+    lintConfig project(':checks')
+    // other dependencies
+}

+ +Now, when lint runs on this application, it will apply the checks +provided from the given project. + +

+ +

This mechanism works well on the CI server for enforcing local code + conventions, and it also works for developers on your team; the + errors should be flagged in the IDE (providing they are analyzing + single-file scopes). However, there have been various bugs and + difficulties around the lint checks getting rebuilt after changes or + clean builds. There are some bugs in the Android Gradle Plugin issue + tracker for this.
+ +

+   

Lint Check Unit Testing

+

+ + +Lint has a dedicated testing library for lint checks. To use it, +add this dependency to your lint check Gradle project: + +

testImplementation "com.android.tools.lint:lint-tests:$lintVersion"

+ +This lends itself nicely to test-driven development. When we get bug +reports of a false positive, we typically start by adding the text for +the repro case, ensure that the test is failing, and then work on the +bug fix (often setting breakpoints and debugging through the unit test) +until it passes. + +

+   

Creating a Unit Test

+

+ + +Here's a sample lint unit test for a simple, sample lint check which +just issues warnings whenever it sees the word “lint” mentioned +in a string: + +

package com.example.lint.checks + +import com.android.tools.lint.checks.infrastructure.TestFiles.java +import com.android.tools.lint.checks.infrastructure.TestLintTask.lint +import org.junit.Test + +class SampleCodeDetectorTest { + @Test + fun testBasic() { + lint().files( + java( + """ + package test.pkg; + public class TestClass1 { + // In a comment, mentioning "lint" has no effect + private static String s1 = "Ignore non-word usages: linting"; + private static String s2 = "Let's say it: lint"; + } + """ + ).indented() + ) + .issues(SampleCodeDetector.ISSUE) + .run() + .expect( + """ + src/test/pkg/TestClass1.java:5: Warning: This code mentions lint: Congratulations [ShortUniqueId] + private static String s2 = "Let's say it: lint"; + ∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼∼ + 0 errors, 1 warnings + """ + ) + } +}

+ +Lint's testing API is a “fluent API”; you chain method calls together, +and the return objects determine what is allowed next. + +

+ +Notice how we construct a test object here on line 10 with the lint() +call. This is a “lint test task”, which has a number of setup methods +on it (such as the set of source files we want to analyze), the issues +it should consider, etc. + +

+ +Then, on line 23, the run() method. This runs the lint unit test, and +then it returns a result object. On the result object we have a number +of methods to verify that the test succeeded. For a test making sure we +don't have false positives, you can just call expectClean(). But the +most common operation is to call expect(output). + +

+ +

Notice how we're including the whole text output here; including not + just the error message and line number, but lint's output of the + relevant line and the error range (using ~~~~ characters). + +

+ + This is the recommended practice for lint checks. It may be tempting + to avoid “duplication” of repeating error messages in the tests + (“DRY”), so some developers have written tests where they just + assert that a given test has say “2 warnings”. But this isn't + testing that the error range is exactly what you expect (which + matters a lot when users are seeing the lint check from the IDE, + since that's the underlined region), and it could also continue to + pass even if the errors flagged are no longer what you intended. + +

+ + Finally, even if the location is correct today, it may not be + correct tomorrow. Several times in the past, some unit tests in + lint's built-in checks have started failing after an update to the + Kotlin compiler because of some changes to the AST which required + tweaks here and there.

+ +

+   

Computing the Expected Output

+

+ + +You may wonder how we knew what to paste into our expect call +to begin with. + +

+ +We didn't. When you write a test, simply start with +expect(""), and run the test. It will fail. You can now +copy the actual output into the expect call as the expected +output, provided of course that it's correct! + +

+   

Test Files

+

+ + +On line 11, we construct a Java test file. We call java(...) and pass +in the source file contents. This constructs a TestFile, and there +are a number of different types of test source files, such as for +Kotlin files, manifest files, icons, property files, and so on. + +

+ +Using test file descriptors like this has a number of advantages +over the traditional approach of checking in test files as sources: + +

+ +

    +
  • Everything is kept together, so it's easier to look at a test and see + what it analyzes and what the expected results are. This is + particularly important for complex lint checks which test a lot of + scenarios. As of this writing, ApiDetectorTest has 157 individual + unit tests. + +

    + +

  • +
  • Lint can provide a DSL to construct test files easily. For example, + projectProperties().compileSdk(17) and + manifest().minSdk(5).targetSdk(17) construct a project.properties + and an AndroidManifest.xml file with the correct contents to + specify for example the right element setting up the + minSdkVersion and targetSdkVersion. + +

    + + For icons, we can construct bitmaps like this:

+ +

        image("res/mipmap-hdpi/my_launcher2_round.png", 50, 50)
+           .fillOval(0, 0, 50, 50, 0xFFFFFFFF)
+           .text(5, 5, "x", 0xFFFFFFFF))

+ + +

    +
  • Similarly, when we construct java() or kotlin() test sources, we + don't have to name the files, because lint will analyze the source + code and figure out what the class file should be named and where to + place it. + +

    + +

  • +
  • We can easily “parameterize” our test files. For example, if you + want to run your unit test against a 100K json file, you can + construct it programmatically; you don't have to check one in. + +

    + +

  • +
  • Since test sources often (deliberately!) have errors in them, this + sometimes causes problems with the tooling; for example, some code + review tools will flag “disallowed” constructs or things like tabs or + trailing spaces, which may be deliberate in a lint unit test. + +

    + +

  • +
  • Lint originally checked in test sources as individual files. + Unfortunately over time, source files ended up getting reused by + multiple tests. And that made it harder to make changes, or figure + out whether test sources are still in use, and so on. + +

    + +

  • +
  • Last but not least, because all the test construction methods + specify the correct mime type for their string parameters, IntelliJ + will actually syntax highlight the test source declarations! Here's + how this looks: + +

    + +

    Screenshot of nested highlighting
+ +

+   

Trimming indents?

+

+ + +Notice how in the above Kotlin unit tests we used raw strings, and +we indented the sources to be flush with the opening “”“ string +delimiter. + +

+ +You might be tempted to call .trimIndent() on the raw string. +However, doing that would break the above nested syntax highlighting +method (or at least it used to). Therefore, instead, call .indented() +on the test file itself, not the string, as shown on line 20. + +

+ +Note that we don't need to do anything with the expect call; lint +will automatically call trimIndent() on the string passed in to it. + +

+   

Dollars in Raw Strings

+

+ + +Kotlin requires that raw strings have to escape the dollar ($) +character. That's normally not a problem, but for some source files, it +makes the source code look really messy and unreadable. + +

+ +For that reason, lint will actually convert $ into $ (a unicode wide +dollar sign). Lint lets you use this character in test sources, and it +always converts the test output to use it (though it will convert in +the opposite direction when creating the test sources on disk). + +

+   

Quickfixes

+

+ + +If your lint check registers quickfixes with the reported incidents, +it's trivial to test these as well. + +

+ +For example, for a lint check result which flags two incidents, with a +single quickfix, the unit test looks like this: + +

lint().files(...)
+    .run()
+    .expect(expected)
+    .expectFixDiffs(
+        ""
+        + "Fix for res/layout/textsize.xml line 10: Replace with sp:\n"
+        + "@@ -11 +11\n"
+        + "-         android:textSize=\"14dp\" />\n"
+        + "+         android:textSize=\"14sp\" />\n"
+        + "Fix for res/layout/textsize.xml line 15: Replace with sp:\n"
+        + "@@ -16 +16\n"
+        + "-         android:textSize=\"14dip\" />\n"
+        + "+         android:textSize=\"14sp\" />\n");

+ +The expectFixDiffs method will iterate over all the incidents it +found, and in succession, apply the fix, diff the two sources, and +append this diff along with the fix message into the log. + +

+ +When there are multiple fixes offered for a single incident, it will +iterate through all of these too: + +

lint().files(...)
+    .run()
+    .expect(expected)
+    .expectFixDiffs(
+        + "Fix for res/layout/autofill.xml line 7: Set autofillHints:\n"
+        + "@@ -12 +12\n"
+        + "          android:layout_width=\"match_parent\"\n"
+        + "          android:layout_height=\"wrap_content\"\n"
+        + "+         android:autofillHints=\"|\"\n"
+        + "          android:hint=\"hint\"\n"
+        + "          android:inputType=\"password\" >\n"
+        + "Fix for res/layout/autofill.xml line 7: Set importantForAutofill=\"no\":\n"
+        + "@@ -13 +13\n"
+        + "          android:layout_height=\"wrap_content\"\n"
+        + "          android:hint=\"hint\"\n"
+        + "+         android:importantForAutofill=\"no\"\n"
+        + "          android:inputType=\"password\" >\n"
+        + "  \n");
+   

Library Dependencies and Stubs

+

+ + +Let's say you're writing a lint check for something like the +AndroidX library's RecyclerView widget. + +

+ +In this case, it's highly likely that your unit test will reference +RecyclerView. But how does lint know what RecyclerView is? If it +doesn't, type resolve won't work, and as a result the detector won't. + +

+ +You could make your test ”depend“ on the recyclerview. This is +possible, using the LibraryReferenceTestFile, but is not recommended. + +

+ +Instead, the recommended approach is to just use ”stubs“; create +skeleton classes which represent only the signatures of the +library, and in particular, only the subset that your lint check +cares about. + +

+ +For example, for lint's own recycler view test, the unit test +declares a field holding the recycler view stub: + +

private val recyclerViewStub = java(
+    """
+    package android.support.v7.widget;
+
+    import android.content.Context;
+    import android.util.AttributeSet;
+    import android.view.View;
+    import java.util.List;
+
+    // Just a stub for lint unit tests
+    public class RecyclerView extends View {
+        public RecyclerView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public abstract static class ViewHolder {
+            public ViewHolder(View itemView) {
+            }
+        }
+
+        public abstract static class Adapter<vh extends="" viewholder=""> {
+            public abstract void onBindViewHolder(VH holder, int position);
+            public void onBindViewHolder(VH holder, int position, List<object> payloads) {
+            }
+            public void notifyDataSetChanged() { }
+        }
+    }
+    """
+).indented()

+ +And now, all the other unit tests simply include recyclerViewStub +as one of the test files. For a larger example, see +this test. + +

+   

Binary and Compiled Source Files

+

+ + +If you need to use binaries in your unit tests, there is +a special test file type for that: base64gzip. Here's an +example from a lint check which tries to recognize usage +of Cordova in the bytecode: + +

fun testVulnerableCordovaVersionInClasses() {
+    lint().files(
+        base64gzip(
+            "bin/classes/org/apache/cordova/Device.class",
+            "" +
+                "yv66vgAAADIAFAoABQAPCAAQCQAEABEHABIHABMBAA5jb3Jkb3ZhVmVyc2lv" +
+                "bgEAEkxqYXZhL2xhbmcvU3RyaW5nOwEABjxpbml0PgEAAygpVgEABENvZGUB" +
+                "AA9MaW5lTnVtYmVyVGFibGUBAAg8Y2xpbml0PgEAClNvdXJjZUZpbGUBAAtE" +
+                "ZXZpY2UuamF2YQwACAAJAQAFMi43LjAMAAYABwEAGW9yZy9hcGFjaGUvY29y" +
+                "ZG92YS9EZXZpY2UBABBqYXZhL2xhbmcvT2JqZWN0ACEABAAFAAAAAQAJAAYA" +
+                "BwAAAAIAAQAIAAkAAQAKAAAAHQABAAEAAAAFKrcAAbEAAAABAAsAAAAGAAEA" +
+                "AAAEAAgADAAJAAEACgAAAB4AAQAAAAAABhICswADsQAAAAEACwAAAAYAAQAA" +
+                "AAUAAQANAAAAAgAO"
+        )`
+    ).run().expect(

+ +Here, ”base64gzip“ means that the file is gzipped and then base64 +encoded. + +

+ +If you want to compute the base64gzip string for a given file, a simple +way to do it is to add this statement at the beginning of your test: + +

assertEquals("", TestFiles.toBase64gzip(File("/tmp/mybinary.bin")))

+ +The test will fail, and now you have your output to copy/paste into the +test. + +

+ +However, if you're writing byte-code based tests, don't just hard code +in the .class file or .jar file contents like this. Lint's own unit +tests did that, and it's hard to later reconstruct what the byte code +was later if you need to make changes or extend it to other bytecode +formats. + +

+ +Instead, use the new compiled or bytecode test files. The key here +is that they automate a bit of the above process: the test file +provides a source test file, as well as a set of corresponding binary +files (since a single source file can create multiple class files, and +for Kotlin, some META-INF data). + +

+ +Initially, you just specify the sources, and when no binary data +has been provided, lint will instead attempt to compile the sources +and emit the full test file registration. + +

+ +This isn't just a convenience; lint's test infrastructure also uses +this to test some additional scenarios (for example, in a multi- module +project it will only provide the binaries, not the sources, for +upstream modules.) + +

+ + + +

+   

Adding Quick Fixes

+ +   

Introduction

+

+ + +When your detector reports an incident, it can also provide one or more +“quick fixes“, which are actions the users can invoke in the IDE (or, +for safe fixes, in batch mode) to address the reported incident. + +

+ +For example, if the lint check reports an unused resource, a quick fix +could offer to remove the unused resource. + +

+ +In some cases, quick fixes can take partial steps towards fixing the +problem, but not fully. For example, the accessibility lint check which +makes sure that for images you set a content description, the quickfix +can offer to add it — but obviously it doesn't know what description +to put. In that case, the lint fix will go ahead and add the attribute +declaration with the correct namespace and attribute name, but will +leave the value up to the user (so it uses a special quick fix provided +by lint to place a TODO marker as the value, along with selecting just +that TODO string such that the user can type to replace without having +to manually delete the TODO string first.) + +

+   

The LintFix builder class

+

+ + +The class in lint which represents a quick fix is LintFix. + +

+ +Note that LintFix is not a class you can subclass and then for +example implement your own arbitrary code in something like a +perform() method. + +

+ +Instead, LintFix has a number of builders where you describe the +action that you would like the quickfix to take. Then, lint will offer +that quickfix in the IDE, and when the user invokes it, lint runs its +own implementation of the various descriptors. + +

+ +The historical reason for this is that many of the quickfixes in lint +depended on machinery in the IDE (such as code and import cleanup after +an edit operation) that isn't available in lint itself, along with +other concepts that only make sense in the IDE, such as moving the +caret, opening files, selecting text, and so on. + +

+ +More recently, this is also used to persist quickfixes properly for +later reuse; this is required for partial +analysis. + +

+   

Creating a LintFix

+

+ + +Lint fixes use a ”fluent API“; you first construct a LintFix, and on +that method you call various available type methods, which will then +further direct you to the allowed options. + +

+ +For example, to create a lint fix to set an XML attribute of a given +name to ”true“, use something like this: + +

LintFix fix = fix().set(null, "singleLine", "true").build()

+ +Here the fix() method is provided by the Detector super class, but +that's just a utility method for LintFix.fix() (or in older versions, +LintFix.create()). + +

+ +There are a number of additional, common methods you can set on +the fix() object: + +

+ +

    +
  • name: Sets the description of the lint fix. This should be brief; + it's in the quickfix popup shown to the user. + +

    + +

  • +
  • sharedName: This sets the ”shared“ or ”family“ name: all fixes in + the file will with the same name can be applied in a single + invocation by the user. For example, if you register 500 ”Remove + unused import“ quickfixes in a file, you don't want to force the user + to have to invoke each and every one. By setting the shared name, the + user will be offered to Fix All $family name problems in the + current file, which they can then perform to have all 500 + individual fixes applied in one go. + +

    + +

  • +
  • autoFix: If you get a lint report and you notice there are a lot of + incidents that lint can fix automatically, you don't want to have to + go and open each and every file and all the fixes in the file. + Therefore, lint can apply the fixes in batch mode; the Gradle + integration has a lintFix target to perform this, and the lint + command has an --apply-suggestions option. + +

    + + However, many quick fixes require user intervention. Not just the + ones where the user has to choose among alternatives, and not just + the ones where the quick fix inserts a placeholder value like TODO. + Take for example lint's built-in check which requires overrides of a + method annotated with @CallSuper to invoke super. on the + overridden method. Where should we insert the call — at the + beginning? At the end? + +

    + + Therefore, lint has the autoFix property you can set on a quickfix. + This indicates that this fix is ”safe“ and can be performed in batch + mode. When the lintFix target runs, it will only apply fixes marked + safe in this way.

+ +

+   

Available Fixes

+

+ + +The current set of available quick fix types are: + +

+ +

    +
  • fix().replace: String replacements. This is the most general + mechanism, and allows you to perform arbitrary edits to the source + code. In addition to the obvious ”replace old string with new“, the + old string can use a different location range than the incident + range, you can match with regular expressions (and perform + replacements on a specific group within the regular expression), and + so on. + +

    + + This fix is also the most straightforward way to delete text. + +

    + + It offers some useful cleanup operations: + +

    + +

      +
    • Source code cleanup, which will run the IDE's code formatter on the + modified source code range. This will apply the user's code + preferences, such as whether there should be a space between a cast + and the expression, and so on. + +

      + +

    • +
    • Import cleanup. That means that if you are referencing a new type, + you don't have to worry about checking whether it is imported and + if not adding an import statement; you can simply write your string + replacements using the fully qualified names, and then tag the + quickfix with the import cleanup option, and when the quickfix is + performed the import will be added if necessary and all the fully + qualified references replaced with simple names. And this will also + correctly handle the scenario where the symbols cannot be replaced + with simple names because there is a conflicting import of the same + name from a different package. + +

      + +

    +
  • fix().annotate: Annotating an element. This will add (or optionally + replace) an annotation on a source element such as a method. It will + also handle import management. + +

    + +

  • +
  • fix().set: Add XML attributes. This will insert an attribute into + the given element, applying the user's code style preferences for + where to insert the attribute. (In Android XML for example there's a + specific sorting convention which is generally alphabetical, except + layout params go before other attributes, and width goes before + height.) + +

    + + You can either set the value to something specific, or place the + caret inside the newly created empty attribute value, or set it + to TODO and select that text for easy type-to-replace.

+ +

+ +

If you use the todo() quickfix, it's a good idea to special case + your lint check to deliberately not accept ”TODO“ as a valid value. + For example, for lint's accessibility check which makes sure you set + a content description, it will complain both when you haven't set + the content description attribute, and if the text is set to + ”TODO“. That way, if the user applies the quickfix, which creates + the attribute in the right place and moves the focus to the right + place, the editor is still showing a warning that the content + description should be set.
+ +

+ +

    +
  • fix().unset: Remove XML attribute. This is a special case of add + attribute. + +

    + +

  • +
  • fix().url: Show URL. In some cases, you can't ”fix“ or do anything + local to address the problem, but you really want to direct the + user's attention to additional documentation. In that case, you can + attach a ”show this URL“ quick fix to the incident which will open + the browser with the given URL when invoked. For example, in a + complicated deprecation where you want users to migrate from one + approach to a completely different one that you cannot automate, you + could use something like this:
+ +

val message = "Job scheduling with `GcmNetworkManager` is deprecated: Use AndroidX `WorkManager` instead"
+val fix = fix()
+.url(http://github.com/%3Cspan%20class=%22hljs-string%22%3E%22https://developer.android.com/topic/libraries/architecture/workmanager/migrating-gcm%22%3C/span%3E)
+.build()
+   

Combining Fixes

+

+ + +You might notice that lint's APIs to report incidents only takes a +single quick fix instead of a list of fixes. + +

+ +But let's say that it did take a list of quick fixes. + +

+ +

    +
  • Should they all be performed as a single unit? That makes sense if + you're trying to write a quickfix which performs multiple string + replacements. + +

    + +

  • +
  • Or should they be offered as separate alternatives for the user to + choose between? That makes sense if the incident says for example + that you must set at least one attribute among three possibilities; + in this case we may want to add quickfixes for setting each attribute.
+ +

+ +Both scenarios have their uses, so lint makes this explicit: + +

+ +

    +
  • fix().composite: create a ”composite“ fix, which composes the fix + out of multiple individual fixes, or + +

    + +

  • +
  • fix().alternatives: create an ”alternatives“ fix, which holds a + number of individual fixes, which lint will present as separate + options to the user.
+ +

+ +Here's an example of how to create a composite fix, which will be +performed as a unit; here we're both setting a new attribute and +deleting a previous attribute: + +

val fix = fix().name("Replace with singleLine=\"true\"")
+    .composite(
+        fix().set(ANDROID_URI, "singleLine", "true").build(),
+        fix().unset(namespace, oldAttributeName).build()
+    )

+ +And here's an example of how to create an alternatives fix, which are +offered to the user as separate options; this is from our earlier +example of the accessibility check which requires you to set a content +description, which can be set either on the ”text“ attribute or the +“contentDescription” attribute: + +

val fix = fix().alternatives(
+    fix().set().todo(ANDROID_URI, "text").build(),
+    fix().set().todo(ANDROID_URI, "contentDescription")
+    .build())
+   

Refactoring Java and Kotlin code

+

+ + +It would be nice if there was an AST manipulation API, similar to UAST +for visiting ASTs, that quickfixes could use to implement refactorings, +but we don't have a library like that. And it's unlikely it would work +well; when you rewrite the user's code you typically have to take +language specific conventions into account. + +

+ +Therefore, today, when you create quickfixes for Kotlin and Java code, +if the quickfix isn't something simple which would work for both +languages, then you need to conditionally create either the Kotlin +version or the Java version of the quickfix based on whether the source +file it applies to is in Kotlin or Java. (For an easy way to check you +can use the isKotlin or isJava package level methods in +com.android.tools.lint.detector.api.) + +

+ +However, it's often the case that the quickfix is something simple +which would work for both; that's true for most of the built-in lint +checks with quickfixes for Kotlin and Java. + +

+   

Regular Expressions and Back References

+

+ + +The replace string quick fix allows you to match the text to +with regular expressions. + +

+ +You can also use back references in the regular expression such +that the quick fix replacement text includes portions from the +original string. + +

+ +Here's an example from lint's AssertDetector: + +

val fix = fix().name("Surround with desiredAssertionStatus() check") + .replace() + .range(context.getLocation(assertCall)) + .pattern("(.*)") + .with("if (javaClass.desiredAssertionStatus()) { \\k<1> }") + .reformat(true) + .build()

+ +The replacement string's back reference above, on line 5, is \k<1>. If +there were multiple regular expression groups in the replacement +string, this could have been \k<2>, \k<3>, and so on. + +

+ +Here's how this looks when applied, from its unit test: + +

lint().files().run().expectFixDiffs(
+    """
+    Fix for src/test/pkg/AssertTest.kt line 18: Surround with desiredAssertionStatus() check:
+    @@ -18 +18
+    -         assert(expensive()) // WARN
+    +         if (javaClass.desiredAssertionStatus()) { assert(expensive()) } // WARN
+    """
+)
+   

Emitting quick fix XML to apply on CI

+

+ + +Note that the lint has an option (--describe-suggestions) to emit +an XML file which describes all the edits to perform on documents to +apply a fix. This maps all quick fixes into chapter edits (including +XML logic operations). This can be (and is, within Google) used to +integrate with code review tools such that the user can choose whether +to auto-fix a suggestion right from within the code review tool. + +

+   

Partial Analysis

+ +   

About

+

+ + +This chapter describes Lint's “partial analysis”; its architecture and +APIs for allowing lint results to be cached. + +

+ +This focuses on how to write or update existing lint checks such that +they work correctly under partial analysis. For other details about +partial analysis, such as the client side implemented by the build +system, see the lint internal docs folder. + +

+ +

Note that while lint has this architecture, and all lint detectors + must support it, the checks may not run in partial analysis mode; + they may instead run in “global analysis mode”, which is how lint + has worked up until this point. + +

+ + This is because coordinating partial results and merging is + performed by the LintClient; e.g. in the IDE, there's no good + reason to do all this extra work (because all sources are generally + available, including “downstream” module info like the + minSdkVersion). + +

+ + Right now, only the Android Gradle Plugin turns on partial analysis + mode. But that's a very important client, since it's usually how + lint checks are performed on continuous integration servers to + validate code reviews.

+ +

+   

The Problem

+

+ + +Many lint checks require “global” analysis. For example you can't +determine whether a particular string defined in a library module is +unused unless you look at all modules transitively consuming this +library as well. + +

+ +However, many developers run lint as part of their continuous +integration. Particularly in large projects, analyzing all modules for +every check-in is too costly. + +

+ +This chapter describes lint's architecture for handling this, such +that module results can be cached. + +

+   

Overview

+

+ + +Briefly stated, lint's architecture for this is “map reduce”: lint now +has two separate phases, analyze and report (map and reduce +respectively): + +

+ +

    +
  • analyze - where lint analyzes source code of a single module in + isolation, and stores some intermediate partial results (map) + +

    + +

  • +
  • report - where lint reads in the previously stored module results, + and performs some post-processing on this data to generate an actual + lint report.
+ +

+ +Crucially, the individual module results can be cached, such that if +nothing has changed in a module, the module results continue to be +valid (unless signatures have changed in libraries it depends on.) + +

+ +Making this work requires some modifications to any Detector which +considers data from outside the current module. However, there are some +very common scenarios that lint has special support for to make this +easier. + +

+ +Detectors fit into one of the following categories (and these +categories will be explained in subsequent sessions) : + +

+ +

    +
  1. Local analysis which doesn't depend on anything else. For example, a + lint check check which flags typos can report incidents immediately. + Lint calls these “definite incidents”. + +

    + +

  2. +
  3. Local analysis which depends on a few, common conditions. For + example, in Android, a check may only apply if the minSdkVersion < + 21. Lint has special support for this; you basically report an + incident and attach a “constraint” to it. Lint calls these, and + incidents reported as part of #3 below, as “provisional incidents”. + +

    + +

  4. +
  5. Analysis which depends on some conditions of downstream modules that + are not part of the built-in constraints. For example, a lint check + may only apply if the consuming module depends on a certain version + of a networking library. In this case, the detector will report the + incident and attach a map to it, with whatever data it needs to + consult later to decide if the incident actually should be reported. + When the detector reports incidents this way, it has to also + override a callback method. Lint will record these incidents, and + during reporting, call the detector and pass it back its data map + and provisional incidents such that it can decide whether the + incidents should indeed be reported. + +

    + +

  6. +
  7. Last, and least, there are some scenarios where you cannot compute + provisional incidents up front and filter them later (or doing so + would be very costly). For example, unused resources fit into this + category. We don't want to report every single resource declaration + as unused and then filter later. Instead, we compute the resource + usage graph within the module analysis. And in the reporting task, + we then load all the partial usage graphs, and merge them together + and walk the graph to report all the unused resources. To support + this, lint provides a map per module for detectors to put their data + into, and you can put maps into the map to model structured data. + Lint will persist these, and in the reporting task the lint + detectors will be passed their data to do their post-processing and + reporting based on their data.
+ +

+ +These are listed in increasing order of effort, and thankfully, they're +also listed in order of frequency. For lint's built-in checks (~385), + +

+ +

    +
  • 89% needed no work at all. +
  • +
  • 6% were updated to report incidents with constraints +
  • +
  • 4% were updated to report incidents with data for later filtering +
  • +
  • 1% were updated to perform map recording and later reduce filtering
+ +

+   

Does my Detector Need Work?

+

+ + +At this point you're probably wondering whether your checks are in the +89% category where you don't need to do anything, or in the remaining +11%. How do you know? + +

+ +Lint has several built-in mechanisms to try to catch problems. There +are a few scenarios it cannot detect, and these are described below, +but for the vast majority, simply running your unit tests (which are +comprehensive, right?) should create unit test failures if your +detector is doing something it shouldn't. + +

+   

Catching Mistakes: Blocking Access to Main Project

+

+ + +In Android checks, it's very common to try to access the main (“app”) +project, to see what the real minSdkVersion is, since the app +minSdkVersion can be higher than the one in the library. For the +targetSdkVersion it's even more important, since the library +targetSdkVersion has no meaningful relationship to the app one. + +

+ +When you run lint unit tests, as of 7.0, it will now run your tests +twice — once with global analysis (the previous behavior), and once +with partial analysis. When lint is running in partial analysis, a +number of calls, such as looking up the main project, or consulting the +merged manifest, is not allowed during the analysis phase. Attempting +to do so will generate an error: + +

    SdCardTest.java: Error: The lint detector
+        com.android.tools.lint.checks.SdCardDetector
+    called context.getMainProject() during module analysis.
+
+    This does not work correctly when running in Lint Unit Tests.
+
+    In particular, there may be false positives or false negatives because
+    the lint check may be using the minSdkVersion or manifest information
+    from the library instead of any consuming app module.
+
+    Contact the vendor of the lint issue to get it fixed/updated (if
+    known, listed below), and in the meantime you can try to work around
+    this by disabling the following issues:
+
+    "SdCardPath"
+
+    Issue Vendor:
+    Vendor: Android Open Source Project
+    Contact: https://groups.google.com/g/lint-dev
+    Feedback: https://issuetracker.google.com/issues/new?component=192708
+
+    Call stack: Context.getMainProject(Context.kt:117)←SdCardDetector$createUastHandler$1.visitLiteralExpression(SdCardDetector.kt:66)
+        ←UElementVisitor$DispatchPsiVisitor.visitLiteralExpression(UElementVisitor.kt:791)
+        ←ULiteralExpression$DefaultImpls.accept(ULiteralExpression.kt:38)
+        ←JavaULiteralExpression.accept(JavaULiteralExpression.kt:24)←UVariableKt.visitContents(UVariable.kt:64)
+        ←UVariableKt.access$visitContents(UVariable.kt:1)←UField$DefaultImpls.accept(UVariable.kt:92)
+        ...

+ +Specific examples of information many lint checks look at in this +category: + +

+ +

    +
  • minSdkVersion and targetSdkVersion +
  • +
  • The merged manifest +
  • +
  • The resource repository +
  • +
  • Whether the main module is an Android project
+ +

+   

Catching Mistakes: Simulated App Module

+

+ + +Lint will also modify the unit test when running the test in partial +analysis mode. In particular, let's say your test has a manifest which +sets minSdkVersion to 21. + +

+ +Lint will instead run the analysis task on a modified test project +where the minSdkVersion is set to 1, and then run the reporting task +where minSdkVersion is set back to 21. This ensures that lint checks +will correctly use the minSdkVersion from the main project, not the +library. + +

+   

Catching Mistakes: Diffing Results

+

+ + +Lint will also diff the report output from running the same unit tests +both in global analysis mode and in partial analysis mode. We expect +the results to always be identical, and in some cases if the module +analysis is not written correctly, they're not. + +

+   

Catching Mistakes: Remaining Issues

+

+ + +The above three mechanisms will catch most problems related to partial +analysis. However, there are a few remaining scenarios to be aware of: + +

+ +

    +
  • Resolving into library source code. If you have a Kotlin or Java + function call AST node (UCallExpression) you can call resolve() + on it to find the called PsiMethod, and from there you can look at + its source code, to make some decisions. + +

    + + For example, lint's API Check uses this to see if a given method is a + version-check utility (“SDK_INT > 21?”); it resolves the method + call in if (isOnLollipop()) { ... } and looks at its method body to + see if the return value corresponds to a proper SDK_INT check. + +

    + + In partial analysis mode, you cannot look at source files from + libraries you depend on; they will only be provided in binary + (bytecode inside a jar file) form. + +

    + + This means that instead, you need to aggregate data along the way. + For example, the way lint handles the version check method lookup is + to look for SDK_INT comparisons, and if found, stores a reference to + the method in ther partial results map which it can later consult + from downstream modules. + +

    + +

  • +
  • Multiple passes across the modules (lint has a way to request + multiple passes; this was used by a few lint checks like the unused + resource detector; the multiple passes now only apply to the local + module)
+ +

+ +In order to test for correct operation of your check, you should add +your own individual unit test for a multi-module project. + +

+ +Lint's unit test infrastructure makes this easy; just use relative +paths in the test file descriptions. + +

+ +For example, if you have the following unit test declaration: + +

lint().files( + manifest().minSdk(15), + manifest().to("../app/AndroidManifest.xml").minSdk(21), + xml( + "res/layout/linear.xml", + "<linearlayout ...="">" + ...

+ +The second manifest() call here on line 3 does all the heavy lifting: +the fact that you're referencing ../app means it will create another +module named “app”, and it will add a dependency from that module on +this one. It will also mark the current module as a library. This is +based on the name patterns; if you for example reference say ../lib1, +it will assume the current module is an app module and the dependency +will go from here to the library. + +

+ +Finally, to test a multi-module setup where the code in the other +module is only available as binary, lint has a new special test file +type. The CompiledSourceFile can be constructed via either +compiled(), if you want to make both the source code and the class +file available in the project, or bytecode() if you want to only +provide the bytecode. In both cases you include the source code in the +test file declaration, and the first time you run your test it will try +to run compilation and emit the extra base64 string to include the test +file. By having the sources included for the binary it's easy to +regenerate bytecode tests later (this was an issue with some of lint's +older unit tests; we recently decompiled them and created new test +files using this mechanism to make the code more maintainable. + +

+ +Lint's partial analysis testing support will automatically only use +binaries for the dependencies (even if using CompiledSourceFile with +sources). + +

+ +

Lint's testing infrastructure may try to automate this testing at + some point; e.g. by looking at the error locations from a global + analysis, it can then create a new project where only the source + file with the warnings is provided as source, and all the other test + files are placed in a separate module, and then represented only as + binaries (through a lint AST to PsiCompiled pretty printer.)
+ +

+   

Incidents

+

+ + +In the past, you would typically report problems like this: +

context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image" + )

+ +At some point, we added support for quickfixes, so the +report method took an additional parameter, line 6: + +

context.report( + ISSUE, + element, + context.getNameLocation(element), + "Missing `contentDescription` attribute on image", + fix().set().todo(ANDROID_URI, ATTR_CONTENT_DESCRIPTION).build() +)

+ +Now that we need to attach various additional data (like constraints +and maps), we don't really want to just add more parameters. + +

+ +Instead, this tuple of data about a particular occurrence of a problem +is called an “incident”, and there is a new Incident class which +represents it. To report an incident you simply call +context.report(incident). There are several ways to create these +incidents. The easiest is to simply edit your existing call above by +adding Incident( (or from Java, new Incident() inside the +context.report block like this: + +

    context.report(Incident(
+        ISSUE,
+        element,
+        context.getNameLocation(element),
+        "Missing `contentDescription` attribute on image"
+    ))

+ +and then reformatting the source code: + +

    context.report(
+        Incident(
+            ISSUE,
+            element,
+            context.getNameLocation(element),
+            "Missing `contentDescription` attribute on image"
+        )
+)

+ +Incident has a number of overloaded constructors to make it easy to +construct it from existing report calls. + +

+ +There are other ways to construct it too, for example like the +following: + +

    Incident(context)
+        .issue(ISSUE)
+        .scope(node)
+        .location(context.getLocation(node))
+        .message("Do not hardcode \"/sdcard/\"").report()

+ +That are additional methods you can fall too, like fix(), and +conveniently, at() which specifies not only the scope node but +automatically computes and records the location of that scope node too, +such that the following is equivalent: + +

    Incident(context)
+        .issue(ISSUE)
+        .at(node)
+        .message("Do not hardcode \"/sdcard/\"").report()

+ +So step one to partial analysis is to convert your code to report +incidents instead of the passing in all the individual properties of an +incident. Note that for backwards compatibility, if your check doesn't +need any work for partial analysis, you can keep calling the older +report methods; they will be redirected to an Incident call +internally, but since you don't need to attach data you don't have to +make any changes + +

+   

Constraints

+

+ + +If your check needs to be conditional, perhaps on the minSdkVersion, +you need to attach a “constraint” to your report call. + +

+ +All the constraints are built in; there isn't a way to implement your +own. For custom logic, see the next section: LintMaps. + +

+ +Here are the current constraints, though this list may grow over time: + +

+ +

    +
  • minSdkAtLeast(Int) +
  • +
  • minSdkLessThan(Int) +
  • +
  • targetSdkAtLeast(Int) +
  • +
  • targetSdkLessThan(Int) +
  • +
  • isLibraryProject() +
  • +
  • isAndroidProject() +
  • +
  • notLibraryProject() +
  • +
  • notAndroidProject()
+ +

+ +These are package-level functions, though from Java you can access them +from the Constraints class. + +

+ +Recording an incident with a constraint is easy; first construct the +Incident as before, and then report them via +context.report(incident, constraint): + +

    String message =
+        "One or more images in this project can be converted to "
+        + "the WebP format which typically results in smaller file sizes, "
+        + "even for lossless conversion";
+    Incident incident = new Incident(WEBP_ELIGIBLE, location, message);
+    context.report(incident, minSdkAtLeast(18));

+ +Finally, note that you can combine constraints; there are both “and” +and “or” operators defined for the Constraint class. so the following +is valid: + +

    val constraint = targetSdkAtLeast(23) and notLibraryProject()
+    context.report(incident, constraint)

+ +That's all you have to do. Lint will record this provisional incident, +and when it is performing reporting, it will evaluate these constraints +on its own and only report incidents that meet the constraint. + +

+   

Incident LintMaps

+

+ + +In some cases, you cannot use one of the built-in constraints; you have +to do your own “filtering” from the reporting task, where you have +access to the main module. + +

+ +In that case, you call context.report(incident, map) instead. + +

+ +Like Incident, LintMap is a new data holder class in lint which +makes it convenient to pass around (and more importantly, persist) +data. All the set methods return the map itself, so you can easily +chain property calls. + +

+ +Here's an example: + +

    context.report(
+        incident,
+        map()
+            .put(KEY_OVERRIDES, overrides)
+            .put(KEY_IMPLICIT, implicitlyExportedPreS)
+    )

+ +Here, map() is a method defined by Detector to create a new +LintMap, similar to how fix() constructs a new LintFix. + +

+ +Note however that when reporting data, you need to do the post +processing yourself. To do this, you need to override this method: + +

    /**
+     * Filter which looks at incidents previously reported via
+     * [Context.report] with a [LintMap], and returns false if the issue
+     * does not apply in the current reporting project context, or true
+     * if the issue should be reported. For issues that are accepted,
+     * the detector is also allowed to mutate the issue, such as
+     * customizing the error message further.
+     */
+    open fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean { }

+ +For example, for the above report call, the corresponding +implementation of filterIncident looks like this: + +

    override fun filterIncident(context: Context, incident: Incident, map: LintMap): Boolean {
+        if (context.mainProject.targetSdk < 19) return true
+        if (map.getBoolean(KEY_IMPLICIT, false) == true && context.mainProject.targetSdk >= 31) return true
+        return map.getBoolean(KEY_OVERRIDES, false) == false
+    }

+ +Note also that you are allowed to modify incidents here before +reporting them. The most common reason scenario for this is changing +the incident message, perhaps to reflect data not known at module +analysis time. For example, lint's API check creates messages like this: + +

+ +Error: Cast from AudioFormat to Parcelable requires API level 24 (current min is 21) + +

+ +At module analysis time when the incident was created, the minSdk being +21 was not known (and in fact can vary if this library is consumed by +many different app modules!) + +

+ +

You must store state in the lint map; don't try to store it in the + detector itself as instance state. That won't work because the + detector instance that filterInstance is called on is not the same + instance as the one which originally reported it. If you think about + it, that makes sense; when module results are cached, the same + reported data can be used over and over again for repeated builds, + each time for new detector instances in the reporting task.
+ +

+   

Module LintMaps

+

+ + +The last (and most involved) scenario for partial analysis is one where +you cannot just create incidents and filter or customize them later. + +

+ +The most complicated example of this is lint's built-in +UnusedResourceDetector, which locates unused resources. This “requires” +global analysis, since we want to include all resources in the entire +project. We also cannot just store lists of “resources declared” and +“resources referenced“ since we really want to treat this as a graph. +For example if @layout/main is including @drawable/icon, then a +naive approach would see the icon as referenced (by main) and therefore +mark it as not unused. But what we want is that if the icon is only +referenced from main, and if main is unused, then so is the icon. + +

+ +To handle this, we model the resources as a graph, with edges +representing references. + +

+ +When analyzing individual modules, we create the resource graph for +just that model, and we store that in the results. That means we store +it in the module's LintMap. This is a map for the whole module +maintained by lint, so you can access it repeatedly and add to it. +(This is also where lint's API check stores the SDK_INT comparison +functions as described earlier in this chapter). + +

+ +The unused resource detector creates a persistence string for the +graph, and records that in the map. + +

+ +Then, during reporting, it is given access to all the lint maps for +all the modules that the reporting module depends on, including itself. +It then merges all the graphs into a single reference graph. + +

+ +For example, let's say in module 1 we have layout A which includes +drawables B and D, and B in turn depends on color C. We get a resource +graph like the following: + +

+ + + + + + + + + + + + + + + + + + + + + + + + + +ABCD + +

+ +Then in another module, we have the following resource reference graph: + +

+ + + + + + + + + + + + + + + + + + +EBD + +

+ +In the reporting task, we merge the two graphs like the following: + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +EABCD + +

+ +Once that's done, it can proceed precisely as before: analyze the graph +and report all the resources that are not reachable from the reference +roots (e.g. manifest and used code). + +

+ +The way this works in code is that you report data into the module by +first looking up the module data map, by calling this method on the +Context: + +

    /**
+     * Returns a [PartialResult] where state can be stored for later
+     * analysis. This is a more general mechanism for reporting
+     * provisional issues when you need to collect a lot of data and do
+     * some post processing before figuring out what to report and you
+     * can't enumerate out specific [Incident] occurrences up front.
+     *
+     * Note that in this case, the lint infrastructure will not
+     * automatically look up the error location (since there isn't one
+     * yet) to see if the issue has been suppressed (via annotations,
+     * lint.xml and other mechanisms), so you should do this
+     * yourself, via the various [LintDriver.isSuppressed] methods.
+     */
+    fun getPartialResults(issue: Issue): PartialResult { ... }

+ +Then you put whatever data you want, such as the resource usage model +encoded as a string. + +

+ +

Note that you don't have to worry about clashes in key names; each + issue (and therefore detector) is given its own map.
+ +

+ +And then your detector should also override the following method, where +you can walk through the map contents, compute incidents and report +them: + +

    /**
+     * Callback to detectors that add partial results (by adding entries
+     * to the map returned by [LintClient.getPartialResults]). This is
+     * where the data should be analyzed and merged and results reported
+     * (via [Context.report]) to lint.
+     */
+    open fun checkPartialResults(context: Context, partialResults: PartialResult) { ... }
+   

Optimizations

+

+ + +Most lint checks run on the fly in the IDE editor as well. In some +cases, if all the map computations are expensive, you can check whether +partial analysis is in effect, and if not, just directly access (for +example) the main project. + +

+ +Do this by calling isGlobalAnalysis(): + +

   if (context.isGlobalAnalysis()) {
+       // shortcut
+   } else {
+       // partial analysis code path
+   }

+ + + +

+   

Frequently Asked Questions

+

+ + +This chapter contains a random collection of questions people +have asked in the past. + +

+   

My detector callbacks aren't invoked

+

+ + +If you've for example implemented the Detector callback for visiting +method calls, visitMethodCall, notice how the third parameter is a +PsiMethod, and that it is not nullable: + +

    open fun visitMethodCall(
+        context: JavaContext,
+        node: UCallExpression,
+        method: PsiMethod
+    ) {

+ +This passes in the method that has been called. When lint is visiting +the AST, it will resolve calls, and if the called method cannot be +resolved, the callback won't be called. + +

+ +This happens when the classpath that lint has been configured with does +not contain everything needed. When lint is running from Gradle, this +shouldn't happen; the build system should have a complete classpath and +pass it to Lint (or the build wouldn't have succeeded in the first +place). + +

+ +This usually comes up in unit tests for lint, where you've added a test +case which is referencing some API for some library, but the library +itself isn't part of the test. The solution for this is to create stubs +for the part of the API you care about. This is discussed in more +detail in the unit testing chapter. + +

+   

My lint check works from the unit test but not in the IDE

+

+ + +There are several things to check if you have a lint check which +works correctly from your unit test but not in the IDE. + +

+ +

    +
  1. First check that the lint jar is packaged correctly; use jar tvf + lint.jar to look at the jar file to make sure it contains the + service loader registration of your issue registry, and javap + -classpath lint.jar com.example.YourIssueRegistry to inspect your + issue registry. + +

    + +

  2. +
  3. If that's correct, the next thing to check is that lint is actually + loading your issue registry. First look in the IDE log (from the + Help menu) to make sure there aren't log messages from lint + explaining why it can't load the registry, for example because it + does not specify a valid applicable API range. + +

    + +

  4. +
  5. If there's no relevant warning in the log, try setting the + $ANDROID_LINT_JARS environment variable to point directly to your + lint jar file and restart Studio to make sure that that works. + +

    + +

  6. +
  7. Next, try running Analyze | Inspect Code.... This runs lint on + the whole project. If that works, then the issue is that your lint + check isn't eligible to run “on the fly”; the reason for this is + that your implementation scope registers more than one scope, which + says that your lint check can only run if lint gets to look at both + types of files, and in the editor, only the current file is analyzed + by lint. However, you can still make the check work on the fly by + specifying additional analysis scopes; see the API guide for more + information about this.
+ +

+   

visitAnnotationUsage isn't called for annotations

+

+ + +If you want to just visit any annotation declarations (e.g. @Foo on +method foo), don't use the applicableAnnotations and +visitAnnotationUsage machinery. The purpose of that facility is to +look at elements that are being combined with annotated elements, +such as a method call to a method whose return value has been +annotated, or an argument to a method a method parameter that has been +annotated, or assigning an assigned value to an annotated variable, etc. + +

+ +If you just want to look at annotations, use getApplicableUastTypes +with UAnnotation::class.java, and a UElementHandler which overrides +visitAnnotation. + +

+   

How do I check if a UAST or PSI element is for Java or Kotlin?

+

+ + +To check whether an element is in Java or Kotlin, call one +of the package level methods in the detector API (and from +Java, you can access them as utility methods on the “Lint” +class) : + +

package com.android.tools.lint.detector.api
+
+/** Returns true if the given element is written in Java. */
+fun isJava(element: PsiElement?): Boolean { /* ... */ }
+
+/** Returns true if the given language is Kotlin. */
+fun isKotlin(language: Language?): Boolean { /* ... */ }
+
+/** Returns true if the given language is Java. */
+fun isJava(language: Language?): Boolean { /* ... */ }

+ +If you have a UElement and need a PsiElement for the above method, +see the next question. + +

+   

What if I need a PsiElement and I have a UElement ?

+

+ + +If you have a UElement, you can get the underlying source PSI element +by calling element.sourcePsi. + +

+   

How do I get the UMethod for a PsiMethod ?

+

+ + +Call psiMethod.toUElementOfType<umethod>(). Note that this may return +null if UAST cannot find valid Java or Kotlin source code for the +method. + +

+ +For PsiField and PsiClass instances use the equivalent +toUElementOfType type arguments. + +

+   

How do get a JavaEvaluator ?

+

+ + +The Context passed into most of the Detector callback methods +relevant to Kotlin and Java analysis is of type JavaContext, and it +has a public evaluator property which provides a JavaEvaluator you +can use in your analysis. + +

+ +If you need one outside of that scenario (this is not common) you can +construct one directly by instantiating a DefaultJavaEvaluator; the +constructor parameters are nullable, and are only needed for a couple +of operations on the evaluator. + +

+   

How do I check whether an element is internal?

+

+ + +First get a JavaEvaluator as explained above, then call +this evaluator method: + +

open fun isInternal(owner: PsiModifierListOwner?): Boolean { /* ... */

+ +(Note that a PsiModifierListOwner is an interface which includes +PsiMethod, PsiClass, PsiField, PsiMember, PsiVariable, etc.) + +

+   

Is element inline, sealed, operator, infix, suspend, data?

+

+ + +Get the JavaEvaluator as explained above, and then call one of these +evaluator method: + +

open fun isData(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isInline(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isLateInit(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isSealed(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isOperator(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isInfix(owner: PsiModifierListOwner?): Boolean { /* ... */
+open fun isSuspend(owner: PsiModifierListOwner?): Boolean { /* ... */
+   

How do I look up a class if I have its fully qualified name?

+

+ + +Get the JavaEvaluator as explained above, then call +evaluator.findClass(qualifiedName: String). Note that the result is +nullable. + +

+   

How do I look up a class if I have a PsiType?

+

+ + +Get the JavaEvaluator as explained above, then call +evaluator.getTypeClass. To go from a class to its type, +use getClassType. + +

    abstract fun getClassType(psiClass: PsiClass?): PsiClassType?
+    abstract fun getTypeClass(psiType: PsiType?): PsiClass?
+   

How do I look up hierarhcy annotations for an element?

+

+ + +You can directly look up annotations via the modified list +of PsiElement or the annotations for a UAnnotated element, +but if you want to search the inheritance hierarchy for +annotations (e.g. if a method is overriding another, get +any annotations specified on super implementations), use +one of these two evaluator methods: + +

    abstract fun getAllAnnotations(
+        owner: UAnnotated,
+        inHierarchy: Boolean
+    ): List<uannotation>
+
+    abstract fun getAllAnnotations(
+        owner: PsiModifierListOwner,
+        inHierarchy: Boolean
+    ): Array<psiannotation>
+   

How do I look up if a class is a subclass of another?

+

+ + +To see if a method is a direct member of a particular +named class, use the following method in JavaEvaluator: + +

fun isMemberInClass(member: PsiMember?, className: String): Boolean { }

+ +To see if a method is a member in any subclass of a named class, use + +

    open fun isMemberInSubClassOf(
+        member: PsiMember,
+        className: String,
+        strict: Boolean = false
+    ): Boolean { /* ... */ }
+

+ +Here, use strict = true if you don't want to include members in the +named class itself as a match. + +

+ +To see if a class extends another or implements an interface, use one +of these methods. Again, strict controls whether we include the super +class or super interface itself as a match. + +

    abstract fun extendsClass(
+        cls: PsiClass?,
+        className: String,
+        strict: Boolean = false
+    ): Boolean
+
+    abstract fun implementsInterface(
+        cls: PsiClass,
+        interfaceName: String,
+        strict: Boolean = false
+    ): Boolean
+   

How do I know which parameter a call argument corresponds to?

+

+ + +In Java, matching up the arguments in a call with the parameters in the +called method is easy: the first argument corresponds to the first +parameter, the second argument corresponds to the second parameter and +so on. If there are more arguments than parameters, the last arguments +are all vararg arguments to the last parameter. + +

+ +In Kotlin, it's much more complicated. With named parameters, but +arguments can appear in any order, and with default parameters, only +some of them may be specified. And if it's an extension method, the +first argument passed to a PsiMethod is actually the instance itself. + +

+ +Lint has a utility method to help with this on the JavaEvaluator: + +

    open fun computeArgumentMapping(
+        call: UCallExpression,
+        method: PsiMethod
+    ): Map<uexpression, psiparameter=""> { /* ... */

+ +This returns a map from UAST expressions (each argument to a UAST call +is a UExpression, and these are the valueArguments property on the +UCallExpression) to each corresponding PsiParameter on the +PsiMethod that the method calls. + +

+   

How can my lint checks target two different versions of lint?

+

+ + +If you need to ship different versions of your lint checks to target +different versions of lint (because perhaps you need to work both with +an older version of lint, and a newer version that has a different +API), the way to do this (as of Lint 7.0) is to use the maxApi +property on the IssueRegistry. In the service loader registration +(META-INF/services), register two issue registries; one for each +implementation, and mark the older one with the right minApi to +maxApi range, and the newer one with minApi following the previous +registry's maxApi. (Both minApi and maxApi are inclusive). When +lint loads the issue registries it will ignore registries with a range +outside of the current API level. + +

+   

How do I check out the current lint source code?

+
$ git clone --branch=mirror-goog-studio-master-dev --single-branch \
+   https://android.googlesource.com/platform/tools/base
+Cloning into 'base'...
+remote: Total 648820 (delta 325442), reused 635137 (delta 325442)
+Receiving objects: 100% (648820/648820), 1.26 GiB | 15.52 MiB/s, done.
+Resolving deltas: 100% (325442/325442), done.
+Updating files: 100% (14416/14416), done.
+
+$ du -sh base
+1.8G    base
+$ cd base/lint
+$ ls
+.editorconfig           BUILD                   build.gradle            libs/
+.gitignore              MODULE_LICENSE_APACHE2  cli/
+$ ls libs/
+intellij-core/   kotlin-compiler/ lint-api/        lint-checks/     lint-gradle/     lint-model/      lint-tests/      uast/
+   

Where do I find examples of lint checks?

+

+ + +The built-in lint checks are a good source. Check out the source code +as shown above and look in +lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ or +browse sources online: +https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-master-dev:lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/ + +

+ + + +

+   

Appendix: Recent Changes

+

+ + +Recent Changes + +

+ +This chapter lists recent changes to lint that affect lint check +authors: new features, API and behavior changes, and so on. For +information about user visible changes to lint, see +7.0 + +

+ +

+ +

+
   

Appendix: Environment Variables and System Properties

+

+ + +This chapter lists the various environment variables and system +properties that Lint will look at. None of these are really intended to +be used or guaranteed to be supported in the future, but documenting +what they are seems useful. + +

+   

Environment Variables

+ +   

Detector Configuration Variables

+

+ + +

ANDROID_LINT_INCLUDE_LDPI

Lint's icon checks normally ignore the ldpi density since it's not + commonly used any more, but you can turn this back on with this + environment variable set to true. + +

ANDROID_LINT_MAX_VIEW_COUNT

Lint's TooManyViews check makes sure that a single layout does not + have more than 80 views. You can set this environment variable to a + different number to change the limit. + +

ANDROID_LINT_MAX_DEPTH

Lint's TooManyViews check makes sure that a single layout does not + have a deeper layout hierarchy than 10 levels.You can set this + environment variable to a different number to change the limit. + +

ANDROID_LINT_NULLNESS_IGNORE_DEPRECATED

Lint's UnknownNullness which flags any API element which is not + explicitly annotated with nullness annotations, normally skips + deprecated elements. Set this environment variable to true to include + these as well. + +

+ + Corresponding system property: lint.nullness.ignore-deprecated + +

+   

Lint Configuration Variables

+

+ + +

ANDROID_SDK_ROOT

Locates the Android SDK root + +

ANDROID_HOME

Locates the Android SDK root, if $ANDROID_SDK_ROOT has not been set + +

JAVA_HOME

Locates the JDK when lint is analyzing JDK (not Android) projects + +

LINT_XML_ROOT

Normally the search for lint.xml files proceeds upwards in the + directory hierarchy. In the Gradle integration, the search will stop + at the root Gradle project, but in other build systems, it can + continue up to the root directory. This environment variable sets a + path where the search should stop. + +

ANDROID_LINT_JARS

A path of jar files (using the path separator — semicolon on + Windows, colon elsewhere) for lint to load extra lint checks from + +

ANDROID_SDK_CACHE_DIR

Sets the directory where lint should read and write its cache files. + Lint has a number of databases that it caches between invocations, + such as its binary representation of the SDK API database, used to + look up API levels quickly. In the Gradle integration of lint, this + cache directory is set to the root build/ directory, but elsewhere + the cache directory is located in a lint subfolder of the normal + Android tooling cache directory, such as ~/.android. + +

LINT_OVERRIDE_CONFIGURATION

Path to a lint XML file which should override any local lint.xml + files closer to reported issues. This provides a way to globally + change configuration. + +

+ + Corresponding system property: lint.configuration.override + +

LINT_DO_NOT_REUSE_UAST_ENV

Set to true to enable a workaround (if affected) for + bug 159733104 + until 7.0 is released. + +

+ + Corresponding system property: lint.do.not.reuse.uast.env + +

LINT_API_DATABASE

Point lint to an alternative API database XML file instead of the + normally used $SDK/platforms/android-?/data/api-versions.xml file. + +

+   

Lint Development Variables

+

+ + +

LINT_PRINT_STACKTRACE

If set to true, lint will print the full stack traces of any internal + exceptions encountered during analysis. This is useful for authors of + lint checks, or for power users who can reproduce a bug and want to + report it with more details. + +

+ + Corresponding system property: lint.print-stacktrace + +

LINT_TEST_KOTLINC

When writing a lint check unit test, when creating a compiled or + bytecode test file, lint can generate the .class file binary + content automatically if it is pointed to the kotlinc compiler. + +

LINT_TEST_JAVAC

When writing a lint check unit test, when creating a compiled or + bytecode test file, lint can generate the .class file binary + content automatically if it is pointed to the javac compiler. + +

INCLUDE_EXPENSIVE_LINT_TESTS

When working on lint itself, set this environment variable to true + some really, really expensive tests that we don't want run on the CI + server or by the rest of the development team. + +

+   

System Properties

+

+ + + + +

To set system properties when running lint via Gradle, try for + example ./gradlew lintDebug -Dlint.baselines.continue=true
+ +

+ +

lint.baselines.continue

When you configure a new baseline, lint normally fails the build + after creating the baseline. You can set this system property to true + to force lint to continue. + +

lint.autofix

Turns on auto-fixing (applying safe quickfixes) by default. This is a + shortcut for invoking the lintFix targets or running the lint + command with --apply-suggestions. + +

lint.html.prefs

This property allows you to customize lint's HTML reports. It + consists of a comma separated list of property assignments, e.g. + ./gradlew :app:lintDebug -Dlint.html.prefs=theme=darcula,window=5 + +

+ + + + +
Property Explanation and Values Default
theme light, darcula, solarized light
window Number of lines around problem 3
maxPerIssue. Issue count before “More...” button 50
underlineErrors If true, wavy underlines, else highlight true
+ +

+ +

lint.unused-resources.exclude-tests

Whether the unused resource check should exclude test sources as + referenced resources. + +

lint.configuration.override

Alias for $LINT_OVERRIDE_CONFIGURATION + +

lint.print-stacktrace

Alias for $LINT_PRINT_STACKTRACE + +

lint.do.not.reuse.uast.env

Alias for $LINT_DO_NOT_REUSE_UAST_ENV + +

+

formatted by Markdeep 1.13  
\ No newline at end of file diff --git a/docs/book.md.html b/docs/book.md.html new file mode 100644 index 00000000..b2336437 --- /dev/null +++ b/docs/book.md.html @@ -0,0 +1,21 @@ + + +**Android Lint API Guide** + +This chapter inlines all the API documentation into a single +long chapter, suitable for printing or reading on a tablet. + + (insert api-guide/terminology.md.html here) + (insert api-guide/basics.md.html here) + (insert api-guide/example.md.html here) + (insert api-guide/publishing.md.html here) + (insert api-guide/unit-testing.md.html here) + (insert api-guide/quickfixes.md.html here) + (insert api-guide/partial-analysis.md.html here) + (insert api-guide/faq.md.html here) + + # Appendix: Recent Changes + (insert api-guide/changes.md.html here) + (insert usage/variables.md.html here) + + diff --git a/docs/changes.md.html b/docs/changes.md.html new file mode 100644 index 00000000..3fb58b0b --- /dev/null +++ b/docs/changes.md.html @@ -0,0 +1,17 @@ +# Recent Changes + +## User-facing Changes + +For recent changes in lint's set of features affecting users +of lint, see [](usage/changes.md.html). + +## Lint API Changes + +For recent changes in lint's internals affecting authors of +lint checks, see [](api-guide/changes.md.html). + +## Documentation Changes + +2021/03/15: Initial version. + + diff --git a/docs/features.md.html b/docs/features.md.html new file mode 100644 index 00000000..a0d52dee --- /dev/null +++ b/docs/features.md.html @@ -0,0 +1,140 @@ + + +**Lint Features** + +# Features for users + +* Around 400 built-in checks and growing regularly + +* Built-in mechanism for libraries to provide their own additional + checks enforcing correct usage of the library. For example, the + AndroidX libraries are currently providing ~30 issues, and other open + source libraries like the Timber logging library also provide their + own checks. + +* Integration with IDEs like Android Studio and IntelliJ + +* Integration with build systems like Gradle (as well as a command line + tool which can be used to run lint from scripts and other build + systems; this is for example used to run lint inside Google with its + own build system.) + +* Support for “baselines”: this allows you to record all the current + issues found in a codebase, check that in, and then any subsequent + runs of lint will only report newly introduced issues, not the ones + already marked in the baseline. This makes it easy to introduce lint + into existing codebases and enforce no regressions without having to + clean everything up right away. + +* Support for suppressing incidents many other ways: + - Using `@Suppress` (Kotlin) or `@SuppressWarnings` (Java) + - Using `//noinspection` comment markers (already used by IntelliJ) + - Using `tools:ignore` attributes in XML files + - Using `lint.xml` files to record specific incidents to ignore, + or matched by globs on paths or regular expressions on messages + or locations + +* Many other configuration options. For example, via `lint.xml` files, + or Gradle DSL methods, or command line flags, you can change the + severity of any issue, to for example turn a warning into an error or + vice versa. The `lint.xml` files can be nested, and the closest + configuration is used. + +* In the Android Gradle integration, support for running a subset of + the checks automatically in release builds, not just when the lint + targets are invoked (the `lintVital` target, which `assembleRelease` + depends on). This helps solve the problem where static analysis tools + are available but unaware users aren't running them. (This is + reserved for only the most critical issues, which in lint's parlance + is known as “fatal” severity. Thanks to the above configurability, + you can promote your own issues to fatal or demote some of the + built-in ones. + +* Support for quickfixes. Many lint checks have associated quickfixes, + which can be invoked in the IDE, or (for fixes marked as safe and + unambiguous) via a Gradle target (`lintFix`) to apply them all Kotlin + batch. + +* Support for many report output formats, including SARIF (recently + supported by GitHub for unified static analysis tool visualization.) + +# Features for lint-check authors + +* Ability to easily target both Java and Kotlin source files without + writing the checks twice, using UAST. + +* Powerful AST APIs for analyzing source code: UAST and PSI for Java + and Kotlin, ASM for bytecode, DOM for XML. + +* Support not just for Kotlin and Java, but a number of other file + types: + - XML files + - Property files + - bytecode / `.class` files + - Gradle files (`.gradle` and `.gradle.kts`) + - Icon files (`.png`, `.webp`, `.gif`, etc) + - Kotlin and Java + - ProGuard/R8 files + - Other (a callback which lets a lint check visit all files; for + example, lint's `PrivateKeyDetector` looks at files to see if it + looks like somebody has accidentally included a private key ASCII + file.) + +* Importantly, a lint check can check more than one of these, so for + example, in Android, it can see which icon is designated as the + application icon in the XML manifest file, and then it can check the + bitmap in the icon to make sure it conforms with the styleguide for + application icons (size guidelines, transparency, etc). + +* Generic support for visiting abstract syntax trees with a visitor, + but a number of convenience implementations to make common operations + much easier. For example, you can get callbacks when + - A method of a given name is called + - An object of a given type is instantiated + - A subclass of a given class or interface is implemented + - A method annotated with a specific annotation is called (or + more generally, an element associated with an annotated package, + class, method, parameter or variable, is referenced.) + +* A number of built-in helpers to make analysis easier, such as + + - A constant evaluator, such that if you evaluate the AST node for `3 + > 4 && 5 < 7> is returns `false`, including references to static + final constants in the code or class files. + + - A code evaluator which can check whether one class extends another + or implements an interface, or computing the parameter to argument + mapping (which in Kotlin with named and default parameters and + extension methods is not as trivial as lining up the parameters and + arguments the way it can be done for Java) + + - A data flow analyzer which performs some simple data flow analysis + within a method. This is used by a number of built-in checks, for + example ensure that when you create a transaction, you also call + `commit` on it. + +* Support for implementing quickfixes. A number of lint fix + implementations making it easy to perform text edits, perform source + formatting or import cleanup afterwards, providing composite or + alternatives, etc.) + +* Support for a pseudo-markdown syntax in error messages, which allows + you to use emphasis or code fonts to reference symbols etc, which + makes the presentation in the IDE (in tooltips) or in HTML reports + look better. + +* Strong unit testing infrastructure, making it easy to write unit + tests for lint checks and catching common errors (by running the unit + tests repeatedly under different conditions for example). There are + also around a dozen lint checks which looks at implementations of + lint checks and flags potential issues. This is implemented by the + LintDetectorDetector :-) + +* Through the analysis scopes mechanism, support for writing lint + checks that run on the fly in the editor in the IDE. + +* For Android checks, a number of important abstractions such as access + to the merged manifest, random access lookup of Android resources, + etc. + + diff --git a/docs/internal/guidelines.md.html b/docs/internal/guidelines.md.html new file mode 100644 index 00000000..66f5f1ff --- /dev/null +++ b/docs/internal/guidelines.md.html @@ -0,0 +1,80 @@ + + +**Lint Development Guidelines** + +(This folder should also contain architectural information documents.) + +# Setup + +To develop lint, use a recent IntelliJ, check out the Android tools +repository and then open up the folder `tools/` as a Gradle project. + +# Code + +* All new files should be written in Kotlin. + +* Code should be formatted using “cd lint && gradle formatLint”, which + will format both the Java and Kotlin files according to the + configured style. + +* All lint checks must have tests checking both for false positives and + false negatives. + +* Make sure that lint checks have the correct platform applied (e.g. + androidSpecific=true in the issue registration for any Android + checks.) + +# Debugging lint under Gradle + +To be able to attach to and debug lint when invoked from within the +Android Gradle plugin, add this to the `gradle.properties` file in the +target project before invoking gradle with the debug flags: + +``` +android.experimental.runLintInProcess=true +``` + +# Debugging lint in the IDE + +When debugging lint running in the IDE, machinery the IDE has +to avoid UI freezes involves throwing exceptions if read operations +are taking too long -- and then the editor restarting highlighting +at a later time. This makes debugging tricky. There are probably +better ways to do this (I remember an IntelliJ registry key to set +that I can't find now), but what I do is open `LintIdeClient` and +in the `runReadAction` method, I change + +``` +if (application.isUnitTestMode()) { +``` + +to + +``` +if (true || application.isUnitTestMode()) { +``` + +# Updating Baselines + +If you've added a new lint check which has a number of existing +violations in the Studio tree, you can run the following target +to update the baselines: + +``` +bazel run --test_env=UPDATE_LINT_BASELINE=1 \ + $(bazel query --output=label 'kind(.*lint_test, //tools/...)') +``` + +# Registering Inspections + +When lint runs in the IDE, all lint checks are wrapped as IntelliJ +inspections, such that they show up in the Inspections options list +etc. Lazily discovered lint checks (3rd party lint checks) will show up +as soon as lint has run to discover them, but for built-in checks we +should register them in advance such that they always show up. To do +this, open the `tools/adt/idea` project and run the +`LintInspectionRegistrationTest` test. To have your sources updated in +place, either set `$ADT_SOURCE_TREE` or edit the test first to point +the `sourceTree` var to point to your checkout. + + diff --git a/docs/usage/baselines.md.html b/docs/usage/baselines.md.html new file mode 100644 index 00000000..681727b1 --- /dev/null +++ b/docs/usage/baselines.md.html @@ -0,0 +1,87 @@ + + +# Baselines + +# Creating a Baseline + +You can take a snapshot of your project's current set of warnings, +and then use the snapshot as a baseline for future inspection runs +so that only new issues are reported. The baseline snapshot lets you +start using lint to fail the build without having to go back and +address all existing issues first. + +To create a baseline snapshot, modify your project's `build.gradle``` +file as follows. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +android { + lintOptions { + baseline file("lint-baseline.xml") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When you first add this line, the `lint-baseline.xml` file is +created to establish your baseline. From then on, the tools only +read the file to determine the baseline. If you want to create a new +baseline, manually delete the file and run lint again to recreate it. + +Then, run lint from the IDE (**Analyze > Inspect Code**) or from +the command line as follows. The output prints the location of the +lint-baseline.xml file. The file location for your setup might be +different from what is shown here. + +```shell + $ ./gradlew lintDebug + ... + Wrote XML report to file:///app/lint-baseline.xml + Created baseline file /app/lint-baseline.xml +``` + +Running lint records all of the current issues in the +`lint-baseline.xml` file. The set of current issues is called the +baseline, and you can check the lint-baseline.xml file into version +control if you want to share it with others. + +## Customize the baseline + +If you want to add some issue types to the baseline, but not all of +them, you can specify the issues to add by editing your project's +build.gradle, as follows. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +android { + lintOptions { + check 'NewApi', 'HandlerLeak' + baseline file("lint-baseline.xml") + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After you create the baseline, if you add any new warnings to the +codebase, lint lists only the newly introduced bugs. + +## Baseline warning + +When baselines are in effect, you get an informational warning that +tells you that one or more issues were filtered out because they +were already listed in the baseline. The reason for this warning is +to help you remember that you have configured a baseline, because +ideally, you would want to fix all of the issues at some point. + +This informational warning does not only tell you the exact number of +errors and warnings that were filtered out, it also keeps track of +issues that are not reported anymore. This information lets you know +if you have actually fixed issues, so you can optionally re-create +the baseline to prevent the error from coming back undetected. + +!!! Note: Baselines are enabled when you run inspections in batch + mode in the IDE, but they are ignored for the in-editor checks that + run in the background when you are editing a file. The reason is that + baselines are intended for the case where a codebase has a massive + number of existing warnings, but you do want to fix issues locally + while you touch the code. + +[Official documentation](https://developer.android.com/studio/write/lint#snapshot) + + diff --git a/docs/usage/changes.md.html b/docs/usage/changes.md.html new file mode 100644 index 00000000..7c47b7af --- /dev/null +++ b/docs/usage/changes.md.html @@ -0,0 +1,35 @@ +**Recent Changes** + +This chapter lists recent changes to lint that affect users of lint. +For information about internal improvements and API changes affecting +lint check authors, see [](../api-guide/changes.md.html). + +**7.0** + +* There are a number of new lint checks, particularly to support + Android 12. + +* Lint checks now include information for reported incidents where the + lint check came from, such as which library artifact provided it. + This should make it easier to request enhancements or file bugs + around false positives or false negatives. + +* Lint “partial analysis” mode is now integrated in lint, off by + default, but you can enable it by modifying `gradle.properties` + to include + + `android.experimental.useLintPartialAnalysis=true` + +**4.2** + +* Improved support for lint.xml configuration files. You can now + specify lint.xml files in project source folders, where the settings + will apply recursively within just that folder. You can also specify + options for detectors, and enable or disable checks for specific + clients (such as just in Gradle, or just in the IDE, and so on.) + +* Support for SARIF reports; a static analysis report file format + supported by for example GitHub, allowing the results to be + visualized in a unified way on CI servers. + + diff --git a/docs/usage/lintxml.md.html b/docs/usage/lintxml.md.html new file mode 100644 index 00000000..d4454c20 --- /dev/null +++ b/docs/usage/lintxml.md.html @@ -0,0 +1,129 @@ + + +# Configuring Using lint.xml Files + +In addition to configuring lint with command line flags or Gradle DSL +options, you can also create XML files named `lint.xml`, which lint +will look for automatically. + +Like `.gitignore` files, these can be nested, so you can for example +create a `lint.xml` file which sets the severity of an issue to error, +but then in a specific subfolder change the severity to be just a +warning. + +This chapter describes the syntax of `lint.xml` files. + +XML Syntax +=========== + +The root tag is always ``, and it can contain one or more +`` elements. Each can specify the following +attributes: `id`: The issue id the following configuration applies +to. Note that this can be a comma separated list of multiple id's, in +which case the configuration applies to all of them. It can also be +the special value “all”, which will match all issue id's. And when +configuring severity, the id is also allowed to be a category, such +as “Security”. + +`in`: Specifies that this configuration only applies when lint +runs in the given hosts. There are predefined names for various +integrations of lint; “gradle” refers to lint running in the Gradle +plugin; “studio” refers to lint running in the IDE, “cli” refers to +lint running from the command line tools “lint” binary, etc. Like +with id's, this can be a comma separated list, which makes the rule +match if the lint host is any of the listed hosts. Finally, note that +you can also add a “!” in front of each host to negate the check. For +example, to enable a check anywhere except when running in Studio, +use `in="!studio"`. + +In addition, the element can specify one or more children: + +``: Specifies a path to ignore. Can contain the +globbing character “*” to match any substring in the path. ``: Specifies either a regular expression to ignore. The +regular expression is matched against both the location of the error +and the error message itself. `