├── .gitattributes ├── .gitignore ├── README.md ├── Release.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties.TEMPLATE ├── settings.gradle.kts └── src ├── functionalTest └── kotlin │ └── android_lint_reporter │ └── AndroidLintReporterPluginFunctionalTest.kt └── main ├── kotlin └── android_lint_reporter │ ├── AndroidLintReporterPlugin.kt │ ├── github │ ├── GithubCommentResponse.kt │ ├── GithubPullRequestFilesResponse.kt │ ├── GithubService.kt │ └── GithubServiceInterface.kt │ ├── model │ ├── AndroidLintIssue.kt │ ├── GithubComment.kt │ ├── GithubCommit.kt │ ├── GithubIssues.kt │ ├── GithubUser.kt │ ├── Issue.kt │ └── Location.kt │ ├── parser │ ├── Parser.kt │ └── Renderer.kt │ └── util │ ├── StringExtension.kt │ └── Util.kt └── resources └── lint-results.xml /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # These are explicitly windows files and should use crlf 5 | *.bat text eol=crlf 6 | 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | 4 | # Ignore Gradle build output directory 5 | build 6 | 7 | .idea 8 | local.properties 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # AndroidLintReporter [![](https://img.shields.io/badge/latest-2.1.0-blue)](https://plugins.gradle.org/plugin/com.worker8.android_lint_reporter) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 | Android Lint Reporter Logo 6 | 7 | This is a Gradle Plugin to report Android Lint and Detekt result back to Github Pull Request. This is targeted for someone who's doing Android Development and using Github who wants to run Android Lint and Detekt on their pull request. 8 | (Currently, this plugin assumes the usage of both Android Lint and Detekt) 9 | 10 | Here is how it works using Github Actions (it can be used in other CI as well). 11 | 12 |
13 | 14 | ( 1 ) When a Pull Request is created, a Github Actions will be triggered: 15 | 16 |
17 | 18 |
19 |
20 |
21 |
22 | 23 | ( 2 ) This will produce this lint report, and it will be reported back in the Pull Request: 24 | 25 | - then you can fix the lint warnings and push again 26 | 27 | 28 | 29 |
30 | 31 | 32 |
33 | 34 | ## How to Setup 35 | There are a couple of steps needed to set up everything. 36 | 37 | #### 1. Github Actions 38 | 39 | First, we need to set up a Github Action trigger to run Detekt and Android Lint. 40 | 41 | Add a file in `.github/workflows/run-lint.yml` (you can name it anything) in the root of your project: 42 | 43 | ```yml 44 | name: Android Pull Request & Master CI 45 | 46 | on: 47 | pull_request: 48 | branches: 49 | - 'master' 50 | push: 51 | branches: 52 | - 'master' 53 | 54 | jobs: 55 | lint: 56 | name: Run Lint 57 | runs-on: ubuntu-18.04 58 | 59 | steps: 60 | - uses: actions/checkout@v1 61 | - name: setup JDK 1.8 62 | uses: actions/setup-java@v1 63 | with: 64 | java-version: 1.8 65 | - name: Run detekt 66 | run: ./gradlew detekt 67 | continue-on-error: true 68 | - name: Run Android Lint 69 | run: ./gradlew lint 70 | - name: Run Android Lint Reporter to report Lint and Detekt result to PR 71 | env: 72 | PR_NUMBER: ${{ github.event.number }} 73 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: | 75 | ./gradlew report -PgithubPullRequestId=$PR_NUMBER -PgithubToken=$GITHUB_TOKEN 76 | ``` 77 |
78 | 79 | How to get Github Token 80 |
81 | 82 | 1. Go to Github's `Settings --> Developer settings --> Generate` new token. 83 | 84 | Screen Shot 2020-03-19 at 16 13 44 85 | 86 | 2. Go to Personal Access Token, and click `Generate new token`: 87 | - Check for **Repo (all)** and **workflow** 88 | 89 | Screen Shot 2020-03-19 at 16 14 18 90 | 91 | 3. It's better to make a bot account and use the token of the bot account 92 | 93 |
94 |
95 | 96 |
97 | 98 | How to add GITHUB_TOKEN 99 | 100 | 101 | After generating the token, paste it under `Settings --> Secrets`: 102 | 103 | ![image](https://user-images.githubusercontent.com/1988156/77247261-a5166000-6c72-11ea-88b8-ab59c96c66e1.png) 104 | 105 |
106 | 107 | ### 2. Add repositories and classpath: 108 | 109 | - `build.gradle`: 110 | 111 | **Groovy** 112 | 113 | ``` 114 | buildscript { 115 | repositories { 116 | maven { 117 | url "https://plugins.gradle.org/m2/" 118 | } 119 | } 120 | dependencies { 121 | classpath "gradle.plugin.com.worker8.android_lint_reporter:android_lint_reporter:" 122 | } 123 | } 124 | ``` 125 | 126 | **Kotlin** 127 | 128 | ``` 129 | buildscript { 130 | repositories { 131 | maven { 132 | url = uri("https://plugins.gradle.org/m2/") 133 | } 134 | } 135 | dependencies { 136 | classpath("gradle.plugin.com.worker8.android_lint_reporter:android_lint_reporter:") 137 | } 138 | } 139 | ``` 140 | 141 | Note: `latest_version` can be found here: https://plugins.gradle.org/plugin/com.worker8.android_lint_reporter 142 | 143 | ### 3. Add the plugin dependency: 144 | 145 | - `app/build.gradle`: 146 | 147 | **Groovy** 148 | 149 | ``` 150 | plugins { 151 | id "com.worker8.android_lint_reporter" 152 | } 153 | android_lint_reporter { 154 | lintFilePath = "./app/build/reports/lint-results.xml" 155 | detektFilePath = ".app/build/reports/detekt_reports.xml" 156 | githubOwner = "worker8" 157 | githubRepositoryName = "AndroidLintReporter" 158 | showLog = true // optional - default to false, show extra information, will slow things down 159 | } 160 | ``` 161 | 162 | **Kotlin** 163 | 164 | ```kotlin 165 | plugins { 166 | id("com.worker8.android_lint_reporter") 167 | } 168 | android_lint_reporter { 169 | lintFilePath = "./app/build/reports/lint-results.xml" 170 | detektFilePath = ".app/build/reports/detekt_reports.xml" 171 | githubOwner = "worker8" 172 | githubRepositoryName = "AndroidLintReporter" 173 | showLog = true // optional - default to false, show extra information, will slow things down 174 | } 175 | ``` 176 | 177 | ### 4. You are ready! 178 | 179 | Try making a pull request, and you should see the Github Actions running under "Check" tab. When it's done, you should see your lint report being posted back to your pull request. 180 | 181 | 182 | ## Development 183 | 184 | For those who is interested to contribute or fork it. Here's a blog post I wrote explaining the source code of this repo: 185 | https://bloggie.io/@_junrong/the-making-of-android-lint-reporter 186 | 187 | **Suggested IDE for development:** 188 | 189 | Download Intellij Community Edition for free and open this project. 190 | 191 | **Setup Github Personal Access Token(PAT)** 192 | 193 | Prepare a file `local.properties` based on [local.properties.TEMPLATE](https://github.com/worker8/AndroidLintReporter/blob/master/local.properties.TEMPLATE) in the root directory of this project, with the following content: 194 | 195 | ``` 196 | github_token= 197 | github_owner= 198 | github_repository= 199 | ``` 200 | 201 | You can test your Github API using your PAT using cURL: 202 | ``` 203 | curl -H "Authorization: token " -H "Content-Type: application/json" --data '{"body":"test123abc"}' -X POST https://api.github.com/repos///issues//comments 204 | ``` 205 | 206 | **run Functional Test** 207 | The gist of this plugin is located in the class of `AndroidLintReporterPlugin.kt`. 208 | After setting everything up, you can run the test using this command: 209 | 210 | ``` 211 | ./gradlew functionalTest 212 | ``` 213 | 214 | To deploy: 215 | 1. Download secrets from https://plugins.gradle.org/ after logging in. 216 | 2. up version in `build.gradle.kts` 217 | 3. then run `./gradlew publishPlugin` 218 | 219 | ## Changelog 220 | Refer to https://github.com/worker8/AndroidLintReporter/releases 221 | 222 | ## License 223 | 224 | ``` 225 | Copyright 2020 Tan Jun Rong 226 | 227 | Licensed under the Apache License, Version 2.0 (the "License"); 228 | you may not use this file except in compliance with the License. 229 | You may obtain a copy of the License at 230 | 231 | http://www.apache.org/licenses/LICENSE-2.0 232 | 233 | Unless required by applicable law or agreed to in writing, software 234 | distributed under the License is distributed on an "AS IS" BASIS, 235 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 236 | See the License for the specific language governing permissions and 237 | limitations under the License. 238 | ``` 239 | -------------------------------------------------------------------------------- /Release.md: -------------------------------------------------------------------------------- 1 | Release to https://plugins.gradle.org is done by using [com.gradle.plugin-publish](https://plugins.gradle.org/plugin/com.gradle.plugin-publish). 2 | 3 | Tutorial: https://guides.gradle.org/publishing-plugins-to-gradle-plugin-portal/ 4 | 5 | **Steps** 6 | 1. Up version 7 | 2. `./gradlew publishPlugins` 8 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `java-gradle-plugin` 3 | id("com.gradle.plugin-publish") version "0.11.0" 4 | id("org.jetbrains.kotlin.jvm") version "1.3.61" 5 | kotlin("kapt") version "1.3.61" 6 | } 7 | 8 | repositories { 9 | jcenter() 10 | } 11 | 12 | object Constant { 13 | val pluginName = "AndroidLintReporterPlugin" 14 | val id = "com.worker8.android_lint_reporter" 15 | val implementationClass = "android_lint_reporter.AndroidLintReporterPlugin" 16 | val version = "2.1.0" 17 | val website = "https://github.com/worker8/AndroidLintReporter" 18 | val displayName = "Android Lint Reporter" 19 | val description = "Gradle Plugin to parse, format, report Android Lint result back to Github Pull Request using Github Actions" 20 | val tags = listOf("android", "lint", "github-actions") 21 | } 22 | 23 | object Version { 24 | val retrofit = "2.8.2" 25 | val moshi = "1.9.2" 26 | } 27 | 28 | dependencies { 29 | // Align versions of all Kotlin components 30 | implementation(platform("org.jetbrains.kotlin:kotlin-bom")) 31 | implementation("com.squareup.moshi:moshi-kotlin:${Version.moshi}") 32 | kapt("com.squareup.moshi:moshi-kotlin-codegen:${Version.moshi}") 33 | 34 | implementation("com.squareup.retrofit2:retrofit:${Version.retrofit}") 35 | implementation("com.squareup.retrofit2:converter-moshi:${Version.retrofit}") 36 | 37 | // Use the Kotlin JDK 8 standard library. 38 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") 39 | 40 | // Use the Kotlin test library. 41 | testImplementation("org.jetbrains.kotlin:kotlin-test") 42 | 43 | // Use the Kotlin JUnit integration. 44 | testImplementation("org.jetbrains.kotlin:kotlin-test-junit") 45 | } 46 | 47 | // Add a source set for the functional test suite 48 | val functionalTestSourceSet = sourceSets.create("functionalTest") { 49 | compileClasspath += sourceSets.main.get().output 50 | runtimeClasspath += sourceSets.main.get().output 51 | } 52 | 53 | gradlePlugin.testSourceSets(functionalTestSourceSet) 54 | configurations.getByName("functionalTestImplementation").extendsFrom(configurations.getByName("testImplementation")) 55 | 56 | // Add a task to run the functional tests 57 | val functionalTest by tasks.creating(Test::class) { 58 | testClassesDirs = functionalTestSourceSet.output.classesDirs 59 | classpath = functionalTestSourceSet.runtimeClasspath 60 | } 61 | 62 | val check by tasks.getting(Task::class) { 63 | // Run the functional tests as part of `check` 64 | dependsOn(functionalTest) 65 | } 66 | 67 | gradlePlugin { 68 | plugins { 69 | create(Constant.pluginName) { 70 | id = Constant.id 71 | implementationClass = Constant.implementationClass 72 | version = Constant.version 73 | } 74 | } 75 | } 76 | 77 | pluginBundle { 78 | // These settings are set for the whole plugin bundle 79 | website = Constant.website 80 | vcsUrl = Constant.website 81 | // tags and description can be set for the whole bundle here, but can also 82 | // be set / overridden in the config for specific plugins 83 | //description = "Just a friendly description for my learning!" 84 | 85 | // The plugins block can contain multiple plugin entries. 86 | // 87 | // The name for each plugin block below (greetingsPlugin, goodbyePlugin) 88 | // does not affect the plugin configuration, but they need to be unique 89 | // for each plugin. 90 | 91 | // Plugin config blocks can set the id, displayName, version, description 92 | // and tags for each plugin. 93 | 94 | // id and displayName are mandatory. 95 | // If no version is set, the project version will be used. 96 | // If no tags or description are set, the tags or description from the 97 | // pluginBundle block will be used, but they must be set in one of the 98 | // two places. 99 | 100 | (plugins) { 101 | 102 | // first plugin 103 | Constant.pluginName { 104 | // id is captured from java-gradle-plugin configuration 105 | description = Constant.description 106 | tags = Constant.tags 107 | version = Constant.version 108 | displayName = Constant.displayName 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worker8/AndroidLintReporter/38cad61f282eff347153338401742056052e757e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /local.properties.TEMPLATE: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Tue May 19 17:00:16 JST 2020 8 | 9 | # rename this file into local.properties by removing '.TEMPLATE' from the filename 10 | sdk.dir= 11 | 12 | # you don't need quotes to surround the string, e.g. github_token=ASDF123123123 13 | github_token= 14 | github_owner= 15 | github_repository= 16 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was generated by the Gradle 'init' task. 3 | * 4 | * The settings file is used to specify which projects to include in your build. 5 | * 6 | * Detailed information about configuring a multi-project build in Gradle can be found 7 | * in the user manual at https://docs.gradle.org/6.2.2/userguide/multi_project_builds.html 8 | */ 9 | 10 | rootProject.name = "android_lint_reporter" 11 | -------------------------------------------------------------------------------- /src/functionalTest/kotlin/android_lint_reporter/AndroidLintReporterPluginFunctionalTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Kotlin source file was generated by the Gradle 'init' task. 3 | */ 4 | package android_lint_reporter 5 | 6 | import org.gradle.internal.impldep.org.junit.Assert.assertTrue 7 | import java.io.File 8 | import org.gradle.testkit.runner.GradleRunner 9 | import java.io.FileInputStream 10 | import java.io.FileNotFoundException 11 | import java.lang.Exception 12 | import java.lang.StringBuilder 13 | import java.util.* 14 | import kotlin.test.Test 15 | import kotlin.test.assertSame 16 | 17 | class AndroidLintReporterPluginFunctionalTest { 18 | val noLocalPropertiesErrorMessage: String by lazy { 19 | val sb = StringBuilder().apply { 20 | appendln("github_token property cannot be found in local.properties") 21 | appendln("please prepare local.properties in the root directory") 22 | appendln("and set the following content:") 23 | appendln(" github_token=\"abcdefgh123456\"") 24 | appendln(" github_owner=\"worker8(replace with your project username)\"") 25 | appendln(" github_repository_name=\"SampleProjectName\"") 26 | appendln("otherwise, this functional test will fail because it needs a github personal access token to work") 27 | } 28 | sb.toString() 29 | } 30 | 31 | @Test 32 | fun `can run task`() { 33 | // Setup the test build 34 | val projectDir = File("./build/functionalTest") 35 | projectDir.mkdirs() 36 | projectDir.resolve("settings.gradle").writeText("") 37 | projectDir.resolve("build.gradle").writeText(""" 38 | plugins { 39 | id('com.worker8.android_lint_reporter') 40 | } 41 | android_lint_reporter { 42 | lintFilePath = "${File("").absolutePath}/lint-results.xml" 43 | detektFilePath = "${File("").absolutePath}/detekt_report.xml" 44 | githubOwner= "${getProperty("github_owner")}" 45 | githubRepositoryName = "${getProperty("github_repository")}" 46 | showLog = true 47 | } 48 | """) 49 | 50 | // Run the build 51 | val runner = GradleRunner.create() 52 | runner.forwardOutput() 53 | runner.withPluginClasspath() 54 | println("getProperty(\"github_token\"): ${getProperty("github_token")}") 55 | println("getProperty(\"github_owner\"): ${getProperty("github_owner")}") 56 | println("getProperty(\"github_repository\"): ${getProperty("github_repository")}") 57 | runner.withArguments(listOf("report", "-PgithubToken=${getProperty("github_token")}", "-PisDebug=true", "-PgithubPullRequestId=366", "--stacktrace")) 58 | runner.withProjectDir(projectDir) 59 | try { 60 | val result = runner.build() 61 | println("function test ended with the following result: ${result.output}") 62 | } catch (e: Exception) { 63 | e.printStackTrace() 64 | } 65 | 66 | assertTrue(true) 67 | } 68 | 69 | private fun getProperty(propertyName: String): String { 70 | val props = getProperties() 71 | if (props[propertyName] == null) { 72 | error(noLocalPropertiesErrorMessage) 73 | } 74 | return props[propertyName] as String 75 | } 76 | 77 | private fun getProperties(): Properties { 78 | val props = Properties() 79 | val localPropertyFile: File 80 | try { 81 | localPropertyFile = File("local.properties") 82 | props.load(FileInputStream(localPropertyFile)) 83 | } catch (e: FileNotFoundException) { 84 | error(noLocalPropertiesErrorMessage) 85 | } 86 | return props 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/AndroidLintReporterPlugin.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This Kotlin source file was generated by the Gradle 'init' task. 3 | */ 4 | package android_lint_reporter 5 | 6 | import android_lint_reporter.github.GithubService 7 | import android_lint_reporter.model.Issue 8 | import android_lint_reporter.parser.Parser 9 | import android_lint_reporter.parser.Renderer 10 | import android_lint_reporter.util.print 11 | import android_lint_reporter.util.printLog 12 | import org.gradle.api.Plugin 13 | import org.gradle.api.Project 14 | import java.io.File 15 | import java.lang.NumberFormatException 16 | import java.util.* 17 | 18 | open class AndroidLintReporterPluginExtension( 19 | var lintFilePath: String = "", 20 | var detektFilePath: String = "", 21 | var githubOwner: String = "", 22 | var githubRepositoryName: String = "", 23 | var showLog: Boolean = false 24 | ) 25 | 26 | class AndroidLintReporterPlugin : Plugin { 27 | override fun apply(project: Project) { 28 | val extension = project.extensions.create("android_lint_reporter", AndroidLintReporterPluginExtension::class.java) 29 | project.tasks.register("report") { task -> 30 | task.doLast { 31 | val projectProperties = project.properties 32 | val githubPullRequestId = projectProperties["githubPullRequestId"] as String 33 | val githubToken = projectProperties["githubToken"] as String 34 | val isDebug = projectProperties["isDebug"] as? String 35 | val projectRootDir = if (isDebug?.toBoolean() == true) { 36 | // replace this with your CI root environment for testing 37 | "/home/runner/work/${extension.githubRepositoryName}/${extension.githubRepositoryName}/" 38 | } else { 39 | project.rootProject.projectDir.path + "/" 40 | } 41 | // uncomment - for debugging path 42 | // val fileTreeWalk = File("./").walkTopDown() 43 | // fileTreeWalk.forEach { 44 | // if (it.name.contains("lint-results.xml")) { 45 | // printLog("path: ${it.absolutePath}") 46 | // } 47 | // } 48 | val service = GithubService.create( 49 | githubToken = githubToken, 50 | username = extension.githubOwner, 51 | repoName = extension.githubRepositoryName, 52 | pullRequestId = githubPullRequestId 53 | ) 54 | val botUsername = service.getUser().execute().body()?.login 55 | if (extension.lintFilePath.length > 1 && extension.lintFilePath[0] == '.') { 56 | // example: this is to replace "./src/main/resources/lint-results.xml" into "/src/main/resources/lint-results.xml" 57 | extension.lintFilePath = "${project.projectDir.absolutePath}${extension.lintFilePath.substring(1)}" 58 | } 59 | if (extension.detektFilePath.length > 1 && extension.detektFilePath[0] == '.') { 60 | // example: this is to replace "./src/main/resources/detekt_report.xml" into "/src/main/resources/detekt_report.xml" 61 | extension.detektFilePath = "${project.projectDir.absolutePath}${extension.detektFilePath.substring(1)}" 62 | } 63 | 64 | /* parse lint issues */ 65 | val githubIssues = Parser.parse(File(extension.lintFilePath)) 66 | val detektIssues = Parser.parseDetektXml(File(extension.detektFilePath)) 67 | printLog("Number of Android Lint Issues: ${githubIssues.size}") 68 | printLog("Number of Detekt Issues: ${detektIssues.size}") 69 | val combinedLineHashMap = hashMapOf>() 70 | val combinedIssueHashMap = hashMapOf() 71 | printLog("ddw projectRootDir: ${projectRootDir}, project.rootProject.projectDir.path: ${project.rootProject.projectDir.path}") 72 | (detektIssues + githubIssues).forEach { issue -> 73 | val filename = issue.file.replace(projectRootDir, "") 74 | val set = combinedLineHashMap[filename] ?: mutableSetOf() 75 | // TODO: handle the case where there's no line number: send a table accumulating all the errors/warnings as a separate comment 76 | val line = issue.line ?: -1 77 | try { 78 | set.add(line) 79 | } catch (e: NumberFormatException) { 80 | // for image files, like asdf.png, it doesn't have lines, so it will cause NumberFormatException 81 | // add -1 in that case 82 | set.add(-1) 83 | } 84 | combinedIssueHashMap[lintIssueKey(filename, line)] = issue 85 | combinedLineHashMap[filename] = set 86 | } 87 | try { 88 | /* get Pull Request files */ 89 | val prFileResponse = service.getPullRequestFiles().execute() 90 | val files = prFileResponse.body()!! 91 | val fileHashMap = hashMapOf>() 92 | files.forEach { githubPullRequestFilesResponse -> 93 | val patch = githubPullRequestFilesResponse.patch 94 | val regex = """@@ -(\d+),(\d+) \+(\d+),(\d+) @@""".toRegex() 95 | val matchGroups = regex.findAll(patch) 96 | val treeMap = TreeMap() // line change start, how many lines 97 | matchGroups.forEach { value -> 98 | treeMap[value.groupValues[3].toInt()] = value.groupValues[3].toInt() + value.groupValues[4].toInt() - 1 99 | } 100 | fileHashMap[githubPullRequestFilesResponse.filename] = treeMap 101 | } 102 | if (extension.showLog) { 103 | printLog("----Files change in this Pull Request----") 104 | fileHashMap.entries.forEach { (filename, treeMap) -> 105 | if (treeMap.isNotEmpty()) { 106 | val pairString = treeMap.map { (a, b) -> 107 | "($a, $b)" 108 | }.reduce { acc, s -> "$acc, $s" } 109 | printLog("$filename -> $pairString") 110 | } 111 | } 112 | } 113 | 114 | /* get all comments from a pull request */ 115 | // commentHashMap is used to check for duplicated comments 116 | val commentHashMap = hashMapOf>() // commentHashMap[filename] -> line number 117 | val commentResults = service.getPullRequestComments().execute() 118 | commentResults.body()?.forEach { comment -> 119 | comment.line?.let { commentLine -> 120 | if (botUsername == comment.user.login) { 121 | val set = commentHashMap[comment.path] ?: mutableSetOf() 122 | set.add(commentLine) 123 | commentHashMap[comment.path] = set 124 | } 125 | } 126 | } 127 | printLog("Number of comments found in PR: ${commentHashMap.size}") 128 | if (extension.showLog) { 129 | printLog("----List of Comments in this Pull Request----") 130 | commentHashMap.forEach { (filename, lineSet) -> 131 | printLog("$filename -> ${lineSet.print()}") 132 | } 133 | if (commentHashMap.isEmpty()) { 134 | printLog("0 comments found in this PR...") 135 | } 136 | } 137 | /* check if lint issues are introduced in the files in this Pull Request */ 138 | /* then check the comments to see if was previously posted, to prevent duplication */ 139 | if (extension.showLog) { 140 | printLog("----List of Issues Locations----") 141 | } 142 | combinedLineHashMap.forEach { (filename, lineSet) -> 143 | lineSet.forEach { lintLine -> 144 | // if violated lint file is introduced in this PR, it will be found in fileHashMap 145 | if (extension.showLog) { 146 | val issue = combinedIssueHashMap[lintIssueKey(filename, lintLine)] 147 | printLog("$filename:$lintLine (${issue?.reporter})") 148 | } 149 | if (fileHashMap.find(filename, lintLine) && commentHashMap[filename]?.contains(lintLine) != true) { 150 | // post to github as a review comment 151 | val issue = combinedIssueHashMap[lintIssueKey(filename, lintLine)] 152 | if (extension.showLog) { 153 | printLog("new issue found: $issue") 154 | } 155 | if (issue?.message != null) { 156 | try { 157 | val commitResult = service.getPullRequestCommits().execute() 158 | commitResult.body()?.last()?.sha?.let { lastCommitId -> 159 | val postReviewCommitResult = service.postReviewComment( 160 | bodyString = Renderer.render(issue), 161 | lineNumber = issue.line ?: 0, 162 | path = issue.file.replace(projectRootDir, ""), 163 | commitId = lastCommitId 164 | ).execute() 165 | if (postReviewCommitResult.isSuccessful) { 166 | printLog("report successfully posted to Pull Request #$githubPullRequestId") 167 | } else { 168 | printLog("Result cannot be posted due to error: ${postReviewCommitResult.errorBody()?.string()}") 169 | } 170 | } 171 | } catch (e: Exception) { 172 | printLog("posting comment failed :(\n error mesage: ${e.message}") 173 | e.printStackTrace() 174 | } 175 | } 176 | } 177 | } 178 | } 179 | } catch (e: Exception) { 180 | printLog("error msg: ${e.message}") 181 | e.printStackTrace() 182 | } 183 | } 184 | } 185 | } 186 | 187 | private fun lintIssueKey(filename: String, line: Int): String { 188 | return "$filename:${line}" 189 | } 190 | } 191 | 192 | fun HashMap>.find(targetFilename: String, targetLine: Int): Boolean { 193 | val entry: MutableMap.MutableEntry? = this[targetFilename]?.floorEntry(targetLine) 194 | if (entry != null && (entry.key <= targetLine && entry.value >= targetLine)) { 195 | // found! 196 | return true 197 | } 198 | return false 199 | } 200 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/github/GithubCommentResponse.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.github 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class GithubCommentResponse(val id: Long, val url: String) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/github/GithubPullRequestFilesResponse.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.github 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class GithubPullRequestFilesResponse( 7 | val sha: String, 8 | val filename: String, 9 | val status: String, 10 | val additions: Int, 11 | val deletions: Int, 12 | val changes: Int, 13 | val blob_url: String?, // this can be null for changes in submodule 14 | val raw_url: String?, // this can be null for changes in submodule 15 | val contents_url: String, 16 | val patch: String = "" 17 | ) 18 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/github/GithubService.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.github 2 | 3 | import android_lint_reporter.model.GithubComment 4 | import android_lint_reporter.model.GithubCommit 5 | import android_lint_reporter.model.GithubUser 6 | import com.squareup.moshi.Moshi 7 | import okhttp3.Interceptor 8 | import okhttp3.RequestBody 9 | import retrofit2.Call 10 | import retrofit2.Retrofit 11 | import retrofit2.converter.moshi.MoshiConverterFactory 12 | 13 | class GithubService( 14 | val service: GithubServiceInterface, 15 | val username: String, 16 | val repoName: String, 17 | val pullRequestId: String 18 | ) { 19 | fun getUser(): Call { 20 | return service.getUser() 21 | } 22 | 23 | fun postComment(bodyString: String): Call { 24 | val requestBody = RequestBody.create( 25 | okhttp3.MediaType.parse("application/json"), 26 | "{\"body\":\"${bodyString}\"}" 27 | ) 28 | return service.postComment(username, repoName, pullRequestId, requestBody) 29 | } 30 | 31 | fun postReviewComment(bodyString: String, lineNumber: Int, path: String, commitId: String): Call { 32 | val requestBody = RequestBody.create( 33 | okhttp3.MediaType.parse("application/json"), 34 | """{ 35 | "body": "$bodyString", 36 | "line": $lineNumber, 37 | "side": "RIGHT", 38 | "position": 1, 39 | "path": "$path", 40 | "commit_id": "$commitId" 41 | } 42 | """.trimIndent() 43 | ) 44 | return service.postReviewComment(username, repoName, pullRequestId, requestBody) 45 | } 46 | 47 | fun getPullRequestFiles(): Call> { 48 | return service.getPullRequestFiles(username, repoName, pullRequestId) 49 | } 50 | 51 | fun getPullRequestComments(): Call> { 52 | return service.getPullRequestComments(username, repoName, pullRequestId) 53 | } 54 | 55 | fun getPullRequestCommits(): Call> { 56 | return service.getPullRequestCommits(username, repoName, pullRequestId) 57 | } 58 | 59 | companion object { 60 | private const val GithubApiBaseUrl = "https://api.github.com" 61 | fun create( 62 | githubToken: String, 63 | username: String, 64 | repoName: String, 65 | pullRequestId: String 66 | ): GithubService { 67 | val interceptor = Interceptor { chain -> 68 | val newRequest = 69 | chain.request().newBuilder().addHeader( 70 | "Authorization", 71 | "token ${githubToken}" 72 | ) 73 | .build() 74 | chain.proceed(newRequest) 75 | } 76 | val okHttpClient = okhttp3.OkHttpClient.Builder() 77 | .addNetworkInterceptor(interceptor).build() 78 | 79 | val retrofit = Retrofit.Builder() 80 | .baseUrl(GithubApiBaseUrl) 81 | .client(okHttpClient) 82 | .addConverterFactory(MoshiConverterFactory.create(Moshi.Builder().build())) 83 | .build() 84 | return GithubService( 85 | service = retrofit.create(GithubServiceInterface::class.java), 86 | username = username, 87 | repoName = repoName, 88 | pullRequestId = pullRequestId 89 | ) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/github/GithubServiceInterface.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.github 2 | 3 | import android_lint_reporter.model.GithubComment 4 | import android_lint_reporter.model.GithubCommit 5 | import android_lint_reporter.model.GithubUser 6 | import okhttp3.RequestBody 7 | import retrofit2.Call 8 | import retrofit2.http.* 9 | 10 | interface GithubServiceInterface { 11 | @POST("repos/{username}/{repoName}/issues/{prId}/comments") 12 | fun postComment( 13 | @Path("username") username: String, 14 | @Path("repoName") repoName: String, 15 | @Path("prId") prId: String, 16 | @Body body: RequestBody 17 | ): Call 18 | 19 | @POST("repos/{username}/{repoName}/pulls/{prId}/comments") 20 | fun postReviewComment( 21 | @Path("username") username: String, 22 | @Path("repoName") repoName: String, 23 | @Path("prId") prId: String, 24 | @Body body: RequestBody 25 | ): Call 26 | 27 | @GET("repos/{username}/{repoName}/pulls/{prId}/files") 28 | fun getPullRequestFiles( 29 | @Path("username") username: String, 30 | @Path("repoName") repoName: String, 31 | @Path("prId") prId: String 32 | ): Call> 33 | 34 | @GET("repos/{username}/{repoName}/pulls/{prId}/comments") 35 | fun getPullRequestComments( 36 | @Path("username") username: String, 37 | @Path("repoName") repoName: String, 38 | @Path("prId") prId: String 39 | ): Call> 40 | 41 | @GET("repos/{username}/{repoName}/pulls/{prId}/commits?per_page=250") 42 | fun getPullRequestCommits( 43 | @Path("username") username: String, 44 | @Path("repoName") repoName: String, 45 | @Path("prId") prId: String 46 | ): Call> 47 | 48 | @GET("user") 49 | fun getUser(): Call 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/AndroidLintIssue.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | 3 | //data class AndroidLintIssue( 4 | // val id: String, 5 | // val severity: String, 6 | // val message: String, 7 | // val category: String, 8 | // val priority: String, 9 | // val summary: String, 10 | // val explanation: String, 11 | // val errorLine1: String, 12 | // val errorLine2: String, 13 | // val location: Location 14 | //) 15 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/GithubComment.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | import com.squareup.moshi.JsonClass 3 | 4 | @JsonClass(generateAdapter = true) 5 | data class GithubComment( 6 | val url: String, 7 | val pull_request_review_id: Long?, 8 | val id: Long, 9 | val node_id: String?, 10 | val diff_hunk: String?, 11 | val path: String, 12 | val position: Int?, 13 | val original_position: Int?, 14 | val commit_id: String?, 15 | val original_commit_id: String?, 16 | val body: String, 17 | val created_at: String?, 18 | val updated_at: String?, 19 | val html_url: String?, 20 | val pull_request_url: String?, 21 | val author_association: String?, 22 | val user: GithubUser, 23 | val start_line: Int?, 24 | val original_start_line: Int?, 25 | val start_side: Int?, 26 | val line: Int?, 27 | val original_line: Int?, 28 | val side: String 29 | // val links: ... not used ... 30 | ) -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/GithubCommit.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | import com.squareup.moshi.JsonClass 3 | 4 | @JsonClass(generateAdapter = true) 5 | data class GithubCommit( 6 | val url: String, 7 | val sha: String 8 | ) -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/GithubIssues.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | 3 | //data class GithubIssues( 4 | // val errorList: MutableList = mutableListOf(), 5 | // val warningList: MutableList = mutableListOf() 6 | //) { 7 | // fun hasErrorOnly() = errorList.isNotEmpty() && warningList.isEmpty() 8 | // fun hasWarningOnly() = warningList.isNotEmpty() && errorList.isEmpty() 9 | // fun hasBothErrorAndWarning() = errorList.isNotEmpty() && warningList.isNotEmpty() 10 | // fun hasNoErrorAndWarning() = errorList.isNotEmpty() && warningList.isNotEmpty() 11 | //} 12 | 13 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/GithubUser.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | 3 | import com.squareup.moshi.JsonClass 4 | 5 | @JsonClass(generateAdapter = true) 6 | data class GithubUser( 7 | val login: String, 8 | val id: Long, 9 | val node_id: String?, 10 | val avatar_url: String?, 11 | val gravatar_id: String?, 12 | val url: String, 13 | val html_url: String?, 14 | val following_url: String?, 15 | val gists_url: String?, 16 | val starred_url: String?, 17 | val subscriptions_url: String?, 18 | val organizations_url: String?, 19 | val repos_url: String?, 20 | val events_url: String?, 21 | val received_events_url: String?, 22 | val type: String, 23 | val site_admin: Boolean? 24 | ) -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/Issue.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | 3 | sealed class Issue( 4 | open val type: Type, 5 | open val line: Int?, // this is nullable, because some changes don't have line number, e.g. a .png file 6 | open val file: String, 7 | open val message: String, 8 | open val rule: String, 9 | open val reportedBy: String 10 | ) { 11 | enum class Type { 12 | Warning, Error 13 | } 14 | 15 | data class AndroidLintIssue( 16 | override val type: Type, 17 | override val line: Int?, 18 | override val file: String, 19 | override val message: String, 20 | override val rule: String, 21 | override val reportedBy: String = "by Android Lint :robot: " 22 | ) : Issue(type, line, file, message, rule, reportedBy) 23 | 24 | data class DetektIssue( 25 | override val type: Type, 26 | override val line: Int?, 27 | override val file: String, 28 | override val message: String, 29 | override val rule: String, 30 | override val reportedBy: String = "by Detekt :mag: " 31 | ) : Issue(type, line, file, message, rule, reportedBy) 32 | 33 | val reporter 34 | get() = if (this is AndroidLintIssue) { 35 | "AndroidLintIssue" 36 | } else { 37 | "DetektIssue" 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/model/Location.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.model 2 | 3 | data class Location(val file: String, val line: String = "", val column: String = "") 4 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/parser/Parser.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.parser 2 | 3 | import android_lint_reporter.model.Issue 4 | import org.w3c.dom.Document 5 | import java.io.File 6 | import org.w3c.dom.Element 7 | import org.w3c.dom.Node 8 | import java.lang.NumberFormatException 9 | import javax.xml.parsers.DocumentBuilderFactory 10 | 11 | object Parser { 12 | fun parseDetektXml(xmlFile: File): List { 13 | val issues = mutableListOf() 14 | val document = parseDocument(xmlFile) 15 | val issuesNodeList = document.getElementsByTagName("checkstyle") 16 | if (issuesNodeList != null && issuesNodeList.length > 0) { 17 | val issuesElement = issuesNodeList.item(0) as Element 18 | val fileList = issuesElement.getElementsByTagName("file") 19 | for (x in 0 until fileList.length) { 20 | val rawFileElement = fileList.item(x) as Element 21 | val errors = rawFileElement.getElementsByTagName("error") 22 | for (i in 0 until errors.length) { 23 | val errorElement = errors.item(i) as Element 24 | val severity = if (errorElement.getAttribute("severity").equals("warning", true)) { 25 | Issue.Type.Warning 26 | } else { 27 | Issue.Type.Error 28 | } 29 | val issue = Issue.DetektIssue( 30 | type = severity, 31 | line = errorElement.getAttribute("line").toInt(), 32 | file = rawFileElement.getAttribute("name"), 33 | message = errorElement.getAttribute("message"), 34 | rule = errorElement.getAttribute("source") 35 | ) 36 | issues.add(issue) 37 | } 38 | } 39 | } 40 | 41 | return issues.toList() 42 | } 43 | 44 | fun parse(xmlFile: File): List { 45 | val documentBuilderFactory = DocumentBuilderFactory.newInstance() 46 | val documentBuilder = documentBuilderFactory.newDocumentBuilder() 47 | val document = documentBuilder.parse(xmlFile) 48 | document.documentElement.normalize() 49 | val issuesNodeList = document.getElementsByTagName("issues") 50 | val issues = mutableListOf() 51 | if (issuesNodeList != null && issuesNodeList.length > 0) { 52 | val issuesElement = issuesNodeList.item(0) as Element 53 | 54 | for (i in 0 until issuesElement.childNodes.length) { 55 | val child = issuesElement.childNodes.item(i) 56 | if (child.nodeType == Node.ELEMENT_NODE) { 57 | val element = child as Element 58 | val locationElement = 59 | element.getElementsByTagName("location").item(0) as Element 60 | val severityType = if (element.getAttribute("severity").equals("warning", true)) { 61 | Issue.Type.Warning 62 | } else { 63 | Issue.Type.Error 64 | } 65 | val line = try { 66 | locationElement.getAttribute("line")?.toInt() ?: -1 67 | } catch (e: NumberFormatException) { 68 | -1 69 | } 70 | val issue = Issue.AndroidLintIssue( 71 | type = severityType, 72 | line = line, 73 | file = locationElement.getAttribute("file"), 74 | message = element.getAttribute("message"), 75 | rule = element.getAttribute("id") 76 | ) 77 | issues.add(issue) 78 | } 79 | } 80 | } 81 | return issues 82 | } 83 | 84 | private fun parseDocument(xmlFile: File): Document { 85 | val documentBuilderFactory = DocumentBuilderFactory.newInstance() 86 | val documentBuilder = documentBuilderFactory.newDocumentBuilder() 87 | val document = documentBuilder.parse(xmlFile) 88 | document.documentElement.normalize() 89 | return document 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/parser/Renderer.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.parser 2 | 3 | import android_lint_reporter.model.Issue 4 | import android_lint_reporter.model.Issue.AndroidLintIssue 5 | import android_lint_reporter.model.Issue.DetektIssue 6 | import android_lint_reporter.util.escapeForJson 7 | import android_lint_reporter.util.removeNewLine 8 | import java.io.File 9 | 10 | object Renderer { 11 | fun render(issue: Issue): String { 12 | val icon = when (issue.type) { 13 | Issue.Type.Error -> { 14 | "Error   :no_entry_sign:" 15 | } 16 | Issue.Type.Warning -> { 17 | "Warning :warning:  " 18 | } 19 | } 20 | return """ 21 | | | **${icon}** | 22 | | :--- | :--- | 23 | | :books: | ${issue.message} | 24 | | :hammer_and_wrench: | `${issue.rule}` | 25 | \n 26 |

${issue.reportedBy}

27 | """.trimIndent().escapeForJson() 28 | } 29 | 30 | // fun renderTable(githubIssues: GithubIssues): String { 31 | // var warningBody = """ 32 | //
Warnings (XXwarning_numberXX) \n 33 | //### Warnings :warning:\n 34 | //| File | Explanation | 35 | //| ---- | ----------- | 36 | //""" 37 | // 38 | // var errorBody = """ 39 | //
Errors (XXerror_numberXX) \n 40 | //### Errors :skull:\n 41 | //| File | Explanation | 42 | //| ---- | ----------- | 43 | //""" 44 | // 45 | // val currentPath = File("").getAbsolutePath() 46 | // githubIssues.warningList.forEach { issue -> 47 | // var locationString = "" 48 | // var errorLineString = "" 49 | // if (issue.location.line.isNotBlank() && issue.location.column.isNotBlank()) { 50 | // locationString = "**L${issue.location.line}:${issue.location.column}**" 51 | // errorLineString = "`${issue.errorLine1}`" 52 | // } 53 | // warningBody += """|
${ 54 | // issue.location.file.replace( 55 | // "${currentPath}/", 56 | // "" 57 | // ) 58 | // } ${locationString} ${errorLineString}
|
${issue.summary}
_${issue.explanation.removeNewLine()}_
|\n""" 59 | // } 60 | // 61 | // warningBody = 62 | // warningBody.replace("XXwarning_numberXX", githubIssues.warningList.count().toString()) 63 | // errorBody = errorBody.replace("XXerror_numberXX", githubIssues.errorList.count().toString()) 64 | // errorBody += "
" 65 | // var bodyString = "" 66 | // if (githubIssues.hasNoErrorAndWarning()) { 67 | // bodyString += "There is no warnings and errors by Android Lint! All good! :)" 68 | // } else if (githubIssues.hasWarningOnly()) { 69 | // bodyString += warningBody 70 | // } else if (githubIssues.hasErrorOnly()) { 71 | // bodyString += errorBody 72 | // } else if (githubIssues.hasBothErrorAndWarning()) { 73 | // bodyString += warningBody + "\n" + errorBody 74 | // } 75 | // return bodyString.escapeForJson() 76 | // } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/util/StringExtension.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.util 2 | 3 | fun String.escapeForJson(): String { 4 | return replace(System.lineSeparator(), "\\n").replace("\"", "'") 5 | } 6 | 7 | fun String.removeNewLine(): String { 8 | return replace("\n", " ") 9 | } 10 | -------------------------------------------------------------------------------- /src/main/kotlin/android_lint_reporter/util/Util.kt: -------------------------------------------------------------------------------- 1 | package android_lint_reporter.util 2 | 3 | internal fun printLog(s: String) { 4 | println("[Android Lint Reporter] $s") 5 | } 6 | 7 | internal fun Set.print() { 8 | map { it.toString() } 9 | .reduce { acc, s -> "$acc, $s" } 10 | .padStart(1, '[') 11 | .padEnd(1, ']') 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/lint-results.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 18 | 19 | 20 | 32 | 36 | 37 | 38 | 50 | 54 | 55 | 56 | 68 | 72 | 73 | 74 | 84 | 89 | 94 | 99 | 100 | 101 | 111 | 116 | 121 | 126 | 131 | 132 | 133 | 143 | 148 | 153 | 158 | 163 | 164 | 165 | 175 | 180 | 185 | 190 | 191 | 192 | 202 | 207 | 212 | 217 | 218 | 219 | 229 | 234 | 239 | 244 | 245 | 246 | 256 | 260 | 261 | 262 | 272 | 276 | 277 | 278 | 288 | 292 | 293 | 294 | 304 | 308 | 309 | 310 | 320 | 324 | 325 | 326 | 336 | 340 | 341 | 342 | 352 | 356 | 357 | 358 | 368 | 372 | 373 | 374 | 384 | 388 | 389 | 390 | 400 | 404 | 405 | 406 | 416 | 420 | 421 | 422 | 432 | 436 | 437 | 438 | 448 | 452 | 453 | 454 | 464 | 468 | 469 | 470 | 480 | 484 | 485 | 486 | 496 | 500 | 501 | 502 | 512 | 516 | 517 | 518 | 528 | 532 | 533 | 534 | 544 | 548 | 549 | 550 | 560 | 564 | 565 | 566 | 576 | 580 | 581 | 582 | 592 | 596 | 597 | 598 | 608 | 612 | 613 | 614 | 624 | 628 | 629 | 630 | 640 | 644 | 645 | 646 | 656 | 660 | 661 | 662 | 672 | 676 | 677 | 678 | 688 | 692 | 693 | 694 | 704 | 708 | 709 | 710 | 720 | 724 | 725 | 726 | 736 | 740 | 741 | 742 | 752 | 756 | 757 | 758 | 768 | 772 | 773 | 774 | 784 | 788 | 789 | 790 | 800 | 804 | 805 | 806 | 816 | 820 | 821 | 822 | 832 | 836 | 837 | 838 | 848 | 852 | 853 | 854 | 864 | 868 | 869 | 870 | 880 | 884 | 885 | 886 | 896 | 900 | 901 | 902 | 912 | 916 | 917 | 918 | 928 | 932 | 933 | 934 | 944 | 948 | 949 | 950 | 960 | 964 | 965 | 966 | 976 | 980 | 981 | 982 | 992 | 996 | 997 | 998 | 1008 | 1012 | 1013 | 1014 | 1024 | 1028 | 1029 | 1030 | 1040 | 1044 | 1045 | 1046 | 1056 | 1060 | 1061 | 1062 | 1072 | 1076 | 1077 | 1078 | 1088 | 1092 | 1093 | 1094 | 1104 | 1108 | 1109 | 1110 | 1120 | 1124 | 1125 | 1126 | 1136 | 1140 | 1141 | 1142 | 1152 | 1156 | 1157 | 1158 | 1168 | 1172 | 1173 | 1174 | 1184 | 1188 | 1189 | 1190 | 1200 | 1204 | 1205 | 1206 | 1216 | 1220 | 1221 | 1222 | 1232 | 1236 | 1237 | 1238 | 1248 | 1252 | 1253 | 1254 | 1264 | 1268 | 1269 | 1270 | 1280 | 1284 | 1285 | 1286 | 1296 | 1300 | 1301 | 1302 | 1312 | 1316 | 1317 | 1318 | 1328 | 1332 | 1333 | 1334 | 1344 | 1348 | 1349 | 1350 | 1360 | 1364 | 1365 | 1366 | 1376 | 1380 | 1381 | 1382 | 1392 | 1396 | 1397 | 1398 | 1408 | 1412 | 1413 | 1414 | 1424 | 1428 | 1429 | 1430 | 1440 | 1444 | 1445 | 1446 | 1456 | 1460 | 1461 | 1462 | 1472 | 1476 | 1477 | 1478 | 1488 | 1492 | 1493 | 1494 | 1504 | 1508 | 1509 | 1510 | 1520 | 1524 | 1525 | 1526 | 1536 | 1540 | 1541 | 1542 | 1552 | 1556 | 1557 | 1558 | 1568 | 1572 | 1573 | 1574 | 1584 | 1588 | 1589 | 1590 | 1600 | 1604 | 1605 | 1606 | 1616 | 1620 | 1621 | 1622 | 1632 | 1636 | 1637 | 1638 | 1648 | 1652 | 1653 | 1654 | 1664 | 1668 | 1669 | 1670 | 1680 | 1684 | 1685 | 1686 | 1696 | 1700 | 1701 | 1702 | 1712 | 1716 | 1717 | 1718 | 1728 | 1732 | 1733 | 1734 | 1744 | 1748 | 1749 | 1750 | 1760 | 1764 | 1765 | 1766 | 1776 | 1780 | 1781 | 1782 | 1792 | 1796 | 1797 | 1798 | 1808 | 1812 | 1813 | 1814 | 1824 | 1828 | 1829 | 1830 | 1840 | 1844 | 1845 | 1846 | 1856 | 1860 | 1861 | 1862 | 1872 | 1876 | 1877 | 1878 | 1888 | 1892 | 1893 | 1894 | 1904 | 1908 | 1909 | 1910 | 1920 | 1924 | 1925 | 1926 | 1936 | 1940 | 1941 | 1942 | 1952 | 1956 | 1957 | 1958 | 1968 | 1972 | 1973 | 1974 | 1984 | 1988 | 1989 | 1990 | 2000 | 2004 | 2005 | 2006 | 2016 | 2020 | 2021 | 2022 | 2032 | 2036 | 2037 | 2038 | 2048 | 2052 | 2053 | 2054 | 2064 | 2068 | 2069 | 2070 | 2080 | 2084 | 2085 | 2086 | 2096 | 2100 | 2101 | 2102 | 2112 | 2116 | 2117 | 2118 | 2128 | 2130 | 2131 | 2132 | 2142 | 2144 | 2145 | 2146 | 2156 | 2158 | 2159 | 2160 | 2170 | 2172 | 2173 | 2174 | 2184 | 2188 | 2189 | 2190 | 2200 | 2204 | 2205 | 2206 | 2216 | 2220 | 2221 | 2222 | 2223 | --------------------------------------------------------------------------------