Skip to content

Commit 6584573

Browse files
committed
only report lint complaints to line change instead of reporting everything
print project root dir show hashmap values fix error up to 5 due to bug debug 6 posting to line change; stop duplicate comments up version to debug7 code cleanup + up version to 2.0.0 for release use local.properties to store github personal access token to prevent human error of accidentally committing it
1 parent 72d9576 commit 6584573

File tree

12 files changed

+2226
-1445
lines changed

12 files changed

+2226
-1445
lines changed

build.gradle.kts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ object Constant {
1313
val pluginName = "AndroidLintReporterPlugin"
1414
val id = "com.worker8.android_lint_reporter"
1515
val implementationClass = "android_lint_reporter.AndroidLintReporterPlugin"
16-
val version = "1.1.5"
16+
val version = "2.0.0"
1717
val website = "https://github.com/worker8/AndroidLintReporter"
1818
val displayName = "Android Lint Reporter"
1919
val description = "Gradle Plugin to parse, format, report Android Lint result back to Github Pull Request using Github Actions"
@@ -39,6 +39,7 @@ dependencies {
3939

4040
// Use the Kotlin test library.
4141
testImplementation("org.jetbrains.kotlin:kotlin-test")
42+
testImplementation(gradleTestKit())
4243

4344
// Use the Kotlin JUnit integration.
4445
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
@@ -59,6 +60,8 @@ val functionalTest by tasks.creating(Test::class) {
5960
classpath = functionalTestSourceSet.runtimeClasspath
6061
}
6162

63+
//tasks.create<SystemProcess>("hoodwink")
64+
6265
val check by tasks.getting(Task::class) {
6366
// Run the functional tests as part of `check`
6467
dependsOn(functionalTest)
@@ -109,3 +112,10 @@ pluginBundle {
109112
}
110113
}
111114
}
115+
116+
//open class SystemProcess @Inject constructor(): DefaultTask() {
117+
// @TaskAction
118+
// fun runCommand() {
119+
// println("aSJKDLjSDKLsj: ${gradle.gradleHomeDir}")
120+
// }
121+
//}

src/functionalTest/kotlin/android_lint_reporter/AndroidLintReporterPluginFunctionalTest.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,28 @@ package android_lint_reporter
66
import org.gradle.internal.impldep.org.junit.Assert.assertTrue
77
import java.io.File
88
import org.gradle.testkit.runner.GradleRunner
9+
import java.io.FileInputStream
10+
import java.io.FileNotFoundException
11+
import java.lang.StringBuilder
12+
import java.util.*
913
import kotlin.test.Test
1014
import kotlin.test.assertSame
1115

1216
class AndroidLintReporterPluginFunctionalTest {
17+
val noLocalPropertiesErrorMessage: String by lazy {
18+
val sb = StringBuilder().apply {
19+
appendln("github_token property cannot be found in local.properties")
20+
appendln("please prepare local.properties in the root directory")
21+
appendln("and set 'github_token=abcdefgh123456'")
22+
appendln("otherwise, this functional test will fail because it needs a github personal access token to work")
23+
}
24+
sb.toString()
25+
}
26+
1327
@Test
1428
fun `can run task`() {
1529
// Setup the test build
1630
val projectDir = File("./build/functionalTest")
17-
println("ddw projectDir: ${projectDir.absolutePath}")
1831
projectDir.mkdirs()
1932
projectDir.resolve("settings.gradle").writeText("")
2033
projectDir.resolve("build.gradle").writeText("""
@@ -23,22 +36,35 @@ class AndroidLintReporterPluginFunctionalTest {
2336
}
2437
android_lint_reporter {
2538
lintFilePath = "${File("").absolutePath}/src/main/resources/lint-results.xml"
26-
githubUsername = "worker8"
27-
githubRepositoryName = "SimpleCurrency"
39+
githubUsername = "u-next"
40+
githubRepositoryName = "UNextAndroid"
2841
}
2942
""")
3043

3144
// Run the build
3245
val runner = GradleRunner.create()
3346
runner.forwardOutput()
3447
runner.withPluginClasspath()
35-
runner.withArguments(listOf("parseAndSendLintResult", "-PgithubToken=", "-PgithubPullRequestId="))
48+
runner.withArguments(listOf("parseAndSendLintResult", "-PgithubToken=${getGithubToken()}", "-PgithubPullRequestId=360"))
3649
runner.withProjectDir(projectDir)
50+
val result = runner.build()
51+
println("output: ${result.output}")
52+
assertTrue(true)
53+
}
3754

38-
val result = runner.build();
39-
40-
System.out.println("ddw, output: ${result.output}")
55+
private fun getGithubToken(): String {
56+
val props = Properties()
57+
val localPropertyFile: File
58+
try {
59+
localPropertyFile = File("local.properties")
60+
props.load(FileInputStream(localPropertyFile))
61+
if (props["github_token"] == null) {
62+
error(noLocalPropertiesErrorMessage)
63+
}
4164

42-
assertTrue(true)
65+
} catch (e: FileNotFoundException) {
66+
error(noLocalPropertiesErrorMessage)
67+
}
68+
return props["github_token"] as String
4369
}
4470
}

src/main/kotlin/android_lint_reporter/AndroidLintReporterPlugin.kt

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,146 @@
44
package android_lint_reporter
55

66
import android_lint_reporter.github.GithubService
7+
import android_lint_reporter.model.Issue
78
import android_lint_reporter.parser.Parser
89
import android_lint_reporter.parser.Renderer
910
import org.gradle.api.Plugin
1011
import org.gradle.api.Project
1112
import java.io.File
13+
import java.lang.NumberFormatException
14+
import java.util.*
1215

1316
open class AndroidLintReporterPluginExtension(
1417
var lintFilePath: String = "",
1518
var githubUsername: String = "",
16-
var githubRepositoryName: String = "")
19+
var githubRepositoryName: String = ""
20+
)
1721

