Skip to content

Commit

Permalink
Merge pull request #1754 from Adyen/chore/context-get-string-lint-rule
Browse files Browse the repository at this point in the history
Add lint rule that checks if `context.getString()` is used
  • Loading branch information
OscarSpruit authored Aug 19, 2024
2 parents 75c2227 + 395c6d2 commit 3112a75
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 0 deletions.
68 changes: 68 additions & 0 deletions lint/src/main/java/com/adyen/checkout/lint/ContextGetString.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) 2024 Adyen N.V.
*
* This file is open source and available under the MIT license. See the LICENSE file for more info.
*
* Created by oscars on 14/8/2024.
*/

package com.adyen.checkout.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.intellij.psi.PsiMethod
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.getContainingUClass

internal val CONTEXT_GET_STRING_ISSUE = Issue.create(
id = "ContextGetString",
briefDescription = "Context.getString() should not be used directly",
explanation = """
Use localizedContext.getString() instead of context.getString to make sure strings are localized correctly.
""".trimIndent().replace(Regex("(\n*)\n"), "$1"),
implementation = Implementation(ContextGetStringDetector::class.java, Scope.JAVA_FILE_SCOPE),
category = Category.I18N,
priority = 5,
severity = Severity.ERROR,
androidSpecific = true,
)

internal class ContextGetStringDetector : Detector(), Detector.UastScanner {

override fun getApplicableMethodNames(): List<String> = listOf(
"getString",
)

override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
if (!context.evaluator.isMemberInClass(method, "android.content.Context")) return

if (!isCalledInsideOfViewClass(context, node)) return

val receiver = node.receiver?.asSourceString()
// Ignore parenthesis
?.replace("(", "")
?.replace(")", "")

if (receiver != "localizedContext") {
context.report(
CONTEXT_GET_STRING_ISSUE,
node,
context.getLocation(node.receiver),
"context used instead of localizedContext",
fix()
.alternatives(
fix().replace().with("localizedContext").build(),
),
)
}
}

private fun isCalledInsideOfViewClass(context: JavaContext, node: UCallExpression): Boolean {
return context.evaluator.extendsClass(node.receiver?.getContainingUClass(), "android.view.View")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal class LintIssueRegistry : IssueRegistry() {
override val api: Int = CURRENT_API

override val issues: List<Issue> = listOf(
CONTEXT_GET_STRING_ISSUE,
NOT_ADYEN_LOG_ISSUE,
OBJECT_IN_PUBLIC_SEALED_CLASS_ISSUE,
TEXT_IN_LAYOUT_XML_ISSUE,
Expand Down
96 changes: 96 additions & 0 deletions lint/src/test/java/com/adyen/checkout/lint/ContextGetStringTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.adyen.checkout.lint

import com.android.tools.lint.checks.infrastructure.LintDetectorTest.kotlin
import com.android.tools.lint.checks.infrastructure.TestLintTask.lint
import org.junit.Test

class ContextGetStringTest {

@Test
fun whenContextGetStringIsUsedInViewClass_thenIssueIsDetected() {
lint()
.files(
CONTEXT_STUB,
VIEW_STUB,
kotlin(
"""
package test
import android.content.Context
import android.view.View
class MyView : View() {
fun initialize(context: Context) {
context.getString(0)
}
}
""".trimIndent(),
),
// Check if deep inheritance works as well
kotlin(
"""
package test
import android.content.Context
import android.view.LinearLayout
class MyLayout : LinearLayout() {
fun initialize(context: Context) {
context.getString(1)
}
}
""".trimIndent(),
),
)
.issues(CONTEXT_GET_STRING_ISSUE)
.allowMissingSdk()
.run()
.expect(
"""
src/test/MyLayout.kt:8: Error: context used instead of localizedContext [ContextGetString]
context.getString(1)
~~~~~~~
src/test/MyView.kt:8: Error: context used instead of localizedContext [ContextGetString]
context.getString(0)
~~~~~~~
2 errors, 0 warnings
""".trimIndent(),
)
.expectFixDiffs(
"""
Fix for src/test/MyLayout.kt line 8: Replace with localizedContext:
@@ -8 +8
- context.getString(1)
+ localizedContext.getString(1)
Fix for src/test/MyView.kt line 8: Replace with localizedContext:
@@ -8 +8
- context.getString(0)
+ localizedContext.getString(0)
""".trimIndent(),
)
}

companion object {

private val CONTEXT_STUB = kotlin(
"""
package android.content
class Context {
fun getString(resId: Int): String = "stub"
}
""".trimIndent(),
)

private val VIEW_STUB = kotlin(
"""
package android.view
open class View
open class ViewGroup : View()
open class LinearLayout : ViewGroup()
""",
)
}
}

0 comments on commit 3112a75

Please sign in to comment.