Skip to content

Commit

Permalink
Make sure that subtypes also have default values for primary constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
gabber235 committed Nov 20, 2024
1 parent cb14b99 commit fa62886
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ data class RoadNetwork(
)

@JvmInline
value class RoadNodeId(val id: Int) {
value class RoadNodeId(val id: Int = 0) {
override fun toString(): String = id.toString()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ class ShowTitleActionEntry(

data class TitleDurations(
@Help("The duration of the fade in effect.")
val fadeIn: Duration,
val fadeIn: Duration = Duration.ZERO,
@Help("The duration that it stays.")
val stay: Duration,
val stay: Duration = Duration.ZERO,
@Help("The duration of the fade out effect.")
val fadeOut: Duration
val fadeOut: Duration = Duration.ZERO,
)
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ class BlindingCinematicEntry(
}

data class BlindingSegment(
override val startFrame: Int,
override val endFrame: Int,
override val startFrame: Int = 0,
override val endFrame: Int = 0,
) : Segment

class BlindingCinematicAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ class CinematicPlayerCommandEntry(
}

data class CommandSegment(
override val startFrame: Int,
override val endFrame: Int,
override val startFrame: Int = 0,
override val endFrame: Int = 0,
@Help("Each line is a different command. Commands should not be prefixed with <code>/</code>.")
@Placeholder
@MultiLine
val command: String,
val command: String = "",
) : Segment

class CommandAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class AmbientSoundActivityEntry(
}

data class AmbientSound(
val sound: Sound,
val sound: Sound = Sound.EMPTY,
@Default("1.0")
val weight: Double = 1.0,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ class AudienceActivityEntry(
}

class AudienceActivityPair(
val audience: Ref<out AudienceEntry>,
val activity: Ref<out IndividualEntityActivityEntry>,
val audience: Ref<out AudienceEntry> = emptyRef(),
val activity: Ref<out IndividualEntityActivityEntry> = emptyRef(),
)

class AudienceActivity(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ class GameTimeRange(
}

class GameTimedActivity(
val time: GameTimeRange,
val activity: Ref<out EntityActivityEntry>,
val time: GameTimeRange = GameTimeRange(),
val activity: Ref<out EntityActivityEntry> = emptyRef(),
) {
operator fun contains(time: Long): Boolean {
return time in this.time
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,110 @@
package com.typewritermc.verification

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.typewritermc.core.extension.annotations.Entry
import com.typewritermc.processors.entry.DataBlueprint
import com.typewritermc.processors.fullName

class EntryConstructorAllHaveDefaultValueValidator : SymbolProcessor {
class EntryConstructorAllHaveDefaultValueValidator(
private val logger: KSPLogger
) : SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val entries = resolver.getSymbolsWithAnnotation(Entry::class.qualifiedName!!)
val invalidEntries = entries
.filterIsInstance<KSClassDeclaration>()
.mapNotNull { classDeclaration ->
val primaryConstructor = classDeclaration.primaryConstructor
?: return@mapNotNull "${classDeclaration.fullName}: No primary constructor"
val invalidParams = primaryConstructor.parameters.filter { it.hasDefault.not() }
if (invalidParams.isNotEmpty()) {
"${classDeclaration.fullName}: ${invalidParams.joinToString(", ") { it.name?.asString() ?: "unknown" }}"
} else {
null
.flatMap {
with(logger) {
with(resolver) {
it.validateConstructor()
}
}
}
.toList()

if (invalidEntries.isEmpty()) return emptyList()
throw InvalidConstructorDefaultValuesException(invalidEntries)
}

context(KSPLogger, Resolver)
private fun KSClassDeclaration.validateConstructor(): List<String> {
val primaryConstructor = primaryConstructor
?: return listOf("${fullName}: No primary constructor")
val errors = mutableListOf<String>()

val invalidParams = primaryConstructor.parameters.filter { it.hasDefault.not() }
if (invalidParams.isNotEmpty()) {
errors.add("${fullName}: ${invalidParams.joinToString(", ") { it.name?.asString() ?: "unknown" }}")
}

val additionalErrors = primaryConstructor.parameters
.asSequence()
.map { it.type.resolve() }
.flatMap {
it.shouldCheck()
}
.map { it.declaration }
.filterIsInstance<KSClassDeclaration>()
.flatMap { it.validateConstructor() }
.toList()

errors.addAll(additionalErrors)

return errors
}

context(KSPLogger, Resolver)
private fun KSType.shouldCheck(): List<KSType> {
val blueprint = DataBlueprint.blueprint(this) ?: return emptyList()

if (blueprint is DataBlueprint.ObjectBlueprint) {
return listOf(this)
}

if (blueprint is DataBlueprint.ListBlueprint) {
val subType = arguments.firstOrNull()?.type?.resolve() ?: return emptyList()
return subType.shouldCheck()
}

if (blueprint is DataBlueprint.MapBlueprint) {
val key = arguments.firstOrNull()?.type?.resolve()?.shouldCheck() ?: emptyList()
val value = arguments.lastOrNull()?.type?.resolve()?.shouldCheck() ?: emptyList()

return key + value
}

if (blueprint is DataBlueprint.AlgebraicBlueprint) {
val classDeclaration = declaration as? KSClassDeclaration ?: return emptyList()
return classDeclaration.getSealedSubclasses()
.map { it.asStarProjectedType() }
.flatMap { it.shouldCheck() }
.toList()
}

if (blueprint is DataBlueprint.CustomBlueprint) {
if (blueprint.editor == "ref") return emptyList()
return arguments
.mapNotNull { it.type?.resolve() }
.flatMap { it.shouldCheck() }
}

return emptyList()
}
}

class InvalidConstructorDefaultValuesException(entries: List<String>) : Exception(
"""
|All primary constructor parameters must have default values.
|The following entries have parameters without default values:
| - ${entries.joinToString("\n - ")}
|
""".trimMargin()
)

class ConstructorDefaultValueValidatorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return EntryConstructorAllHaveDefaultValueValidator()
return EntryConstructorAllHaveDefaultValueValidator(environment.logger)
}
}

0 comments on commit fa62886

Please sign in to comment.