1822
class AndroidLintReporterPlugin : Plugin<Project> {
1923
override fun apply(project: Project) {
2024
val extension = project.extensions.create("android_lint_reporter", AndroidLintReporterPluginExtension::class.java)
2125
project.tasks.register("parseAndSendLintResult") { task ->
2226
task.doLast {
23-
println("received extension: ${extension.githubUsername}/${extension.githubRepositoryName}")
27+
val DEBUG = false
2428
val projectProperties = project.properties
2529
val githubPullRequestId = projectProperties["githubPullRequestId"] as String
2630
val githubToken = projectProperties["githubToken"] as String
31+
val projectRootDir = if (DEBUG) {
32+
// replace this with your CI root environment for testing
33+
"/home/runner/work/UNextAndroid/UNextAndroid/"
34+
} else {
35+
project.rootProject.projectDir
36+
}
2737
// for debugging path
2838
// val fileTreeWalk = File("./").walkTopDown()
2939
// fileTreeWalk.forEach {
3040
// if (it.name.contains("lint-results.xml")) {
3141
// println("path: ${it.absolutePath}")
3242
// }
3343
// }
34-
if (extension.lintFilePath.length > 1 && extension.lintFilePath[0] == '.') {
35-
// example: this is to replace "./src/main/resources/lint-results.xml" into "<projectDir>/src/main/resources/lint-results.xml"
36-
extension.lintFilePath = "${project.projectDir.absolutePath}${extension.lintFilePath.substring(1)}"
37-
}
38-
val issues = Parser.parse(File(extension.lintFilePath))
39-
val bodyString = Renderer.render(issues)
40-
4144
val service = GithubService.create(
4245
githubToken = githubToken,
4346
username = extension.githubUsername,
4447
repoName = extension.githubRepositoryName,
4548
pullRequestId = githubPullRequestId
4649
)
50+
val botUsername = service.getUser().execute().body()?.login
51+
if (extension.lintFilePath.length > 1 && extension.lintFilePath[0] == '.') {
52+
// example: this is to replace "./src/main/resources/lint-results.xml" into "<projectDir>/src/main/resources/lint-results.xml"
53+
extension.lintFilePath = "${project.projectDir.absolutePath}${extension.lintFilePath.substring(1)}"
54+
}
55+
/* parse lint issues */
56+
val issues = Parser.parse(File(extension.lintFilePath))
57+
val lintHashMap = hashMapOf<String, MutableSet<Int>>()
58+
val lintIssueHashMap = hashMapOf<String, Issue>()
59+
issues.warningList.forEach { value ->
60+
val filename = value.location.file.replace("${projectRootDir}/", "")
61+
val set = lintHashMap[filename] ?: mutableSetOf()
62+
lintIssueHashMap[filename] = value
63+
try {
64+
set.add(value.location.line.toInt())
65+
} catch (e: NumberFormatException) {
66+
// for image files, like asdf.png, it doesn't have lines, so it will cause NumberFormatException
67+
// add -1 in that case
68+
set.add(-1)
69+
}
70+
lintHashMap[filename] = set
71+
}
4772

48-
// escape single backslash, which will cause json parsing to fail
49-
val regex = """\\(?!n)""".toRegex() // only escape backslash that doesn't followed by 'n', cause we want to keep '\n'
50-
val escapedString = regex.replace(bodyString, "")
51-
val response = service.postComment(escapedString).execute()
52-
if (response.isSuccessful) {
53-
println("Lint result is posted to https://github.com/${extension.githubUsername}/${extension.githubRepositoryName}/${githubPullRequestId}!")
54-
} else {
55-
println("An error has occurred... ")
56-
println("response code: ${response.code()}, message: ${response.message()}, body: ${response.errorBody()?.string()}")
73+
try {
74+
/* get Pull Request files */
75+
val prFileResponse = service.getPullRequestFiles().execute()
76+
val files = prFileResponse.body()!!
77+
val fileHashMap = hashMapOf<String, TreeMap<Int, Int>>()
78+
files.forEach { githubPullRequestFilesResponse ->
79+
val patch = githubPullRequestFilesResponse.patch
80+
val regex = """@@ -(\d+),(\d+) \+(\d+),(\d+) @@""".toRegex()
81+
val matchGroups = regex.findAll(patch)
82+
val treeMap = TreeMap<Int, Int>() // line change start, how many lines
83+
matchGroups.forEach { value ->
84+
treeMap[value.groupValues[3].toInt()] = value.groupValues[3].toInt() + value.groupValues[4].toInt() - 1
85+
}
86+
fileHashMap[githubPullRequestFilesResponse.filename] = treeMap
87+
}
88+
fileHashMap.entries.forEach { (filename, treeMap) ->
89+
if (treeMap.isNotEmpty()) {
90+
val pairString = treeMap.map { (a, b) ->
91+
"($a, $b)"
92+
}.reduce { acc, s -> "$acc, $s" }
93+
println("$filename -> $pairString")
94+
}
95+
}
96+
/* get all comments from a pull request */
97+
// commentHashMap is used to check for duplicated comments
98+
val commentHashMap = hashMapOf<String, Int>() // commentHashMap[filename] -> line number
99+
val temp = service.getPullRequestComments().execute()
100+
temp.body()?.forEach { comment ->
101+
comment.line?.let { commentLine ->
102+
if (botUsername == comment.user.login) {
103+
commentHashMap[comment.path] = commentLine
104+
}
105+
}
106+
}
107+
/* check if lint issues are introduced in the files in this Pull Request */
108+
/* then check the comments to see if was previously posted, to prevent duplication */
109+
lintHashMap.forEach { (lintFilename, lintLineSet) ->
110+
lintLineSet.forEach { lintLine ->
111+
// if violated lint file is introduced in this PR, it will be found in fileHashMap
112+
if (fileHashMap.find(lintFilename, lintLine) && commentHashMap[lintFilename] != lintLine) {
113+
// post to github as a review comment
114+
val issue = lintIssueHashMap[lintFilename]
115+
if (issue?.message != null) {
116+
try {
117+
val commitResult = service.getPullRequestCommits().execute()
118+
commitResult.body()?.last()?.sha?.let { lastCommitId ->
119+
val postReviewCommitResult = service.postReviewComment(
120+
bodyString = Renderer.render(issue),
121+
lineNumber = issue.location.line.toInt(),
122+
path = issue.location.file.replace("${projectRootDir}/", ""),
123+
commitId = lastCommitId
124+
).execute()
125+
}
126+
} catch (e: Exception) {
127+
e.printStackTrace()
128+
}
129+
}
130+
}
131+
}
132+
}
133+
} catch (e: Exception) {
134+
println("error msg: ${e.message}")
135+
e.printStackTrace()
57136
}
58137
}
59138
}
60139
}
61140
}
141+
142+
fun HashMap<String, TreeMap<Int, Int>>.find(targetFilename: String, targetLine: Int): Boolean {
143+
val entry: MutableMap.MutableEntry<Int, Int>? = this[targetFilename]?.floorEntry(targetLine)
144+
if (entry != null && (entry.key <= targetLine && entry.value >= targetLine)) {
145+
// found!
146+
return true
147+
}
148+
return false
149+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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,
14+
val raw_url: String,
15+
val contents_url: String,
16+
val patch: String = ""
17+
)

0 commit comments

Comments
 (0)