mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
Add LongToDuration lint check mirroring the IDE-only inspection
Android Studio's "Long overload to Duration conversion" inspection (ConvertLongToDurationInspection) only runs inside the IDE, so its findings never fail ./gradlew lint. Port it as a custom lint check in a new lint-rules module: calls to the kotlinx.coroutines timing functions (delay, withTimeout, debounce, ...) are flagged when the time argument is a Long/Int rather than a kotlin.time.Duration. Overloads cannot be told apart at the JVM level (Duration is a value class backed by long), so the check classifies the argument's Kotlin type via the bundled K2 analysis API. With warningsAsErrors already enabled, the pre-commit/pre-push hooks and CI now fail on any new legacy-Long call site. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ho6LW6eEBkVnnpFvr5tvpi
This commit is contained in:
@@ -212,6 +212,8 @@ ksp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
lintChecks(project(":lint-rules"))
|
||||||
|
|
||||||
implementation(libs.quartz) {
|
implementation(libs.quartz) {
|
||||||
exclude(group = "net.java.dev.jna")
|
exclude(group = "net.java.dev.jna")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ coroutinesTest = "1.11.0"
|
|||||||
kmpTor = "2.6.0"
|
kmpTor = "2.6.0"
|
||||||
kmpTorResource = "409.5.0"
|
kmpTorResource = "409.5.0"
|
||||||
secp256k1Jni = "0.23.0"
|
secp256k1Jni = "0.23.0"
|
||||||
|
# Android Lint API version follows AGP: lint = agp major + 23
|
||||||
|
lintApi = "32.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
|
||||||
@@ -80,6 +82,7 @@ paging-compose = { group = "androidx.paging", name = "paging-compose", version.r
|
|||||||
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingCommon" }
|
paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "pagingCommon" }
|
||||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||||
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" }
|
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutinesTest" }
|
||||||
|
lint-api = { module = "com.android.tools.lint:lint-api", version.ref = "lintApi" }
|
||||||
kmptor-runtime = { module = "io.matthewnelson.kmp-tor:runtime", version.ref = "kmpTor" }
|
kmptor-runtime = { module = "io.matthewnelson.kmp-tor:runtime", version.ref = "kmpTor" }
|
||||||
kmptor-resource-exec = { module = "io.matthewnelson.kmp-tor:resource-exec-tor", version.ref = "kmpTorResource" }
|
kmptor-resource-exec = { module = "io.matthewnelson.kmp-tor:resource-exec-tor", version.ref = "kmpTorResource" }
|
||||||
secp256k1-jni-jvm = { module = "fr.acinq.secp256k1:secp256k1-kmp-jni-jvm", version.ref = "secp256k1Jni" }
|
secp256k1-jni-jvm = { module = "fr.acinq.secp256k1:secp256k1-kmp-jni-jvm", version.ref = "secp256k1Jni" }
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
plugins {
|
||||||
|
// No version: AGP's built-in Kotlin already puts the plugin on the build classpath
|
||||||
|
id("org.jetbrains.kotlin.jvm")
|
||||||
|
alias(libs.plugins.gradle.ktlint) version libs.versions.ktlint.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvmToolchain(21)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compileOnly(libs.lint.api)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.jar {
|
||||||
|
manifest {
|
||||||
|
attributes("Lint-Registry-v2" to "com.greenart7c3.nostrsigner.lint.AmberIssueRegistry")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.client.api.IssueRegistry
|
||||||
|
import com.android.tools.lint.client.api.Vendor
|
||||||
|
import com.android.tools.lint.detector.api.CURRENT_API
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
|
||||||
|
class AmberIssueRegistry : IssueRegistry() {
|
||||||
|
override val issues: List<Issue> = listOf(
|
||||||
|
LongToDurationDetector.ISSUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val api: Int = CURRENT_API
|
||||||
|
|
||||||
|
override val vendor: Vendor = Vendor(
|
||||||
|
vendorName = "Amber",
|
||||||
|
feedbackUrl = "https://github.com/greenart7c3/Amber/issues",
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.greenart7c3.nostrsigner.lint
|
||||||
|
|
||||||
|
import com.android.tools.lint.detector.api.Category
|
||||||
|
import com.android.tools.lint.detector.api.Detector
|
||||||
|
import com.android.tools.lint.detector.api.Implementation
|
||||||
|
import com.android.tools.lint.detector.api.Issue
|
||||||
|
import com.android.tools.lint.detector.api.JavaContext
|
||||||
|
import com.android.tools.lint.detector.api.Scope
|
||||||
|
import com.android.tools.lint.detector.api.Severity
|
||||||
|
import com.android.tools.lint.detector.api.SourceCodeScanner
|
||||||
|
import com.intellij.psi.PsiMethod
|
||||||
|
import com.intellij.psi.PsiTypes
|
||||||
|
import org.jetbrains.kotlin.analysis.api.analyze
|
||||||
|
import org.jetbrains.kotlin.analysis.api.types.KaClassType
|
||||||
|
import org.jetbrains.kotlin.psi.KtExpression
|
||||||
|
import org.jetbrains.uast.UCallExpression
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build-time port of Android Studio's "Long overload to Duration conversion" IDE
|
||||||
|
* inspection (ConvertLongToDurationInspection), which only runs inside the IDE.
|
||||||
|
* Flags calls that resolve to the legacy Long-millisecond overloads of the
|
||||||
|
* kotlinx.coroutines timing functions so they get migrated to the
|
||||||
|
* kotlin.time.Duration overloads.
|
||||||
|
*/
|
||||||
|
class LongToDurationDetector :
|
||||||
|
Detector(),
|
||||||
|
SourceCodeScanner {
|
||||||
|
override fun getApplicableMethodNames(): List<String> = listOf(
|
||||||
|
"delay",
|
||||||
|
"withTimeout",
|
||||||
|
"withTimeoutOrNull",
|
||||||
|
"debounce",
|
||||||
|
"sample",
|
||||||
|
"throttle",
|
||||||
|
"timeout",
|
||||||
|
"onTimeout",
|
||||||
|
"advanceTimeBy",
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
|
||||||
|
val packageName = context.evaluator.getPackage(method)?.qualifiedName ?: return
|
||||||
|
if (!packageName.startsWith("kotlinx.coroutines")) return
|
||||||
|
// The legacy overloads take the time value as a Long. Duration is a value
|
||||||
|
// class backed by a JVM long, so the Duration overloads erase to the same
|
||||||
|
// JVM signature and PSI resolution cannot tell them apart — classify by the
|
||||||
|
// Kotlin type of the argument at the call site instead.
|
||||||
|
if (method.parameterList.parameters.none { it.type == PsiTypes.longType() }) return
|
||||||
|
for (argument in node.valueArguments) {
|
||||||
|
val ktExpression = argument.sourcePsi as? KtExpression ?: continue
|
||||||
|
val argumentClassFqName = try {
|
||||||
|
analyze(ktExpression) {
|
||||||
|
(ktExpression.expressionType as? KaClassType)?.classId?.asFqNameString()
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
// Analysis session unavailable — cannot classify the overload
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (argumentClassFqName) {
|
||||||
|
// Already migrated to the Duration overload
|
||||||
|
"kotlin.time.Duration" -> return
|
||||||
|
"kotlin.Long", "kotlin.Int" -> {
|
||||||
|
context.report(
|
||||||
|
ISSUE,
|
||||||
|
node,
|
||||||
|
context.getCallLocation(node, includeReceiver = false, includeArguments = true),
|
||||||
|
"Legacy `Long` overload can be converted to `Duration`",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ISSUE = Issue.create(
|
||||||
|
id = "LongToDuration",
|
||||||
|
briefDescription = "Legacy Long overload can be converted to Duration",
|
||||||
|
explanation = """
|
||||||
|
kotlinx.coroutines timing functions (`delay`, `withTimeout`, `debounce`, …) \
|
||||||
|
have `kotlin.time.Duration` overloads that are preferred over the legacy \
|
||||||
|
Long-millisecond overloads. Replace e.g. `delay(3000)` with \
|
||||||
|
`delay(3.seconds)` using `kotlin.time.Duration.Companion` extensions.
|
||||||
|
""",
|
||||||
|
category = Category.CORRECTNESS,
|
||||||
|
priority = 5,
|
||||||
|
severity = Severity.WARNING,
|
||||||
|
implementation = Implementation(LongToDurationDetector::class.java, Scope.JAVA_FILE_SCOPE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,3 +17,4 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
rootProject.name = "Nostr Signer"
|
rootProject.name = "Nostr Signer"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
include(":lint-rules")
|
||||||
|
|||||||
Reference in New Issue
Block a user