mirror of
https://github.com/BetterAndroid/Hikage.git
synced 2025-09-07 19:14:22 +08:00
Bump hikage-core, hikage-extension, hikage-extension-betterandroid, hikage-extension-compose, hikage-compiler, hikage-widget-androidx, hikage-widget-material version to 1.0.0
This commit is contained in:
46
hikage-core-lint/build.gradle.kts
Normal file
46
hikage-core-lint/build.gradle.kts
Normal file
@@ -0,0 +1,46 @@
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
autowire(libs.plugins.kotlin.jvm)
|
||||
}
|
||||
|
||||
group = property.project.groupName
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(17)
|
||||
compilerOptions {
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xno-param-assertions",
|
||||
"-Xno-call-assertions",
|
||||
"-Xno-receiver-assertions"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_17)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named<Jar>("jar") {
|
||||
manifest {
|
||||
attributes(
|
||||
"Lint-Registry-V2" to property.project.hikage.core.lint.registry.v2.clazz
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly(org.jetbrains.kotlin.kotlin.stdlib)
|
||||
compileOnly(com.android.tools.lint.lint.api)
|
||||
compileOnly(com.android.tools.lint.lint.checks)
|
||||
testImplementation(com.android.tools.lint.lint)
|
||||
testImplementation(com.android.tools.lint.lint.tests)
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/17.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint
|
||||
|
||||
object DeclaredSymbol {
|
||||
|
||||
const val HIKAGEABLE_ANNOTATION_CLASS = "com.highcapable.hikage.annotation.Hikageable"
|
||||
const val HIKAGE_CLASS = "com.highcapable.hikage.core.Hikage"
|
||||
const val HIKAGE_PERFORMER_CLASS = "com.highcapable.hikage.core.Hikage.Performer"
|
||||
|
||||
val HIKAGE_VIEW_REGEX = "kotlin.jvm.functions.Function1<\\?\\s*super\\s+[^,]+,kotlin.Unit>".toRegex()
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/14.
|
||||
*/
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.highcapable.hikage.core.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.highcapable.hikage.core.lint.detector.HikageSafeTypeCastDetector
|
||||
import com.highcapable.hikage.core.lint.detector.HikageableBeyondScopeDetector
|
||||
import com.highcapable.hikage.core.lint.detector.HikageableFunctionsDetector
|
||||
import com.highcapable.hikage.core.lint.detector.WidgetsUsageDetector
|
||||
import com.highcapable.hikage.generated.HikageCoreLintProperties
|
||||
|
||||
class HikageIssueRegistry : IssueRegistry() {
|
||||
|
||||
override val issues get() = listOf(
|
||||
HikageableBeyondScopeDetector.ISSUE,
|
||||
HikageableFunctionsDetector.ISSUE,
|
||||
HikageSafeTypeCastDetector.ISSUE,
|
||||
WidgetsUsageDetector.ISSUE
|
||||
)
|
||||
|
||||
override val minApi = HikageCoreLintProperties.PROJECT_HIKAGE_CORE_LINT_MIN_API
|
||||
override val api = CURRENT_API
|
||||
override val vendor = Vendor(
|
||||
vendorName = HikageCoreLintProperties.PROJECT_NAME,
|
||||
identifier = HikageCoreLintProperties.PROJECT_HIKAGE_CORE_LINT_IDENTIFIER,
|
||||
feedbackUrl = "${HikageCoreLintProperties.PROJECT_URL}/issues",
|
||||
contact = HikageCoreLintProperties.PROJECT_URL
|
||||
)
|
||||
}
|
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/4/2.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector
|
||||
|
||||
import com.android.tools.lint.client.api.UElementHandler
|
||||
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.LintFix
|
||||
import com.android.tools.lint.detector.api.Scope
|
||||
import com.android.tools.lint.detector.api.Severity
|
||||
import com.highcapable.hikage.core.lint.DeclaredSymbol
|
||||
import org.jetbrains.uast.UArrayAccessExpression
|
||||
import org.jetbrains.uast.UBinaryExpressionWithType
|
||||
import org.jetbrains.uast.UParenthesizedExpression
|
||||
import org.jetbrains.uast.UQualifiedReferenceExpression
|
||||
|
||||
class HikageSafeTypeCastDetector : Detector(), Detector.UastScanner {
|
||||
|
||||
companion object {
|
||||
|
||||
val ISSUE = Issue.create(
|
||||
id = "UseHikageSafeTypeCast",
|
||||
briefDescription = "Hikage safe type cast usage.",
|
||||
explanation = "Recommended to use `hikage.get<YourView>(\"your_id\")` instead of `hikage[\"your_id\"] as YourView`.",
|
||||
category = Category.COMPLIANCE,
|
||||
priority = 5,
|
||||
severity = Severity.WARNING,
|
||||
implementation = Implementation(
|
||||
HikageSafeTypeCastDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getApplicableUastTypes() = listOf(UQualifiedReferenceExpression::class.java, UBinaryExpressionWithType::class.java)
|
||||
|
||||
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
|
||||
|
||||
override fun visitQualifiedReferenceExpression(node: UQualifiedReferenceExpression) {
|
||||
if (node.selector !is UBinaryExpressionWithType) return
|
||||
val castExpr = node.selector as UBinaryExpressionWithType
|
||||
visitAndLint(context, castExpr, node)
|
||||
}
|
||||
|
||||
override fun visitBinaryExpressionWithType(node: UBinaryExpressionWithType) {
|
||||
visitAndLint(context, node)
|
||||
}
|
||||
|
||||
private fun visitAndLint(
|
||||
context: JavaContext,
|
||||
node: UBinaryExpressionWithType,
|
||||
parent: UQualifiedReferenceExpression? = null
|
||||
) {
|
||||
// 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"]`.
|
||||
val receiverText = receiver.sourcePsi?.text ?: return
|
||||
// Like `hikage`.
|
||||
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'")
|
||||
.replace()
|
||||
.range(location)
|
||||
.with(replacement)
|
||||
.reformat(true)
|
||||
.build()
|
||||
context.report(
|
||||
ISSUE, locationNode, location,
|
||||
message = "Can be replaced with safe type cast `$replaceSuggestion`.",
|
||||
quickfixData = lintFix
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/17.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector
|
||||
|
||||
import com.android.tools.lint.client.api.UElementHandler
|
||||
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.LintFix
|
||||
import com.android.tools.lint.detector.api.Scope
|
||||
import com.android.tools.lint.detector.api.Severity
|
||||
import com.highcapable.hikage.core.lint.DeclaredSymbol
|
||||
import com.highcapable.hikage.core.lint.detector.entity.ReportDetail
|
||||
import com.highcapable.hikage.core.lint.detector.extension.hasHikageable
|
||||
import com.intellij.psi.PsiMethod
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.kotlin.psi.KtExpression
|
||||
import org.jetbrains.kotlin.psi.KtLambdaArgument
|
||||
import org.jetbrains.kotlin.psi.KtLambdaExpression
|
||||
import org.jetbrains.kotlin.psi.KtValueArgument
|
||||
import org.jetbrains.uast.UCallExpression
|
||||
import org.jetbrains.uast.toUElementOfType
|
||||
|
||||
class HikageableBeyondScopeDetector : Detector(), Detector.UastScanner {
|
||||
|
||||
companion object {
|
||||
|
||||
val ISSUE = Issue.create(
|
||||
id = "HikageableBeyondScope",
|
||||
briefDescription = "Hikageable beyond scope.",
|
||||
explanation = "Functions marked with `@Hikageable` can only be passed in `Hikage.Performer`.",
|
||||
category = Category.COMPLIANCE,
|
||||
priority = 10,
|
||||
severity = Severity.ERROR,
|
||||
implementation = Implementation(
|
||||
HikageableBeyondScopeDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getApplicableUastTypes() = listOf(UCallExpression::class.java)
|
||||
|
||||
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
|
||||
|
||||
private val reportedNodes = mutableSetOf<UCallExpression>()
|
||||
private val reports = mutableListOf<ReportDetail>()
|
||||
|
||||
override fun visitCallExpression(node: UCallExpression) {
|
||||
val callExpr = node.sourcePsi as? KtCallExpression ?: return
|
||||
val method = node.resolve() ?: return
|
||||
startLint(callExpr, method)
|
||||
organizeAndReport()
|
||||
}
|
||||
|
||||
private fun startLint(callExpr: KtCallExpression, method: PsiMethod) {
|
||||
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)
|
||||
}
|
||||
|
||||
private fun organizeAndReport() {
|
||||
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")
|
||||
.replace()
|
||||
.range(location)
|
||||
// Delete the call expression.
|
||||
.with("")
|
||||
.build()
|
||||
context.report(ISSUE, it.callExpr, location, it.message, lintFix)
|
||||
reportedNodes.add(it.callExpr)
|
||||
}
|
||||
}
|
||||
|
||||
private fun visitAndLint(callExpr: KtCallExpression, method: PsiMethod) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/17.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector
|
||||
|
||||
import com.android.tools.lint.client.api.UElementHandler
|
||||
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.LintFix
|
||||
import com.android.tools.lint.detector.api.Scope
|
||||
import com.android.tools.lint.detector.api.Severity
|
||||
import com.highcapable.hikage.core.lint.DeclaredSymbol
|
||||
import com.highcapable.hikage.core.lint.detector.extension.hasHikageable
|
||||
import com.intellij.psi.PsiMethod
|
||||
import org.jetbrains.uast.UBlockExpression
|
||||
import org.jetbrains.uast.UCallExpression
|
||||
import org.jetbrains.uast.UMethod
|
||||
import org.jetbrains.uast.UReturnExpression
|
||||
import org.jetbrains.uast.tryResolve
|
||||
|
||||
class HikageableFunctionsDetector : Detector(), Detector.UastScanner {
|
||||
|
||||
companion object {
|
||||
|
||||
val ISSUE = Issue.create(
|
||||
id = "HikageableFunctions",
|
||||
briefDescription = "Hikageable functions.",
|
||||
explanation = "Functions which invoke `@Hikageable` functions must be marked with the `@Hikageable` annotation.",
|
||||
category = Category.COMPLIANCE,
|
||||
priority = 10,
|
||||
severity = Severity.ERROR,
|
||||
implementation = Implementation(
|
||||
HikageableFunctionsDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE
|
||||
)
|
||||
)
|
||||
|
||||
private val functionRegex = "(\\s?.+)?fun\\s?".toRegex()
|
||||
}
|
||||
|
||||
override fun getApplicableUastTypes() = listOf(UMethod::class.java)
|
||||
|
||||
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
|
||||
|
||||
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
|
||||
is UReturnExpression ->
|
||||
(it.returnExpression?.tryResolve() as? PsiMethod?)?.hasHikageable() ?: false
|
||||
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()
|
||||
.range(location)
|
||||
.with(replacement)
|
||||
.imports(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS)
|
||||
.reformat(true)
|
||||
.build()
|
||||
context.report(ISSUE, node, nameLocation, message, lintFix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/17.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector
|
||||
|
||||
import com.android.tools.lint.client.api.UElementHandler
|
||||
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.LintFix
|
||||
import com.android.tools.lint.detector.api.Scope
|
||||
import com.android.tools.lint.detector.api.Severity
|
||||
import com.highcapable.hikage.core.lint.detector.extension.hasHikageable
|
||||
import com.intellij.psi.PsiMethod
|
||||
import org.jetbrains.kotlin.psi.KtCallExpression
|
||||
import org.jetbrains.uast.UCallExpression
|
||||
import org.jetbrains.uast.UElement
|
||||
import org.jetbrains.uast.UImportStatement
|
||||
import org.jetbrains.uast.toUElement
|
||||
|
||||
class WidgetsUsageDetector : Detector(), Detector.UastScanner {
|
||||
|
||||
companion object {
|
||||
|
||||
val ISSUE = Issue.create(
|
||||
id = "ReplaceWithHikageWidgets",
|
||||
briefDescription = "Hikage built-in widget usability.",
|
||||
explanation = "Use the built-in widget function component provided by Hikage like `TextView(...)` " +
|
||||
"without using a form like `View<TextView>(...)` to use the component.",
|
||||
category = Category.USABILITY,
|
||||
priority = 5,
|
||||
severity = Severity.WARNING,
|
||||
implementation = Implementation(
|
||||
WidgetsUsageDetector::class.java,
|
||||
Scope.JAVA_FILE_SCOPE
|
||||
)
|
||||
)
|
||||
|
||||
private const val BUILT_IN_WIDGETS_PACKAGE_PREFIX = "android.widget"
|
||||
private const val WIDGET_FUNCTION_PREFIX = "com.highcapable.hikage.widget.$BUILT_IN_WIDGETS_PACKAGE_PREFIX"
|
||||
private const val VIEW_CLASS_NAME = "android.view.View"
|
||||
private const val VIEW_GROUP_CLASS_NAME = "android.view.ViewGroup"
|
||||
|
||||
private val viewExpressionRegex = "(?:View|ViewGroup)<.*?>".toRegex()
|
||||
|
||||
private val builtInWidgets = listOf(
|
||||
"SeekBar",
|
||||
"LinearLayout",
|
||||
"ScrollView",
|
||||
"TextView",
|
||||
"EditText",
|
||||
"AutoCompleteTextView",
|
||||
"ExpandableListView",
|
||||
"ListView",
|
||||
"RatingBar",
|
||||
"ViewSwitcher",
|
||||
"ActionMenuView",
|
||||
"ImageView",
|
||||
"ViewAnimator",
|
||||
"HorizontalScrollView",
|
||||
"MediaController",
|
||||
"RelativeLayout",
|
||||
"TextClock",
|
||||
"CalendarView",
|
||||
"ToggleButton",
|
||||
"RadioGroup",
|
||||
"VideoView",
|
||||
"GridView",
|
||||
"QuickContactBadge",
|
||||
"TableLayout",
|
||||
"NumberPicker",
|
||||
"Toolbar",
|
||||
"ViewFlipper",
|
||||
"Chronometer",
|
||||
"ImageSwitcher",
|
||||
"Button",
|
||||
"CheckBox",
|
||||
"TabWidget",
|
||||
"TabHost",
|
||||
"SearchView",
|
||||
"Spinner",
|
||||
"TimePicker",
|
||||
"ImageButton",
|
||||
"TextSwitcher",
|
||||
"DatePicker",
|
||||
"RadioButton",
|
||||
"CheckedTextView",
|
||||
"FrameLayout",
|
||||
"Space",
|
||||
"GridLayout",
|
||||
"Switch",
|
||||
"ProgressBar",
|
||||
"TableRow"
|
||||
)
|
||||
}
|
||||
|
||||
data class ImportReference(val packagePrefix: String, val name: String, val alias: String? = null)
|
||||
|
||||
override fun getApplicableUastTypes(): List<Class<out UElement>> = listOf(UImportStatement::class.java, UCallExpression::class.java)
|
||||
|
||||
override fun createUastHandler(context: JavaContext) = object : UElementHandler() {
|
||||
|
||||
private val importReferences = mutableSetOf<ImportReference>()
|
||||
|
||||
override fun visitImportStatement(node: UImportStatement) {
|
||||
val imported = node.asSourceString().replace("import", "").let {
|
||||
when {
|
||||
it.contains("//") -> it.split("//")[0]
|
||||
it.contains("/*") -> it.split("/*")[0]
|
||||
else -> it
|
||||
}
|
||||
}.trim()
|
||||
val importRefs = imported.split(" as ")
|
||||
val alias = if (importRefs.size > 1)
|
||||
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)
|
||||
}
|
||||
|
||||
private fun startLint(callExpr: KtCallExpression, method: PsiMethod) {
|
||||
val hasHikageable = method.hasHikageable()
|
||||
if (hasHikageable) visitAndReport(callExpr, method)
|
||||
}
|
||||
|
||||
private fun visitAndReport(callExpr: KtCallExpression, method: PsiMethod) {
|
||||
val typeParameters = method.typeParameterList?.typeParameters ?: emptyArray()
|
||||
val typeArguments = callExpr.typeArgumentList?.arguments ?: emptyList()
|
||||
val typeArgumentsText = typeArguments.mapNotNull { it.text }
|
||||
val typedViewFunctionIndex = typeParameters.indexOfFirst {
|
||||
it.extendsListTypes.any { type ->
|
||||
type.canonicalText == VIEW_CLASS_NAME ||
|
||||
type.canonicalText == VIEW_GROUP_CLASS_NAME
|
||||
}
|
||||
}
|
||||
val isTypedViewFunction = typedViewFunctionIndex >= 0
|
||||
val imports = typeArgumentsText.mapNotNull { typeName ->
|
||||
when {
|
||||
// Like `TextView`.
|
||||
!typeName.contains(".") -> importReferences.firstOrNull {
|
||||
it.packagePrefix == BUILT_IN_WIDGETS_PACKAGE_PREFIX &&
|
||||
(it.alias == typeName || it.name == typeName)
|
||||
}
|
||||
// Like `android.widget.TextView`.
|
||||
typeName.startsWith(BUILT_IN_WIDGETS_PACKAGE_PREFIX) ->
|
||||
ImportReference(BUILT_IN_WIDGETS_PACKAGE_PREFIX, typeName.replace("$BUILT_IN_WIDGETS_PACKAGE_PREFIX.", ""))
|
||||
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()
|
||||
.range(sourceLocation)
|
||||
.with(replacement)
|
||||
.imports("$WIDGET_FUNCTION_PREFIX.$widgetName")
|
||||
.reformat(true)
|
||||
.build()
|
||||
val message = "Can be simplified to `$widgetName`."
|
||||
context.report(ISSUE, callExpr, nameLocation, message, lintFix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/17.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector.entity
|
||||
|
||||
import org.jetbrains.uast.UCallExpression
|
||||
|
||||
data class ReportDetail(
|
||||
val message: String,
|
||||
val callExpr: UCallExpression
|
||||
)
|
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Hikage - An Android responsive UI building tool.
|
||||
* Copyright (C) 2019 HighCapable
|
||||
* https://github.com/BetterAndroid/Hikage
|
||||
*
|
||||
* Apache License Version 2.0
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* This file is created by fankes on 2025/3/30.
|
||||
*/
|
||||
package com.highcapable.hikage.core.lint.detector.extension
|
||||
|
||||
import com.highcapable.hikage.core.lint.DeclaredSymbol
|
||||
import com.intellij.psi.PsiMethod
|
||||
|
||||
fun PsiMethod.hasHikageable() = hasAnnotation(DeclaredSymbol.HIKAGEABLE_ANNOTATION_CLASS)
|
Reference in New Issue
Block a user