Skip to content

Commit 65d564f

Browse files
authored
Merge pull request #344 from jmfayard/lint-updater
Add lint rules for Android projects if needed to suppress unwanted warnings/errors.
2 parents b9cc6d5 + 6022687 commit 65d564f

File tree

36 files changed

+518
-2
lines changed

36 files changed

+518
-2
lines changed

plugins/core/src/main/kotlin/de/fayard/refreshVersions/core/RefreshVersionsTask.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
package de.fayard.refreshVersions.core
22

33
import de.fayard.refreshVersions.core.internal.RefreshVersionsConfigHolder
4+
import de.fayard.refreshVersions.core.internal.RefreshVersionsConfigHolder.settings
45
import de.fayard.refreshVersions.core.internal.SettingsPluginsUpdater
6+
import de.fayard.refreshVersions.core.internal.configureLintIfRunningOnAnAndroidProject
57
import de.fayard.refreshVersions.core.internal.legacy.LegacyBootstrapUpdater
68
import de.fayard.refreshVersions.core.internal.lookupVersionCandidates
9+
import de.fayard.refreshVersions.core.internal.problems.log
710
import de.fayard.refreshVersions.core.internal.versions.VersionsPropertiesModel
811
import de.fayard.refreshVersions.core.internal.versions.writeWithNewVersions
912
import kotlinx.coroutines.*
1013
import org.gradle.api.DefaultTask
1114
import org.gradle.api.artifacts.Dependency
12-
import org.gradle.api.logging.LogLevel
1315
import org.gradle.api.tasks.Input
1416
import org.gradle.api.tasks.Optional
1517
import org.gradle.api.tasks.TaskAction
1618
import org.gradle.api.tasks.options.Option
17-
import org.gradle.api.tasks.options.OptionValues
1819
import org.gradle.util.GradleVersion
1920

