refactor: merge to BetterAndroid new usage

This commit is contained in:
2025-08-03 23:27:46 +08:00
parent 8990e03e97
commit ee97222bcb
15 changed files with 163 additions and 25 deletions

View File

@@ -60,6 +60,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
if (node.selector !is UBinaryExpressionWithType) return
val castExpr = node.selector as UBinaryExpressionWithType
visitAndLint(context, castExpr, node)
}
@@ -75,11 +76,14 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
) {
// Get the parent node, if it is wrapped with brackets will also be included.
val locationNode = node.uastParent as? UParenthesizedExpression ?: node
val receiver = parent?.receiver ?: node.operand
val receiverType = (node.operand as? UArrayAccessExpression)?.receiver?.getExpressionType() ?: return
val receiverClass = receiverType.canonicalText
// Filter retains results that meet the conditions.
if (receiverClass != DeclaredSymbol.HIKAGE_CLASS) return
// Like `hikage["your_id"] as YourView`.
val exprText = node.sourcePsi?.text ?: return
// Like `hikage["your_id"]`.
@@ -88,11 +92,15 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
val receiverNameText = receiverText.split("[")[0]
// Like `"your_id"`.
val receiverContent = runCatching { receiverText.split("[")[1].split("]")[0] }.getOrNull() ?: return
val isSafeCast = exprText.contains("as?") || exprText.endsWith("?")
// Like `YourView`.
val castTypeContent = node.typeReference?.sourcePsi?.text?.removeSuffix("?") ?: return
val replacement = "$receiverNameText.${if (isSafeCast) "getOrNull" else "get"}<$castTypeContent>($receiverContent)"
val replaceSuggestion = if (isSafeCast) "Hikage.getOrNull<$castTypeContent>" else "Hikage.get<$castTypeContent>"
val location = context.getLocation(locationNode)
val lintFix = LintFix.create()
.name("Replace with '$replacement'")
@@ -101,6 +109,7 @@ class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
.with(replacement)
.reformat(true)
.build()
context.report(
ISSUE, locationNode, location,
message = "Can be replaced with safe type cast `$replaceSuggestion`.",

View File

@@ -70,6 +70,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
override fun visitCallExpression(node: UCallExpression) {
val callExpr = node.sourcePsi as? KtCallExpression ?: return
val method = node.resolve() ?: return
startLint(callExpr, method)
organizeAndReport()
}
@@ -78,6 +79,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
val className = method.containingClass?.qualifiedName ?: ""
val hasHikageable = method.hasHikageable()
val hasLayoutParams = className == DeclaredSymbol.HIKAGE_PERFORMER_CLASS && method.name == "LayoutParams"
if (hasHikageable || hasLayoutParams) visitAndLint(callExpr, method)
}
@@ -85,6 +87,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
reports.forEach {
// Check if the call has been reported before reporting.
if (reportedNodes.contains(it.callExpr)) return@forEach
val location = context.getLocation(it.callExpr)
val lintFix = LintFix.create()
.name("Delete Call Expression")
@@ -93,6 +96,7 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
// Delete the call expression.
.with("")
.build()
context.report(ISSUE, it.callExpr, location, it.message, lintFix)
reportedNodes.add(it.callExpr)
}
@@ -102,28 +106,37 @@ class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
val bodyBlocks = mutableMapOf<String, KtExpression>()
val parameters = method.parameterList.parameters
val valueArguments = callExpr.valueArgumentList?.arguments ?: emptyList()
fun visitValueArg(arg: KtValueArgument) {
val name = arg.getArgumentName()?.asName?.identifier ?: ""
val expr = arg.getArgumentExpression()
val parameter = parameters.firstOrNull { it.name == name }
// If the last bit is a lambda expression, then `parameter` must have a lambda parameter defined by the last bit.
?: if (arg is KtLambdaArgument) parameters.lastOrNull() else null
val isMatched = parameter?.type?.canonicalText?.matches(DeclaredSymbol.HIKAGE_VIEW_REGEX) == true &&
!parameter.type.canonicalText.contains(DeclaredSymbol.HIKAGE_PERFORMER_CLASS)
if (expr is KtLambdaExpression && isMatched)
expr.bodyExpression?.let { bodyBlocks[name] = it }
}
// Get the last lambda expression.
val lastLambda = callExpr.lambdaArguments.lastOrNull()
if (lastLambda != null) visitValueArg(lastLambda)
valueArguments.forEach { arg -> visitValueArg(arg) }
bodyBlocks.toList().flatMap { (_, value) -> value.children.filterIsInstance<KtCallExpression>() }.forEach {
val expression = it.toUElementOfType<UCallExpression>() ?: return@forEach
val sCallExpr = expression.sourcePsi as? KtCallExpression ?: return@forEach
val sMethod = expression.resolve() ?: return@forEach
if (sMethod.hasHikageable()) {
val message = "Performers are not allowed to appear in `${method.name}` DSL creation process."
reports.add(ReportDetail(message, expression))
// Recursively to visit next level.
visitAndLint(sCallExpr, sMethod)
}

View File

@@ -65,6 +65,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
override fun visitMethod(node: UMethod) {
val uastBody = node.uastBody as? UBlockExpression ?: return
val bodyHasHikageable = uastBody.expressions.any {
when (it) {
is UCallExpression -> it.resolve()?.hasHikageable() ?: false
@@ -73,17 +74,22 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
else -> false
}
}
if (!node.hasHikageable() && bodyHasHikageable) {
val location = context.getLocation(node)
val nameLocation = context.getNameLocation(node)
val message = "Function `${node.name}` must be marked with the `@Hikageable` annotation."
val functionText = node.asSourceString()
val hasDoubleSlash = functionText.startsWith("//")
val replacement = functionRegex.replace(functionText) { result ->
val functionBody = result.groupValues.getOrNull(0) ?: functionText
val prefix = if (hasDoubleSlash) "\n" else ""
"$prefix@Hikageable $functionBody"
}
val lintFix = LintFix.create()
.name("Add '@Hikageable' to '${node.name}'")
.replace()
@@ -92,6 +98,7 @@ class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
.imports(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS)
.reformat(true)
.build()
context.report(ISSUE, node, nameLocation, message, lintFix)
}
}

View File

@@ -135,18 +135,23 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
importRefs.getOrNull(1)?.trim()
else null
val importPrefix = importRefs[0]
val hasPrefix = importPrefix.contains(".")
val name = if (hasPrefix)
importPrefix.split(".").last()
else importPrefix
val packagePrefix = importPrefix.replace(if (hasPrefix) ".$name" else name, "")
val reference = ImportReference(packagePrefix, name, alias)
importReferences.add(reference)
}
override fun visitCallExpression(node: UCallExpression) {
val callExpr = node.sourcePsi as? KtCallExpression ?: return
val method = node.resolve() ?: return
startLint(callExpr, method)
}
@@ -165,7 +170,9 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
type.canonicalText == VIEW_GROUP_CLASS_NAME
}
}
val isTypedViewFunction = typedViewFunctionIndex >= 0
val imports = typeArgumentsText.mapNotNull { typeName ->
when {
// Like `TextView`.
@@ -179,18 +186,24 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
else -> null
}
}
val matchedIndex = builtInWidgets.indexOfFirst { imports.any { e -> e.alias == it || e.name == it } }
val isBuiltInWidget = matchedIndex >= 0
if (isTypedViewFunction && isBuiltInWidget) {
val widgetName = builtInWidgets[matchedIndex]
val sourceLocation = context.getLocation(callExpr)
val sourceText = callExpr.toUElement()?.asSourceString() ?: return
val callExprElement = callExpr.toUElement() ?: return
// Matchs '>' and like `View<TextView`'s length + 1.
val callExprLength = sourceText.split(">")[0].trim().length + 1
val nameLocation = context.getRangeLocation(callExprElement, fromDelta = 0, callExprLength)
// Only replace the first one, because there may be multiple sub-functions in DSL.
val replacement = sourceText.replaceFirst(viewExpressionRegex, widgetName)
val lintFix = LintFix.create()
.name("Replace with '$widgetName'")
.replace()
@@ -199,6 +212,7 @@ class WidgetsUsageDetector : Detector(), Detector.UastScanner {
.imports("$WIDGET_FUNCTION_PREFIX.$widgetName")
.reformat(true)
.build()
val message = "Can be simplified to `$widgetName`."
context.report(ISSUE, callExpr, nameLocation, message, lintFix)
}