Added Purity for readonly validation (#13600)

* Added purity to check readonly-ness :)

* Update build.gradle.kts
This commit is contained in:
Yair Morgenstern 2025-07-11 14:00:39 +03:00 committed by GitHub
parent b828338aa0
commit 694354af09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 50 additions and 6 deletions

View File

@ -35,9 +35,23 @@ plugins {
// This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about
kotlin("multiplatform") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24"
id("io.github.yairm210.purity-plugin") version "0.0.15" apply(false)
}
allprojects {
// repositories{ // for local purity
// mavenLocal()
// }
apply(plugin = "io.github.yairm210.purity-plugin")
configure<yairm210.purity.PurityConfiguration>{
wellKnownPureFunctions = setOf("kotlin.internal.ir.CHECK_NOT_NULL")
wellKnownReadonlyFunctions = setOf(
"kotlin.collections.any",
"kotlin.collections.Iterator.hasNext"
)
}
apply(plugin = "eclipse")
apply(plugin = "idea")

View File

@ -8,6 +8,7 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.models.ruleset.validation.ModCompatibility
import com.unciv.models.stats.Stat
import org.jetbrains.annotations.Contract
import kotlin.random.Random
object Conditionals {
@ -19,6 +20,7 @@ object Conditionals {
return Random(seed).nextFloat()
}
@Contract("readonly") @Suppress("purity")
fun conditionalApplies(
unique: Unique?,
conditional: Unique,

View File

@ -8,6 +8,7 @@ import com.unciv.models.translations.equalsPlaceholderText
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
import org.jetbrains.annotations.Contract
import org.jetbrains.annotations.VisibleForTesting
/**
@ -257,6 +258,7 @@ enum class Countables(
open val noPlaceholders = !text.contains('[')
// Leave these in place only for the really simple cases
@Contract("readonly")
open fun matches(parameterText: String) = if (noPlaceholders) parameterText == text
else parameterText.equalsPlaceholderText(placeholderText)
@ -266,7 +268,9 @@ enum class Countables(
/** This indicates whether a parameter *is of this countable type*, not *whether its parameters are correct*
* E.g. "[fakeBuilding] Buildings" is obviously a countable of type "[buildingFilter] Buildings", therefore matches will return true.
* But it has another problem, which is that the building filter is bad, so its getErrorSeverity will return "ruleset specific" */
@Contract("readonly")
open fun matches(parameterText: String, ruleset: Ruleset): Boolean = false
@Contract("readonly")
abstract fun eval(parameterText: String, stateForConditionals: StateForConditionals): Int?
open val documentationHeader get() =
@ -289,6 +293,7 @@ enum class Countables(
getErrorSeverity(parameterText.getPlaceholderParameters().first(), ruleset)
companion object {
@Contract("readonly")
fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries
.firstOrNull {
if (it.matchesWithRuleset)
@ -296,6 +301,7 @@ enum class Countables(
else it.matches(parameterText)
}
@Contract("readonly")
fun getCountableAmount(parameterText: String, stateForConditionals: StateForConditionals): Int? {
val ruleset = stateForConditionals.gameInfo?.ruleset
val countable = getMatching(parameterText, ruleset) ?: return null

View File

@ -12,6 +12,7 @@ import com.unciv.models.translations.getModifiers
import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText
import com.unciv.models.translations.removeConditionals
import org.jetbrains.annotations.Contract
import kotlin.math.max
@ -47,7 +48,9 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
fun hasFlag(flag: UniqueFlag) = type != null && type.flags.contains(flag)
fun isHiddenToUsers() = hasFlag(UniqueFlag.HiddenToUsers) || hasModifier(UniqueType.ModifierHiddenFromUsers)
@Contract("readonly")
fun getModifiers(type: UniqueType) = modifiersMap[type] ?: emptyList()
@Contract("readonly")
fun hasModifier(type: UniqueType) = modifiersMap.containsKey(type)
fun isModifiedByGameSpeed() = hasModifier(UniqueType.ModifiedByGameSpeed)
fun isModifiedByGameProgress() = hasModifier(UniqueType.ModifiedByGameProgress)
@ -81,6 +84,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
return conditionalsApply(StateForConditionals(civInfo, city))
}
@Contract("readonly")
fun conditionalsApply(state: StateForConditionals): Boolean {
if (state.ignoreConditionals) return true
// Always allow Timed conditional uniques. They are managed elsewhere
@ -92,6 +96,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
return true
}
@Contract("readonly") @Suppress("purity")
private fun getUniqueMultiplier(stateForConditionals: StateForConditionals): Int {
if (stateForConditionals == StateForConditionals.IgnoreMultiplicationForCaching)
return 1
@ -126,6 +131,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
}
/** Multiplies the unique according to the multiplication conditionals */
@Contract("readonly")
fun getMultiplied(stateForConditionals: StateForConditionals): Sequence<Unique> {
val multiplier = getUniqueMultiplier(stateForConditionals)
return EndlessSequenceOf(this).take(multiplier)

View File

@ -1,5 +1,6 @@
package com.unciv.models.ruleset.unique
import org.jetbrains.annotations.Contract
import java.util.*
open class UniqueMap() {
@ -14,8 +15,6 @@ open class UniqueMap() {
addUniques(uniques.asIterable())
}
fun isEmpty(): Boolean = innerUniqueMap.isEmpty()
/** Adds one [unique] unless it has a ConditionalTimedUnique conditional */
open fun addUnique(unique: Unique) {
val existingArrayList = innerUniqueMap[unique.placeholderText]
@ -42,26 +41,33 @@ open class UniqueMap() {
typedUniqueMap.clear()
}
// Pure functions
@Contract("readonly")
fun isEmpty(): Boolean = innerUniqueMap.isEmpty()
@Contract("readonly")
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
@Contract("readonly")
fun hasUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
@Contract("readonly")
fun hasTagUnique(tagUnique: String) =
innerUniqueMap.containsKey(tagUnique)
// 160ms vs 1000-1250ms/30s
@Contract("readonly")
fun getUniques(uniqueType: UniqueType) = typedUniqueMap[uniqueType]
?.asSequence()
?: emptySequence()
@Contract("readonly")
fun getUniques(uniqueTag: String) = innerUniqueMap[uniqueTag]
?.asSequence()
?: emptySequence()
@Contract("readonly")
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test)
@ -73,6 +79,7 @@ open class UniqueMap() {
}
}
@Contract("readonly")
fun getMatchingUniques(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test)
@ -84,15 +91,19 @@ open class UniqueMap() {
}
}
@Contract("readonly")
fun hasMatchingUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) }
@Contract("readonly")
fun hasMatchingUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag)
.any { it.conditionalsApply(state) }
@Contract("readonly")
fun getAllUniques() = innerUniqueMap.values.asSequence().flatten()
@Contract("readonly")
fun getTriggeredUniques(trigger: UniqueType, stateForConditionals: StateForConditionals,
triggerFilter: (Unique) -> Boolean = { true }): Sequence<Unique> {
return getAllUniques().filter { unique ->

View File

@ -12,6 +12,7 @@ import com.unciv.ui.components.fonts.DiacriticSupport
import com.unciv.ui.components.fonts.FontRulesetIcons
import com.unciv.utils.Log
import com.unciv.utils.debug
import org.jetbrains.annotations.Contract
import java.util.Locale
import org.jetbrains.annotations.VisibleForTesting
@ -472,6 +473,7 @@ private fun String.translateIndividualWord(language: String, hideIcons: Boolean,
* For example, a string like 'The city of [New [York]]' will return ['New [York]'],
* allowing us to have nested translations!
*/
@Contract("readonly")
fun String.getPlaceholderParameters(): List<String> {
if (!this.contains('[')) return emptyList()
@ -492,6 +494,7 @@ fun String.getPlaceholderParameters(): List<String> {
return parameters
}
@Contract("readonly")
fun String.getPlaceholderText(): String {
var stringToReturn = this.removeConditionals()
val placeholderParameters = stringToReturn.getPlaceholderParameters()
@ -500,6 +503,7 @@ fun String.getPlaceholderText(): String {
return stringToReturn
}
@Contract("readonly")
fun String.equalsPlaceholderText(str: String): Boolean {
if (isEmpty()) return str.isEmpty()
if (str.isEmpty()) return false // Empty strings have no .first()
@ -529,6 +533,7 @@ fun String.getModifiers(): List<Unique> {
return pointyBraceRegex.findAll(this).map { Unique(it.groups[1]!!.value) }.toList()
}
@Contract("readonly")
fun String.removeConditionals(): String {
if (!this.contains('<')) return this // no need to regex search
return this