2021
/**
@@ -55,6 +56,9 @@ open class RefreshVersionsTask : DefaultTask() {
5556
// will reduce the number of repositories lookups, improving performance a little more.
5657

5758
runBlocking {
59+
val lintUpdatingProblemsAsync = async {
60+
configureLintIfRunningOnAnAndroidProject(settings, RefreshVersionsConfigHolder.readVersionsMap())
61+
}
5862
val result = lookupVersionCandidates(
5963
httpClient = RefreshVersionsConfigHolder.httpClient,
6064
project = project,
@@ -77,6 +81,9 @@ open class RefreshVersionsTask : DefaultTask() {
7781
warnAboutHardcodedVersionsIfAny(result.dependenciesWithHardcodedVersions)
7882
warnAboutDynamicVersionsIfAny(result.dependenciesWithDynamicVersions)
7983
warnAboutGradleUpdateAvailableIfAny(result.gradleUpdates)
84+
lintUpdatingProblemsAsync.await().forEach { problem ->
85+
logger.log(problem)
86+
}
8087
}
8188
}
8289

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package de.fayard.refreshVersions.core.extensions.dom
2+
3+
import org.w3c.dom.Node
4+
import org.w3c.dom.NodeList
5+
6+
internal fun NodeList.asList(): List<Node> = List(length) { item(it) }

plugins/core/src/main/kotlin/de/fayard/refreshVersions/core/extensions/text/String.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,12 @@ internal fun String.substringUpTo(delimiter: Char, missingDelimiterValue: String
3737
val index = indexOf(delimiter)
3838
return if (index == -1) missingDelimiterValue else substring(0, index + 1)
3939
}
40+
41+
/**
42+
* Returns a substring up to the first occurrence of [delimiter].
43+
* If the string does not contain the delimiter, returns [missingDelimiterValue] which defaults to the original string.
44+
*/
45+
internal fun String.substringUpTo(delimiter: String, missingDelimiterValue: String = this): String {
46+
val index = indexOf(delimiter)
47+
return if (index == -1) missingDelimiterValue else substring(0, index + delimiter.length)
48+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package de.fayard.refreshVersions.core.internal
2+
3+
import de.fayard.refreshVersions.core.extensions.dom.asList
4+
import de.fayard.refreshVersions.core.extensions.text.substringUpTo
5+
import de.fayard.refreshVersions.core.internal.problems.Problem
6+
import org.gradle.api.initialization.Settings
7+
import org.w3c.dom.Document
8+
import org.w3c.dom.Element
9+
import org.w3c.dom.Node
10+
import java.io.File
11+
import javax.xml.parsers.DocumentBuilderFactory
12+
13+
/**
14+
* Android projects have Android lint enabled that will warn when the version of a dependency
15+
* is set to the version placeholder (`_`). This function will edit the `lint.xml` config file
16+
* if needed to add ignore rules for these lint warnings/errors.
17+
*
18+
* If these lint "issues" are already set to a non ignore value, they will not be edited,
19+
* but problems will be returned in the list instead.
20+
*/
21+
internal fun configureLintIfRunningOnAnAndroidProject(
22+
settings: Settings,
23+
versionsMap: Map<String, String>
24+
): List<Problem<LintUpdatingIssue>> {
25+
if ("plugin.android" !in versionsMap) return emptyList()
26+
val lintFile = settings.rootDir.resolve("lint.xml")
27+
val (newXml, problems) = attemptGettingLintXmlWithMissingRules(lintFile = lintFile)
28+
newXml?.let {
29+
lintFile.writeText(it)
30+
}
31+
return problems
32+
}
33+
34+
internal sealed class LintUpdatingIssue {
35+
36+
class UnexpectedSeverity(
37+
val issueId: String,
38+
val actualSeverity: String?
39+
) : LintUpdatingIssue() {
40+
override fun toString() = """
41+
|Expected the severity of $issueId to be "ignore" but found $actualSeverity instead.
42+
|If it's intentional, you can ignore this message, otherwise,
43+
|we recommend to correct the severity manually.
44+
""".trimMargin()
45+
}
46+
47+
class ParsingFailure(
48+
val throwable: Throwable? = null,
49+
val errorMessage: String = throwable?.message
50+
?: throwable?.toString()
51+
?: error("Expected a throwable or an explicit message")
52+
) : LintUpdatingIssue() {
53+
override fun toString() = errorMessage
54+
}
55+
56+
abstract override fun toString(): String
57+
}
58+
59+
@Suppress("SuspiciousCollectionReassignment")
60+
internal fun attemptGettingLintXmlWithMissingRules(
61+
lintFile: File? = null,
62+
lintXmlContent: String = when {
63+
lintFile == null -> error("Need lintFile or lintXmlContent to be set")
64+
lintFile.exists() -> lintFile.readText()
65+
else -> ""
66+
}
67+
): Pair<String?, List<Problem<LintUpdatingIssue>>> {
68+
69+
fun nonFatalError(issue: LintUpdatingIssue): Problem<LintUpdatingIssue> {
70+
return Problem(level = Problem.Level.Error, issue = issue, affectedFile = lintFile)
71+
}
72+
73+
if (lintXmlContent.isBlank()) {
74+
return """
75+
|<?xml version="1.0" encoding="UTF-8"?>
76+
|<lint>
77+
| <!-- Generated by `./gradlew refreshVersions` to avoid errors when using _ as a version. -->
78+
| <issue id="GradlePluginVersion" severity="ignore" />
79+
| <issue id="GradleDependency" severity="ignore" />
80+
|</lint>
81+
""".trimMargin() to emptyList()
82+
}
83+
84+
val document: Document = runCatching {
85+
DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(lintXmlContent.byteInputStream())
86+
}.getOrElse {
87+
return null to listOf(nonFatalError(issue = LintUpdatingIssue.ParsingFailure(throwable = it)))
88+
}
89+
90+
if (document.documentElement.tagName != "lint") {
91+
return null to listOf(
92+
nonFatalError(
93+
issue = LintUpdatingIssue.ParsingFailure(
94+
errorMessage = "Couldn't find the root tag named \"lint\""
95+
)
96+
)
97+
)
98+
}
99+
100+
var problems = emptyList<Problem<LintUpdatingIssue>>()
101+
102+
val nodes = document.documentElement.childNodes.asList()
103+
104+
fun findSuppressNode(issueId: String): Node? = nodes.firstOrNull {
105+
it is Element && it.isIssue(id = issueId)
106+
}?.also {
107+
check(it is Element)
108+
if (it.hasAttribute("severity", expectedValue = "ignore").not()) {
109+
problems += Problem(
110+
level = Problem.Level.Warning, issue = LintUpdatingIssue.UnexpectedSeverity(
111+
issueId = issueId,
112+
actualSeverity = it.attributes.getNamedItem("severity")?.nodeValue
113+
),
114+
affectedFile = lintFile
115+
)
116+
}
117+
}
118+
// Note: "AGP" stands for "Android Gradle Plugin"
119+
val agpErrorSuppressNodeOrNull = findSuppressNode("GradlePluginVersion")
120+
val libsVersionsWarningSuppressNode = findSuppressNode("GradleDependency")
121+
122+
123+
if (agpErrorSuppressNodeOrNull != null && libsVersionsWarningSuppressNode != null) return null to problems
124+
125+
// We are not using the javax.xml API to edit the xml file because it messes-up the formatting by adding
126+
// unwanted empty lines after every tag, and working around that issue is overly complicated:
127+
// it requires feeding a schema, which doesn't seemed to exist, and we are not going to reverse-engineer it.
128+
// This text insertion logic is good enough
129+
val newLintXmlFile = buildString {
130+
val commentText = "Generated by `./gradlew refreshVersions` to avoid errors when using _ as a version."
131+
val firstPart = lintXmlContent.substringUpTo("<lint>")
132+
val secondPart = lintXmlContent.substringAfter("<lint>")
133+
append(firstPart)
134+
appendln()
135+
appendln(" <!-- $commentText -->")
136+
if (agpErrorSuppressNodeOrNull == null) {
137+
append(""" <issue id="GradlePluginVersion" severity="ignore" />""")
138+
}
139+
if (libsVersionsWarningSuppressNode == null) {
140+
if (agpErrorSuppressNodeOrNull == null) appendln()
141+
append(""" <issue id="GradleDependency" severity="ignore" />""")
142+
}
143+
144+
append(secondPart)
145+
}
146+
return newLintXmlFile to problems
147+
}
148+
149+
private fun Element.isIssue(id: String): Boolean {
150+
return tagName == "issue" && hasAttribute(name = "id", expectedValue = id)
151+
}
152+
153+
private fun Element.hasAttribute(name: String, expectedValue: String): Boolean {
154+
@Suppress("RedundantLambdaArrow") // Used to specify that the type is nullable.
155+
return attributes.getNamedItem(name).let { it: Node? ->
156+
it != null && (expectedValue == it.nodeValue)
157+
}
158+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package de.fayard.refreshVersions.core.internal.problems
2+
3+
import java.io.File
4+
5+
internal data class Problem<out T>(
6+
val level: Level,
7+
val issue: T,
8+
val errorMessage: String = issue.toString(),
9+
val affectedFile: File? = null
10+
) {
11+
12+
sealed class Level {
13+
object Warning : Level()
14+
sealed class Error : Level() {
15+
companion object : Error()
16+
object Fatal : Error()
17+
}
18+
}
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package de.fayard.refreshVersions.core.internal.problems
2+
3+
import org.gradle.api.logging.LogLevel
4+
import org.gradle.api.logging.Logger
5+
6+
internal fun Logger.log(problem: Problem<Any>) {
7+
val logLevel = when (problem.level) {
8+
Problem.Level.Warning -> LogLevel.WARN
9+
Problem.Level.Error -> LogLevel.ERROR
10+
Problem.Level.Error.Fatal -> LogLevel.ERROR
11+
}
12+
val levelLetter = when (problem.level) {
13+
Problem.Level.Warning -> 'w'
14+
is Problem.Level.Error -> 'e'
15+
}
16+
val affectedFile = problem.affectedFile
17+
val message = problem.errorMessage
18+
when (affectedFile) {
19+
null -> log(logLevel, message)
20+
else -> {
21+
// This log level format is recognized by IntelliJ IDEA.
22+
log(logLevel, "$levelLetter: ${affectedFile.path}:\n$message")
23+
}
24+
}
25+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package de.fayard.refreshVersions.core
2+
3+
import de.fayard.refreshVersions.core.internal.LintUpdatingIssue
4+
import de.fayard.refreshVersions.core.internal.attemptGettingLintXmlWithMissingRules
5+
import io.kotest.matchers.shouldBe
6+
import org.junit.jupiter.api.TestFactory
7+
import testutils.junit.mapDynamicTest
8+
import kotlin.test.Test
9+
import kotlin.test.assertTrue
10+
11+
class LintUpdaterTest {
12+
13+
private val testDataDir = testResources.resolve("lint-updater")
14+
15+
@TestFactory
16+
fun `test lint updater`() = testDataDir.resolve("no-problem").listFiles { file ->
17+
file.isDirectory
18+
}!!.asList().mapDynamicTest { directory ->
19+
require(directory.isDirectory)
20+
val input = directory.resolve("input.xml").readText()
21+
val expected = directory.resolve("expected.xml").readText()
22+
val (updatedXml, _) = attemptGettingLintXmlWithMissingRules(lintXmlContent = input)
23+
(updatedXml ?: input).trim() shouldBe expected.trim()
24+
}
25+
26+
@TestFactory
27+
fun `test updating lint files with one wrong severity`() =
28+
testDataDir.resolve("one-wrong-severity").listFiles { file ->
29+
file.isDirectory
30+
}!!.asList().mapDynamicTest { directory ->
31+
require(directory.isDirectory)
32+
val input = directory.resolve("input.xml").readText()
33+
val expected = directory.resolve("expected.xml").readText()
34+
val (updatedXml, problems) = attemptGettingLintXmlWithMissingRules(lintXmlContent = input)
35+
(updatedXml ?: input).trim() shouldBe expected.trim()
36+
problems.size shouldBe 1
37+
assertTrue(problems.single().issue is LintUpdatingIssue.UnexpectedSeverity)
38+
}
39+
40+
@TestFactory
41+
fun `test updating lint files with two wrong severity`() =
42+
testDataDir.resolve("two-wrong-severity").listFiles { file ->
43+
file.isDirectory
44+
}!!.asList().mapDynamicTest { directory ->
45+
require(directory.isDirectory)
46+
val input = directory.resolve("input.xml").readText()
47+
val expected = directory.resolve("expected.xml").readText()
48+
val (updatedXml, problems) = attemptGettingLintXmlWithMissingRules(lintXmlContent = input)
49+
(updatedXml ?: input).trim() shouldBe expected.trim()
50+
problems.size shouldBe 2
51+
problems.forEach {
52+
assertTrue(it.issue is LintUpdatingIssue.UnexpectedSeverity)
53+
}
54+
}
55+
56+
@Test
57+
fun `wrong root tag returns a problem`() {
58+
val input = """
59+
|<?xml version="1.0" encoding="UTF-8"?>
60+
|<mint>
61+
| <!-- Generated by `./gradlew refreshVersions` to avoid errors when using _ as a version. -->
62+
| <issue id="GradlePluginVersion" severity="ignore" />
63+
| <issue id="GradleDependency" severity="ignore" />
64+
|</mint>
65+
""".trimMargin()
66+
val (updatedXml, problems) = attemptGettingLintXmlWithMissingRules(lintXmlContent = input)
67+
updatedXml shouldBe null
68+
problems.size shouldBe 1
69+
assertTrue(problems.single().issue is LintUpdatingIssue.ParsingFailure)
70+
}
71+
72+
@Test
73+
fun `improper xml lint file returns a problem`() {
74+
val input = """
75+
|<?xml version="1.0" encoding="UTF-8"?>
76+
|<lint>
77+
""".trimMargin()
78+
val (updatedXml, problems) = attemptGettingLintXmlWithMissingRules(lintXmlContent = input)
79+
updatedXml shouldBe null
80+
problems.size shouldBe 1
81+
assertTrue(problems.single().issue is LintUpdatingIssue.ParsingFailure)
82+
}
83+
}
84+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<lint>
3+
<issue id="InlinedApi">
4+
<ignore path="src/com.example.app/SomeFile.kt" />
5+
</issue>
6+
<issue id="NewApi">
7+
<ignore path="src/com.example.app/SomeActivity.kt" />
8+
<ignore path="src/com.example.app/SomeFile.kt" />
9+
</issue>
10+
<issue id="GradleDependency" severity="ignore" />
11+
<issue id="GradlePluginVersion" severity="ignore" />
12+
</lint>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<lint>
3+
<issue id="InlinedApi">
4+
<ignore path="src/com.example.app/SomeFile.kt" />
5+
</issue>
6+
<issue id="NewApi">
7+
<ignore path="src/com.example.app/SomeActivity.kt" />
8+
<ignore path="src/com.example.app/SomeFile.kt" />
9+
</issue>
10+
<issue id="GradleDependency" severity="ignore" />
11+
<issue id="GradlePluginVersion" severity="ignore" />
12+
</lint>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<lint>
3+
<!-- Generated by `./gradlew refreshVersions` to avoid errors when using _ as a version. -->
4+
<issue id="GradlePluginVersion" severity="ignore" />
5+
<issue id="MissingPermission" severity="informational" />
6+
<issue id="GradleDependency" severity="ignore" />
7+
</lint>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<lint>
3+
<issue id="MissingPermission" severity="informational" />
4+
<issue id="GradleDependency" severity="ignore" />
5+
</lint>

0 commit comments

Comments
 (0)