Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Moshi Lint Check
/*
* Copyright (C) 2021 Remco Mokveld
*
* 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.
*/
package com.squareup.moshi.lint
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.intellij.lang.jvm.JvmModifier
import org.jetbrains.kotlin.asJava.elements.KtLightField
import org.jetbrains.kotlin.psi.psiUtil.visibilityModifierType
import org.jetbrains.uast.UClass
import org.jetbrains.uast.UElement
import org.jetbrains.uast.ULiteralExpression
import org.jetbrains.uast.kotlin.KotlinUClass
/**
* Lint [Detector] for finding private, non-transient, properties on classes which get moshi JsonAdapters generated.
* This is not allowed because the generated class must be able to access the field without using reflection, which
* means the field must be public, or internal
*/
class JsonClassDetector : Detector(), Detector.UastScanner {
override fun getApplicableUastTypes(): List<Class<out UElement>>? = listOf(UClass::class.java)
override fun createUastHandler(context: JavaContext): UElementHandler? {
return JsonClassElementHandler(context)
}
private class JsonClassElementHandler(private val context: JavaContext) : UElementHandler() {
override fun visitClass(node: UClass) {
if (node is KotlinUClass) {
node.findAnnotation("com.squareup.moshi.JsonClass")?.let { annotation ->
val expression = (annotation.attributeValues.first().expression as ULiteralExpression)
if (expression.value == true)
checkClass(node)
}
}
}
private fun checkClass(kotlinUClass: KotlinUClass) {
kotlinUClass.allFields.filterIsInstance<KtLightField>().forEach { field ->
if (field.isPrivateOrProtected && !field.isTransient) {
if (field.containingClass.qualifiedName == kotlinUClass.qualifiedName)
reportPrivateNonTransientField(field)
else {
reportPrivateNonTransientFieldInSuperClass(kotlinUClass, field)
}
}
}
}
private val KtLightField.isPrivateOrProtected: Boolean
get() = lightMemberOrigin
?.originalElement
?.visibilityModifierType()
?.value.let { it == "private" || it == "protected" }
private val KtLightField.isTransient: Boolean
get() = hasModifier(JvmModifier.TRANSIENT)
private fun reportPrivateNonTransientField(it: KtLightField) {
context.report(
PRIVATE_FIELD_ISSUE,
context.getLocation(it),
"${it.name} will not be accessible to the generated JsonAdapter",
LintFix.LintFixGroup.create()
.alternatives(
LintFix.create()
.replace()
.name("Mark '${it.name}' as @Transient")
.beginning()
.with("@Transient ")
.build(),
LintFix.create()
.replace()
.name("Make '${it.name}' internal")
.pattern("^((private|protected) )")
.with("internal ")
.build(),
LintFix.create()
.replace()
.name("Make '${it.name}' public")
.pattern("^((private|protected) )")
.with("")
.build()
)
)
}
private fun reportPrivateNonTransientFieldInSuperClass(kotlinUClass: KotlinUClass, field: KtLightField) {
val superConstructorInvocation = kotlinUClass.ktClass
?.getSuperTypeList()
?.entries
?.get(0)
?.navigationElement!!
context.report(
issue = PRIVATE_FIELD_ISSUE,
location = context.getLocation(superConstructorInvocation),
message = "${field.containingClass.qualifiedName}.${field.name} is not accessible for the " +
"generated JsonAdapter",
quickfixData = null
)
}
}
companion object {
val PRIVATE_FIELD_ISSUE = Issue.create(
id = "PrivateJsonClassField",
briefDescription = "Generated JsonAdapters require all private field to be @Transient",
explanation = "When the annotation processor is enabled this will fail.",
category = Category.CORRECTNESS,
severity = Severity.FATAL,
implementation = Implementation(JsonClassDetector::class.java, Scope.JAVA_FILE_SCOPE),
priority = 8
)
}
}
/*
* Copyright (C) 2021 Remco Mokveld
*
* 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.
*/
package nl.remcomokveld.moshi.lint
import com.android.tools.lint.checks.infrastructure.LintDetectorTest
import com.android.tools.lint.detector.api.Detector
/**
* Unit tests for the [JsonClassDetector].
*/
class JsonClassDetectorTest : LintDetectorTest() {
fun `test fatal error when json class has private non transient val`() {
assertFatalError(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
private val privateProperty: String
)
""",
exepectOutput = """
src/com/squareup/moshi/lint/test/SimpleJsonClass.kt:7: Error: privateProperty will not be accessible to the generated JsonAdapter [PrivateJsonClassField]
private val privateProperty: String
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""",
expectFixDiff = """
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Mark 'privateProperty' as @Transient:
@@ -7 +7
- private val privateProperty: String
+ @Transient private val privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' internal:
@@ -7 +7
- private val privateProperty: String
+ internal val privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' public:
@@ -7 +7
- private val privateProperty: String
+ val privateProperty: String
"""
)
}
fun `test fatal error when json class has protected non transient val`() {
assertFatalError(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
protected val privateProperty: String
)
""",
exepectOutput = """
src/com/squareup/moshi/lint/test/SimpleJsonClass.kt:7: Error: privateProperty will not be accessible to the generated JsonAdapter [PrivateJsonClassField]
protected val privateProperty: String
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""",
expectFixDiff = """
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Mark 'privateProperty' as @Transient:
@@ -7 +7
- protected val privateProperty: String
+ @Transient protected val privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' internal:
@@ -7 +7
- protected val privateProperty: String
+ internal val privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' public:
@@ -7 +7
- protected val privateProperty: String
+ val privateProperty: String
"""
)
}
fun `test fatal error when json class has private non transient var`() {
assertFatalError(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
private var privateProperty: String
)
""",
exepectOutput = """
src/com/squareup/moshi/lint/test/SimpleJsonClass.kt:7: Error: privateProperty will not be accessible to the generated JsonAdapter [PrivateJsonClassField]
private var privateProperty: String
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""",
expectFixDiff = """
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Mark 'privateProperty' as @Transient:
@@ -7 +7
- private var privateProperty: String
+ @Transient private var privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' internal:
@@ -7 +7
- private var privateProperty: String
+ internal var privateProperty: String
Fix for src/com/squareup/moshi/lint/test/SimpleJsonClass.kt line 7: Make 'privateProperty' public:
@@ -7 +7
- private var privateProperty: String
+ var privateProperty: String
"""
)
}
fun `test no warnings when json class has no private fields`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
class SimpleJsonClass(
val privateProperty: String
)
"""
)
}
fun `test no warnings when json class has transient private field`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
@Transient private val privateProperty: String
)
"""
)
}
fun `test no warnings when json class field is internal`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
internal val privateProperty: String
)
"""
)
}
fun `test no warnings when json class field is explicit public`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
public val privateProperty: String
)
"""
)
}
fun `test no warnings when json class has generateAdapter = false`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = false)
data class SimpleJsonClass(
private val privateProperty: String
)
"""
)
}
fun `test fatal error when json super class has private non transient field`() {
assertFatalError(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
abstract class BaseJsonClass(
private val privateSuperField: String
)
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
@Transient private val privateProperty: String,
val superField: String
) : BaseJsonClass(superField)
""",
exepectOutput = """
src/com/squareup/moshi/lint/test/BaseJsonClass.kt:13: Error: com.squareup.moshi.lint.test.BaseJsonClass.privateSuperField is not accessible for the generated JsonAdapter [PrivateJsonClassField]
) : BaseJsonClass(superField)
~~~~~~~~~~~~~~~~~~~~~~~~~
1 errors, 0 warnings
""",
expectFixDiff = ""
)
}
fun `test no warnings when super class has private transient field`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
import com.squareup.moshi.JsonClass
abstract class BaseJsonClass(
@Transient private val privateSuperField: String
)
@JsonClass(generateAdapter = true)
data class SimpleJsonClass(
val superField: String
) : BaseJsonClass(superField)
"""
)
}
fun `test no warnings when no JsonClass annotation present`() {
assertNoWarnings(
code = """
package com.squareup.moshi.lint.test
data class SimpleJsonClass(
private val privateProperty: String
)
"""
)
}
private fun assertNoWarnings(code: String) {
lint()
.files(
jsonClassAnnotation(),
kotlin(code.trimIndent())
)
.run()
.expectClean()
}
private fun assertFatalError(code: String, exepectOutput: String, expectFixDiff: String) {
lint()
.files(
jsonClassAnnotation(),
kotlin(code.trimIndent())
)
.run()
.expect(exepectOutput.trimIndent())
.expectFixDiffs(expectFixDiff.trimIndent())
}
private fun jsonClassAnnotation(): TestFile? {
return java(
"""
package com.squareup.moshi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Documented
public @interface JsonClass {
boolean generateAdapter();
}
""".trimIndent()
)
}
override fun getDetector(): Detector = JsonClassDetector()
override fun getIssues() = mutableListOf(JsonClassDetector.PRIVATE_FIELD_ISSUE)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment