├── .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://plugins.gradle.org/plugin/com.worker8.android_lint_reporter) [](https://opensource.org/licenses/MIT)
4 |
5 |
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 |
85 |
86 | 2. Go to Personal Access Token, and click `Generate new token`:
87 | - Check for **Repo (all)** and **workflow**
88 |
89 |
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 | 
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 |
--------------------------------------------------------------------------------