Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ivan Makarov Homework_AndroidLint #12

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lint-checks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ java {
}

dependencies {
testImplementation 'junit:junit:4.13.2'
def lintVersion = "30.2.0"
compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
testImplementation "com.android.tools.lint:lint:$lintVersion"
testImplementation "com.android.tools.lint:lint-tests:$lintVersion"
testImplementation "com.android.tools:testutils:$lintVersion"
}

jar {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package ru.otus.homework.lintchecks

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.android.tools.lint.detector.api.isKotlin
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.getParentOfType

private const val ID = "GlobalScopeUsage"

private const val BRIEF_DESCRIPTION = "Замените GlobalScope на Scope контролируемые жизненным циклом класса"
private const val EXPLANATION = "Замените GlobalScope на Scope контролируемые жизненным циклом класса. " +
"Корутины, запущенные на kotlinx.coroutines.GlobalScope нужно контролировать вне скоупа класс, в котором они созданы. " +
"Контролировать глобальные корутины неудобно, а отсутствие контроля может привести к излишнему использованию ресурсов и утечкам памяти."

private const val PRIORITY = 6

private const val GLOBAL_SCOPE_CLASS = "kotlinx.coroutines.GlobalScope"
private const val VIEW_MODEL_CLASS = "androidx.lifecycle.ViewModel"
private const val VIEW_MODEL_KTX_ARTIFACT = "androidx.lifecycle:lifecycle-viewmodel-ktx"
private const val FRAGMENT_CLASS = "androidx.fragment.app.Fragment"
private const val RUNTIME_KTX_ARTIFACT = "androidx.lifecycle:lifecycle-runtime-ktx"

private const val GLOBAL_SCOPE_TEXT = "GlobalScope"
private const val VIEW_MODEL_SCOPE_TEXT = "viewModelScope"
private const val LIFECYCLE_SCOPE_TEXT = "lifecycleScope"

class GlobalScopeUsageDetector : Detector(), Detector.UastScanner {

companion object {
val ISSUE = Issue.create(
ID,
BRIEF_DESCRIPTION,
EXPLANATION,
Category.CORRECTNESS,
PRIORITY,
Severity.WARNING,
Implementation(GlobalScopeUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}

override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(UCallExpression::class.java)
}

override fun createUastHandler(context: JavaContext): UElementHandler {
return MethodCallHandler(context)
}

}

class MethodCallHandler(private val context: JavaContext): UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
val receiverType = node.receiverType?.canonicalText

if (receiverType == GLOBAL_SCOPE_CLASS) {
val isKotlin = isKotlin(node.sourcePsi)

if (isKotlin
&& context.evaluator.extendsClass(node.getParentOfType<UClass>()?.javaPsi, VIEW_MODEL_CLASS)
&& context.evaluator.dependencies?.packageDependencies?.roots?.find { it.artifactName == VIEW_MODEL_KTX_ARTIFACT } != null
) {
context.report(
GlobalScopeUsageDetector.ISSUE,
context.getLocation(node),
BRIEF_DESCRIPTION,
createViewModelFix()
)
} else if (isKotlin
&& context.evaluator.extendsClass(node.getParentOfType<UClass>()?.javaPsi, VIEW_MODEL_CLASS)
&& context.evaluator.dependencies?.packageDependencies?.roots?.find { it.artifactName == RUNTIME_KTX_ARTIFACT } != null
) {
context.report(
GlobalScopeUsageDetector.ISSUE,
context.getLocation(node),
BRIEF_DESCRIPTION,
createFragmentFix()
)
} else {
context.report(
GlobalScopeUsageDetector.ISSUE,
context.getLocation(node),
BRIEF_DESCRIPTION
)
}
}
}

private fun createViewModelFix(): LintFix {
return LintFix.create()
.replace()
.text(GLOBAL_SCOPE_TEXT)
.with(VIEW_MODEL_SCOPE_TEXT)
.build()
}

private fun createFragmentFix(): LintFix {
return LintFix.create()
.replace()
.text(GLOBAL_SCOPE_TEXT)
.with(LIFECYCLE_SCOPE_TEXT)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package ru.otus.homework.lintchecks

import com.android.tools.lint.client.api.IssueRegistry
import com.android.tools.lint.detector.api.CURRENT_API
import com.android.tools.lint.detector.api.Issue

class HomeworkIssueRegistry : IssueRegistry() {

override val issues: List<Issue>
get() = TODO("Not yet implemented")
}
get() = listOf(GlobalScopeUsageDetector.ISSUE, JobInBuilderUsageDetector.ISSUE, RawColorUsageDetector.ISSUE)

override val api: Int
get() = CURRENT_API
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package ru.otus.homework.lintchecks

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.Location
import com.android.tools.lint.detector.api.Scope
import com.android.tools.lint.detector.api.Severity
import org.jetbrains.uast.UBinaryExpression
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.UExpression
import org.jetbrains.uast.UParenthesizedExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getParentOfType

private const val ID = "JobInBuilderUsage"

private const val BRIEF_DESCRIPTION = "Не используйте Job/SupervisorJob для передачи в корутин билдер."
private const val EXPLANATION = "Не используйте Job/SupervisorJob для передачи в корутин билдер. " +
"Хоть Job и его наследники являются элементами CoroutineContext, их использование внутри корутин-билдеров не имеет никакого эффекта, " +
"это может сломать ожидаемые обработку ошибок и механизм отмены корутин."

private const val SUPERVISOR_JOB_FIX_NAME = "Удалить SupervisorJob"

private const val PRIORITY = 6

private const val COMPLETABLE_JOB_CLASS = "kotlinx.coroutines.CompletableJob"
private const val NON_CANCELABLE_CLASS = "kotlinx.coroutines.NonCancellable"
private const val JOB_CLASS = "kotlinx.coroutines.Job"
private const val SUPERVISOR_JOB_CALL_NAME = "SupervisorJob"

private const val VIEW_MODEL_CLASS = "androidx.lifecycle.ViewModel"
private const val VIEW_MODEL_SCOPE_TEXT = "viewModelScope"

const val regexWithOperand = """\s*\+\s*"""
const val regexParenthesized = """\(\s*SupervisorJob\s*\(\s*\)\s*\)"""
const val regexDef = """SupervisorJob\s*\(\s*\)"""

class JobInBuilderUsageDetector : Detector(), Detector.UastScanner {

companion object {
val ISSUE = Issue.create(
ID,
BRIEF_DESCRIPTION,
EXPLANATION,
Category.CORRECTNESS,
PRIORITY,
Severity.WARNING,
Implementation(JobInBuilderUsageDetector::class.java, Scope.JAVA_FILE_SCOPE)
)
}

override fun getApplicableUastTypes(): List<Class<out UElement>> {
return listOf(UCallExpression::class.java)
}

override fun createUastHandler(context: JavaContext): UElementHandler {
return MethodCallHandler(context)
}

class MethodCallHandler(private val context: JavaContext) : UElementHandler() {
override fun visitCallExpression(node: UCallExpression) {
if (node.methodIdentifier?.name in listOf("launch", "async")) {
val argument = node.valueArguments.getOrNull(0)
checkArgument(argument, node)
}
}

private fun checkArgument(
argument: UExpression?,
node: UCallExpression,
isRightOperand: Boolean = false,
isLeftOperand: Boolean = false,
isParenthesized: Boolean = false
) {
when(argument) {
is UParenthesizedExpression -> {
checkArgument(
argument.expression,
node,
isRightOperand = isRightOperand,
isLeftOperand = isLeftOperand,
isParenthesized = true
)
}
is UBinaryExpression -> {
checkArgument(argument.leftOperand, node, isLeftOperand = true)
checkArgument(argument.rightOperand, node, isRightOperand = true)
}
is UCallExpression -> {
if (argument.getExpressionType()?.canonicalText == JOB_CLASS || argument.getExpressionType()?.superTypes?.any { it.canonicalText == JOB_CLASS } == true) {
val lintFix = if (
argument.classReference?.getExpressionType()?.canonicalText == COMPLETABLE_JOB_CLASS
&& argument.methodName == SUPERVISOR_JOB_CALL_NAME
&& isOnViewModelScope(node)
) {
createSupervisorJobFix(isRightOperand = isRightOperand, isLeftOperand = isLeftOperand, isParenthesized = isParenthesized)
} else null

makeReport(context.getLocation(node.valueArguments[0]), lintFix)
}
}
is UReferenceExpression -> {
if (argument.getExpressionType()?.canonicalText == JOB_CLASS || argument.getExpressionType()?.superTypes?.any { it.canonicalText == JOB_CLASS } == true) {
if (
argument.getExpressionType()?.canonicalText == NON_CANCELABLE_CLASS
&& node.receiver == null
) {
makeReport(context.getLocation(node), createNonCancelableFix(node))
} else {
makeReport(context.getLocation(argument))
}
}
}
}

}

private fun isOnViewModelScope(node: UCallExpression): Boolean {
return context.evaluator.extendsClass(node.getParentOfType<UClass>()?.javaPsi, VIEW_MODEL_CLASS)
&& node.receiver is USimpleNameReferenceExpression
&& (node.receiver as USimpleNameReferenceExpression).sourcePsi?.text == VIEW_MODEL_SCOPE_TEXT
}

private fun createSupervisorJobFix(isRightOperand: Boolean = false, isLeftOperand: Boolean = false, isParenthesized: Boolean = false): LintFix {
var replaceText = if (isParenthesized) {
regexParenthesized
} else {
regexDef
}

if (isRightOperand) {
replaceText = regexWithOperand + replaceText
}
if(isLeftOperand) {
replaceText += regexWithOperand
}

return LintFix.create()
.name(SUPERVISOR_JOB_FIX_NAME)
.replace()
.pattern(replaceText)
.with("")
.build()
}

private fun createNonCancelableFix(node: UCallExpression): LintFix {
val coroutineBuilderName = node.methodIdentifier?.name?: ""
return LintFix.create()
.name("Заменить $coroutineBuilderName на withContext")
.replace()
.text(coroutineBuilderName)
.with("withContext")
.build()
}

private fun makeReport(location: Location, lintFix: LintFix? = null) {
context.report(
ISSUE,
location,
BRIEF_DESCRIPTION,
lintFix
)
}
}

}
Loading