Last active
March 23, 2021 15:18
-
-
Save remcomokveld/a96a245cc21f3976b5c56b27b7384767 to your computer and use it in GitHub Desktop.
Moshi Lint Check
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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