mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 10:58: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 {
|
||||
lintChecks(project(":lint-rules"))
|
||||
|
||||
implementation(libs.quartz) {
|
||||
exclude(group = "net.java.dev.jna")
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ coroutinesTest = "1.11.0"
|
||||
kmpTor = "2.6.0"
|
||||
kmpTorResource = "409.5.0"
|
||||
secp256k1Jni = "0.23.0"
|
||||
# Android Lint API version follows AGP: lint = agp major + 23
|
||||
lintApi = "32.2.1"
|
||||
|
||||
[libraries]
|
||||
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" }
|
||||
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
|
||||
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-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" }
|
||||
|
||||
@@ -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"
|
||||
include(":app")
|
||||
include(":lint-rules")
|
||||
|
||||
Reference in New Issue
Block a user