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 // This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about
kotlin("multiplatform") version "1.9.24" kotlin("multiplatform") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24" kotlin("plugin.serialization") version "1.9.24"
id("io.github.yairm210.purity-plugin") version "0.0.15" apply(false)
} }
allprojects { 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 = "eclipse")
apply(plugin = "idea") 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.logic.civilization.managers.ReligionState
import com.unciv.models.ruleset.validation.ModCompatibility import com.unciv.models.ruleset.validation.ModCompatibility
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import org.jetbrains.annotations.Contract
import kotlin.random.Random import kotlin.random.Random
object Conditionals { object Conditionals {
@ -18,7 +19,8 @@ object Conditionals {
seed = seed * 31 + state.hashCode() seed = seed * 31 + state.hashCode()
return Random(seed).nextFloat() return Random(seed).nextFloat()
} }
@Contract("readonly") @Suppress("purity")
fun conditionalApplies( fun conditionalApplies(
unique: Unique?, unique: Unique?,
conditional: 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.fillPlaceholders
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import org.jetbrains.annotations.Contract
import org.jetbrains.annotations.VisibleForTesting import org.jetbrains.annotations.VisibleForTesting
/** /**
@ -257,6 +258,7 @@ enum class Countables(
open val noPlaceholders = !text.contains('[') open val noPlaceholders = !text.contains('[')
// Leave these in place only for the really simple cases // Leave these in place only for the really simple cases
@Contract("readonly")
open fun matches(parameterText: String) = if (noPlaceholders) parameterText == text open fun matches(parameterText: String) = if (noPlaceholders) parameterText == text
else parameterText.equalsPlaceholderText(placeholderText) 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* /** 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. * 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" */ * 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 open fun matches(parameterText: String, ruleset: Ruleset): Boolean = false
@Contract("readonly")
abstract fun eval(parameterText: String, stateForConditionals: StateForConditionals): Int? abstract fun eval(parameterText: String, stateForConditionals: StateForConditionals): Int?
open val documentationHeader get() = open val documentationHeader get() =
@ -289,6 +293,7 @@ enum class Countables(
getErrorSeverity(parameterText.getPlaceholderParameters().first(), ruleset) getErrorSeverity(parameterText.getPlaceholderParameters().first(), ruleset)
companion object { companion object {
@Contract("readonly")
fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries
.firstOrNull { .firstOrNull {
if (it.matchesWithRuleset) if (it.matchesWithRuleset)
@ -296,6 +301,7 @@ enum class Countables(
else it.matches(parameterText) else it.matches(parameterText)
} }
@Contract("readonly")
fun getCountableAmount(parameterText: String, stateForConditionals: StateForConditionals): Int? { fun getCountableAmount(parameterText: String, stateForConditionals: StateForConditionals): Int? {
val ruleset = stateForConditionals.gameInfo?.ruleset val ruleset = stateForConditionals.gameInfo?.ruleset
val countable = getMatching(parameterText, ruleset) ?: return null 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.getPlaceholderParameters
import com.unciv.models.translations.getPlaceholderText import com.unciv.models.translations.getPlaceholderText
import com.unciv.models.translations.removeConditionals import com.unciv.models.translations.removeConditionals
import org.jetbrains.annotations.Contract
import kotlin.math.max 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 hasFlag(flag: UniqueFlag) = type != null && type.flags.contains(flag)
fun isHiddenToUsers() = hasFlag(UniqueFlag.HiddenToUsers) || hasModifier(UniqueType.ModifierHiddenFromUsers) fun isHiddenToUsers() = hasFlag(UniqueFlag.HiddenToUsers) || hasModifier(UniqueType.ModifierHiddenFromUsers)
@Contract("readonly")
fun getModifiers(type: UniqueType) = modifiersMap[type] ?: emptyList() fun getModifiers(type: UniqueType) = modifiersMap[type] ?: emptyList()
@Contract("readonly")
fun hasModifier(type: UniqueType) = modifiersMap.containsKey(type) fun hasModifier(type: UniqueType) = modifiersMap.containsKey(type)
fun isModifiedByGameSpeed() = hasModifier(UniqueType.ModifiedByGameSpeed) fun isModifiedByGameSpeed() = hasModifier(UniqueType.ModifiedByGameSpeed)
fun isModifiedByGameProgress() = hasModifier(UniqueType.ModifiedByGameProgress) 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)) return conditionalsApply(StateForConditionals(civInfo, city))
} }
@Contract("readonly")
fun conditionalsApply(state: StateForConditionals): Boolean { fun conditionalsApply(state: StateForConditionals): Boolean {
if (state.ignoreConditionals) return true if (state.ignoreConditionals) return true
// Always allow Timed conditional uniques. They are managed elsewhere // 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 return true
} }
@Contract("readonly") @Suppress("purity")
private fun getUniqueMultiplier(stateForConditionals: StateForConditionals): Int { private fun getUniqueMultiplier(stateForConditionals: StateForConditionals): Int {
if (stateForConditionals == StateForConditionals.IgnoreMultiplicationForCaching) if (stateForConditionals == StateForConditionals.IgnoreMultiplicationForCaching)
return 1 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 */ /** Multiplies the unique according to the multiplication conditionals */
@Contract("readonly")
fun getMultiplied(stateForConditionals: StateForConditionals): Sequence<Unique> { fun getMultiplied(stateForConditionals: StateForConditionals): Sequence<Unique> {
val multiplier = getUniqueMultiplier(stateForConditionals) val multiplier = getUniqueMultiplier(stateForConditionals)
return EndlessSequenceOf(this).take(multiplier) return EndlessSequenceOf(this).take(multiplier)

View File

@ -1,5 +1,6 @@
package com.unciv.models.ruleset.unique package com.unciv.models.ruleset.unique
import org.jetbrains.annotations.Contract
import java.util.* import java.util.*
open class UniqueMap() { open class UniqueMap() {
@ -14,8 +15,6 @@ open class UniqueMap() {
addUniques(uniques.asIterable()) addUniques(uniques.asIterable())
} }
fun isEmpty(): Boolean = innerUniqueMap.isEmpty()
/** Adds one [unique] unless it has a ConditionalTimedUnique conditional */ /** Adds one [unique] unless it has a ConditionalTimedUnique conditional */
open fun addUnique(unique: Unique) { open fun addUnique(unique: Unique) {
val existingArrayList = innerUniqueMap[unique.placeholderText] val existingArrayList = innerUniqueMap[unique.placeholderText]
@ -42,26 +41,33 @@ open class UniqueMap() {
typedUniqueMap.clear() typedUniqueMap.clear()
} }
// Pure functions @Contract("readonly")
fun isEmpty(): Boolean = innerUniqueMap.isEmpty()
@Contract("readonly")
fun hasUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) = fun hasUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) && !it.isTimedTriggerable } getUniques(uniqueType).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
@Contract("readonly")
fun hasUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) = fun hasUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag).any { it.conditionalsApply(state) && !it.isTimedTriggerable } getUniques(uniqueTag).any { it.conditionalsApply(state) && !it.isTimedTriggerable }
@Contract("readonly")
fun hasTagUnique(tagUnique: String) = fun hasTagUnique(tagUnique: String) =
innerUniqueMap.containsKey(tagUnique) innerUniqueMap.containsKey(tagUnique)
// 160ms vs 1000-1250ms/30s // 160ms vs 1000-1250ms/30s
@Contract("readonly")
fun getUniques(uniqueType: UniqueType) = typedUniqueMap[uniqueType] fun getUniques(uniqueType: UniqueType) = typedUniqueMap[uniqueType]
?.asSequence() ?.asSequence()
?: emptySequence() ?: emptySequence()
@Contract("readonly")
fun getUniques(uniqueTag: String) = innerUniqueMap[uniqueTag] fun getUniques(uniqueTag: String) = innerUniqueMap[uniqueTag]
?.asSequence() ?.asSequence()
?: emptySequence() ?: emptySequence()
@Contract("readonly")
fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) = fun getMatchingUniques(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType) getUniques(uniqueType)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test) // 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) = fun getMatchingUniques(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag) getUniques(uniqueTag)
// Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test) // Same as .filter | .flatMap, but more cpu/mem performant (7.7 GB vs ?? for test)
@ -83,16 +90,20 @@ open class UniqueMap() {
else -> it.getMultiplied(state) else -> it.getMultiplied(state)
} }
} }
@Contract("readonly")
fun hasMatchingUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) = fun hasMatchingUnique(uniqueType: UniqueType, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueType).any { it.conditionalsApply(state) } getUniques(uniqueType).any { it.conditionalsApply(state) }
@Contract("readonly")
fun hasMatchingUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) = fun hasMatchingUnique(uniqueTag: String, state: StateForConditionals = StateForConditionals.EmptyState) =
getUniques(uniqueTag) getUniques(uniqueTag)
.any { it.conditionalsApply(state) } .any { it.conditionalsApply(state) }
@Contract("readonly")
fun getAllUniques() = innerUniqueMap.values.asSequence().flatten() fun getAllUniques() = innerUniqueMap.values.asSequence().flatten()
@Contract("readonly")
fun getTriggeredUniques(trigger: UniqueType, stateForConditionals: StateForConditionals, fun getTriggeredUniques(trigger: UniqueType, stateForConditionals: StateForConditionals,
triggerFilter: (Unique) -> Boolean = { true }): Sequence<Unique> { triggerFilter: (Unique) -> Boolean = { true }): Sequence<Unique> {
return getAllUniques().filter { 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.ui.components.fonts.FontRulesetIcons
import com.unciv.utils.Log import com.unciv.utils.Log
import com.unciv.utils.debug import com.unciv.utils.debug
import org.jetbrains.annotations.Contract
import java.util.Locale import java.util.Locale
import org.jetbrains.annotations.VisibleForTesting 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]'], * For example, a string like 'The city of [New [York]]' will return ['New [York]'],
* allowing us to have nested translations! * allowing us to have nested translations!
*/ */
@Contract("readonly")
fun String.getPlaceholderParameters(): List<String> { fun String.getPlaceholderParameters(): List<String> {
if (!this.contains('[')) return emptyList() if (!this.contains('[')) return emptyList()
@ -492,6 +494,7 @@ fun String.getPlaceholderParameters(): List<String> {
return parameters return parameters
} }
@Contract("readonly")
fun String.getPlaceholderText(): String { fun String.getPlaceholderText(): String {
var stringToReturn = this.removeConditionals() var stringToReturn = this.removeConditionals()
val placeholderParameters = stringToReturn.getPlaceholderParameters() val placeholderParameters = stringToReturn.getPlaceholderParameters()
@ -500,6 +503,7 @@ fun String.getPlaceholderText(): String {
return stringToReturn return stringToReturn
} }
@Contract("readonly")
fun String.equalsPlaceholderText(str: String): Boolean { fun String.equalsPlaceholderText(str: String): Boolean {
if (isEmpty()) return str.isEmpty() if (isEmpty()) return str.isEmpty()
if (str.isEmpty()) return false // Empty strings have no .first() 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() return pointyBraceRegex.findAll(this).map { Unique(it.groups[1]!!.value) }.toList()
} }
@Contract("readonly")
fun String.removeConditionals(): String { fun String.removeConditionals(): String {
if (!this.contains('<')) return this // no need to regex search if (!this.contains('<')) return this // no need to regex search
return this return this