Skip to content

Commit d49c170

Browse files
authored
Allow sealed classes to be Moshi-compatible (#431)
* Allow enums to be Moshi compatible * Run spotless * Support sealed interfaces with moshi-sealed * Support primitives in collections * Support sealed classes * Run spotless * Clarify comment * Edit comment formatting
1 parent b6ad839 commit d49c170

File tree

2 files changed

+115
-3
lines changed

2 files changed

+115
-3
lines changed

slack-lint-checks/src/main/java/slack/lint/JsonInflaterMoshiCompatibilityDetector.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,28 @@ class JsonInflaterMoshiCompatibilityDetector : Detector(), SourceCodeScanner {
172172
}
173173

174174
private fun isAbstractOrNonPublicClass(psiClass: PsiClass): Boolean {
175-
return !psiClass.isInterface &&
176-
(psiClass.hasModifierProperty(PsiModifier.ABSTRACT) ||
177-
!psiClass.hasModifierProperty(PsiModifier.PUBLIC))
175+
if (psiClass.isInterface) return false
176+
177+
// We return false for sealed classes here even though they are technically considered abstract
178+
// by PsiClass. From a Moshi perspective, sealed classes can be compatible, while abstract
179+
// classes cannot.
180+
if (isSealedClass(psiClass)) return false
181+
182+
return (psiClass.hasModifierProperty(PsiModifier.ABSTRACT) ||
183+
!psiClass.hasModifierProperty(PsiModifier.PUBLIC))
184+
}
185+
186+
private fun isSealedClass(psiClass: PsiClass): Boolean {
187+
if (psiClass.isInterface) return false
188+
189+
// For Kotlin classes, check using Kotlin PSI
190+
if (psiClass is KtLightClass) {
191+
val ktClass = psiClass.kotlinOrigin
192+
return ktClass?.hasModifier(KtTokens.SEALED_KEYWORD) == true
193+
}
194+
195+
// Fallback for Java classes (Java doesn't have sealed classes in older versions)
196+
return psiClass.hasModifierProperty(PsiModifier.SEALED)
178197
}
179198

180199
private fun isSealedInterface(psiClass: PsiClass): Boolean {

slack-lint-checks/src/test/java/slack/lint/JsonInflaterMoshiCompatibilityDetectorTest.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,4 +686,97 @@ class JsonInflaterMoshiCompatibilityDetectorTest : LintDetectorTest() {
686686
"""
687687
)
688688
}
689+
690+
@Test
691+
fun testSealedClass() {
692+
lint()
693+
.files(
694+
jsonClassStub,
695+
jsonInflaterStub,
696+
typeLabelStub,
697+
defaultObjectStub,
698+
kotlin(
699+
"""
700+
package test
701+
702+
import com.squareup.moshi.JsonClass
703+
import slack.commons.json.JsonInflater
704+
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
705+
import dev.zacsweers.moshix.sealed.annotations.DefaultObject
706+
707+
@JsonClass(generateAdapter = true, generator = "sealed:type")
708+
sealed class Animal {
709+
@TypeLabel("dog")
710+
@JsonClass(generateAdapter = true)
711+
data class Dog(val name: String) : Animal()
712+
713+
@TypeLabel("cat")
714+
@JsonClass(generateAdapter = true)
715+
data class Cat(val age: Int) : Animal()
716+
717+
@DefaultObject
718+
object Default : Animal()
719+
}
720+
721+
fun useJsonInflater(jsonInflater: JsonInflater) {
722+
val model = jsonInflater.inflate("{}", Animal::class.java)
723+
val json = jsonInflater.deflate(model, Animal::class.java)
724+
}
725+
"""
726+
),
727+
)
728+
.run()
729+
.expectClean()
730+
}
731+
732+
@Test
733+
fun testSealedClassMissingJsonClassAnnotation() {
734+
lint()
735+
.files(
736+
jsonClassStub,
737+
jsonInflaterStub,
738+
typeLabelStub,
739+
defaultObjectStub,
740+
kotlin(
741+
"""
742+
package test
743+
744+
import com.squareup.moshi.JsonClass
745+
import slack.commons.json.JsonInflater
746+
import dev.zacsweers.moshix.sealed.annotations.TypeLabel
747+
import dev.zacsweers.moshix.sealed.annotations.DefaultObject
748+
749+
sealed class Animal {
750+
@TypeLabel("dog")
751+
@JsonClass(generateAdapter = true)
752+
data class Dog(val name: String) : Animal()
753+
754+
@TypeLabel("cat")
755+
@JsonClass(generateAdapter = true)
756+
data class Cat(val age: Int) : Animal()
757+
758+
@DefaultObject
759+
object Default : Animal()
760+
}
761+
762+
fun useJsonInflater(jsonInflater: JsonInflater) {
763+
val model = jsonInflater.inflate("{}", Animal::class.java)
764+
val json = jsonInflater.deflate(model, Animal::class.java)
765+
}
766+
"""
767+
),
768+
)
769+
.run()
770+
.expect(
771+
"""
772+
src/test/Animal.kt:23: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType]
773+
val model = jsonInflater.inflate("{}", Animal::class.java)
774+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
775+
src/test/Animal.kt:24: Error: Using JsonInflater.inflate/deflate with a Moshi-incompatible type. [JsonInflaterMoshiIncompatibleType]
776+
val json = jsonInflater.deflate(model, Animal::class.java)
777+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
778+
2 errors
779+
"""
780+
)
781+
}
689782
}

0 commit comments

Comments
 (0)