chore(purity): UniqueValidator

This commit is contained in:
yairm210 2025-08-01 16:05:04 +03:00
parent d074612c0d
commit 3220d3c52b
4 changed files with 66 additions and 39 deletions

View File

@ -285,7 +285,7 @@ enum class Countables(
/** Leave this only for Countables without any parameters - they can rely on [matches] having validated enough */ /** Leave this only for Countables without any parameters - they can rely on [matches] having validated enough */
open fun getErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueParameterErrorSeverity? = null open fun getErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueParameterErrorSeverity? = null
fun getDeprecationAnnotation(): Deprecated? = declaringJavaClass.getField(name).getAnnotation(Deprecated::class.java) @Readonly fun getDeprecationAnnotation(): Deprecated? = declaringJavaClass.getField(name).getAnnotation(Deprecated::class.java)
protected fun UniqueParameterType.getTranslatedErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueParameterErrorSeverity? = protected fun UniqueParameterType.getTranslatedErrorSeverity(parameterText: String, ruleset: Ruleset): UniqueType.UniqueParameterErrorSeverity? =
getErrorSeverity(parameterText.getPlaceholderParameters().first(), ruleset) getErrorSeverity(parameterText.getPlaceholderParameters().first(), ruleset)

View File

@ -169,6 +169,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
return -1 // Not found return -1 // Not found
} }
@Readonly
fun getReplacementText(ruleset: Ruleset): String { fun getReplacementText(ruleset: Ruleset): String {
val deprecationAnnotation = getDeprecationAnnotation() ?: return "" val deprecationAnnotation = getDeprecationAnnotation() ?: return ""
val replacementUniqueText = deprecationAnnotation.replaceWith.expression val replacementUniqueText = deprecationAnnotation.replaceWith.expression
@ -180,7 +181,7 @@ class Unique(val text: String, val sourceObjectType: UniqueTarget? = null, val s
// note this is only done for the replacement, not the deprecated unique, thus parameters of // note this is only done for the replacement, not the deprecated unique, thus parameters of
// conditionals on the deprecated unique are ignored // conditionals on the deprecated unique are ignored
val finalPossibleUniques = ArrayList<String>() @LocalState val finalPossibleUniques = ArrayList<String>()
for (possibleUnique in possibleUniques) { for (possibleUnique in possibleUniques) {
var resultingUnique = possibleUnique var resultingUnique = possibleUnique

View File

@ -58,9 +58,11 @@ class Expressions {
} }
} }
@Readonly
fun getParsingError(parameterText: String): Parser.ParsingError? = fun getParsingError(parameterText: String): Parser.ParsingError? =
parse(parameterText).exception parse(parameterText).exception
@Readonly
fun getCountableErrors(parameterText: String, ruleset: Ruleset): List<String> { fun getCountableErrors(parameterText: String, ruleset: Ruleset): List<String> {
val parseResult = parse(parameterText) val parseResult = parse(parameterText)
return if (parseResult.node == null) emptyList() else parseResult.node.getErrors(ruleset) return if (parseResult.node == null) emptyList() else parseResult.node.getErrors(ruleset)

View File

@ -15,6 +15,9 @@ import com.unciv.models.ruleset.unique.UniqueParameterType
import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTarget
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unique.expressions.Expressions import com.unciv.models.ruleset.unique.expressions.Expressions
import yairm210.purity.annotations.Cache
import yairm210.purity.annotations.LocalState
import yairm210.purity.annotations.Readonly
class UniqueValidator(val ruleset: Ruleset) { class UniqueValidator(val ruleset: Ruleset) {
@ -63,6 +66,7 @@ class UniqueValidator(val ruleset: Ruleset) {
UniqueType.ConditionalNotAdjacentTo UniqueType.ConditionalNotAdjacentTo
) )
@Readonly
fun checkUnique( fun checkUnique(
unique: Unique, unique: Unique,
tryFixUnknownUniques: Boolean, tryFixUnknownUniques: Boolean,
@ -72,7 +76,7 @@ class UniqueValidator(val ruleset: Ruleset) {
val prefix by lazy { getUniqueContainerPrefix(uniqueContainer) + "\"${unique.text}\"" } val prefix by lazy { getUniqueContainerPrefix(uniqueContainer) + "\"${unique.text}\"" }
if (unique.type == null) return checkUntypedUnique(unique, tryFixUnknownUniques, uniqueContainer, prefix, reportRulesetSpecificErrors) if (unique.type == null) return checkUntypedUnique(unique, tryFixUnknownUniques, uniqueContainer, prefix, reportRulesetSpecificErrors)
val rulesetErrors = RulesetErrorList(ruleset) @LocalState val rulesetErrors = RulesetErrorList(ruleset)
if (uniqueContainer != null && if (uniqueContainer != null &&
!(unique.type.canAcceptUniqueTarget(uniqueContainer.getUniqueTarget()) || !(unique.type.canAcceptUniqueTarget(uniqueContainer.getUniqueTarget()) ||
@ -105,14 +109,14 @@ class UniqueValidator(val ruleset: Ruleset) {
complianceError.errorSeverity.getRulesetErrorSeverity(), uniqueContainer, unique complianceError.errorSeverity.getRulesetErrorSeverity(), uniqueContainer, unique
) )
addExpressionParseErrors(complianceError, rulesetErrors, uniqueContainer, unique) rulesetErrors += getExpressionParseErrors(complianceError, uniqueContainer, unique)
} }
for (conditional in unique.modifiers) { for (conditional in unique.modifiers) {
addConditionalErrors(conditional, rulesetErrors, prefix, unique, uniqueContainer, reportRulesetSpecificErrors) rulesetErrors += getConditionalErrors(conditional, prefix, unique, uniqueContainer, reportRulesetSpecificErrors)
} }
addUniqueTypeSpecificErrors(rulesetErrors, prefix, unique, uniqueContainer, reportRulesetSpecificErrors) rulesetErrors += getUniqueTypeSpecificErrors(prefix, unique, uniqueContainer, reportRulesetSpecificErrors)
val conditionals = unique.modifiers.filter { it.type?.canAcceptUniqueTarget(UniqueTarget.Conditional) == true } val conditionals = unique.modifiers.filter { it.type?.canAcceptUniqueTarget(UniqueTarget.Conditional) == true }
if (conditionals.size > 1){ if (conditionals.size > 1){
@ -142,18 +146,19 @@ class UniqueValidator(val ruleset: Ruleset) {
if (reportRulesetSpecificErrors) if (reportRulesetSpecificErrors)
// If we don't filter these messages will be listed twice as this function is called twice on most objects // If we don't filter these messages will be listed twice as this function is called twice on most objects
// The tests are RulesetInvariant in nature, but RulesetSpecific is called for _all_ objects, invariant is not. // The tests are RulesetInvariant in nature, but RulesetSpecific is called for _all_ objects, invariant is not.
addDeprecationAnnotationErrors(unique, prefix, rulesetErrors, uniqueContainer) rulesetErrors += addDeprecationAnnotationErrors(unique, prefix, uniqueContainer)
return rulesetErrors return rulesetErrors
} }
private fun addExpressionParseErrors( @Readonly
private fun getExpressionParseErrors(
complianceError: UniqueComplianceError, complianceError: UniqueComplianceError,
rulesetErrors: RulesetErrorList,
uniqueContainer: IHasUniques?, uniqueContainer: IHasUniques?,
unique: Unique unique: Unique
) { ): RulesetErrorList {
if (!complianceError.acceptableParameterTypes.contains(UniqueParameterType.Countable)) return @LocalState val rulesetErrors = RulesetErrorList(ruleset)
if (!complianceError.acceptableParameterTypes.contains(UniqueParameterType.Countable)) return rulesetErrors
val parseError = Expressions.getParsingError(complianceError.parameterName) val parseError = Expressions.getParsingError(complianceError.parameterName)
if (parseError != null) { if (parseError != null) {
@ -165,7 +170,7 @@ class UniqueValidator(val ruleset: Ruleset) {
val text = "\"${complianceError.parameterName}\" could not be parsed as an expression due to:" + val text = "\"${complianceError.parameterName}\" could not be parsed as an expression due to:" +
" ${parseError.message}. \n$parameterWithErrorLocationMarked" " ${parseError.message}. \n$parameterWithErrorLocationMarked"
rulesetErrors.add(text, RulesetErrorSeverity.WarningOptionsOnly, uniqueContainer, unique) rulesetErrors.add(text, RulesetErrorSeverity.WarningOptionsOnly, uniqueContainer, unique)
return return rulesetErrors
} }
val countableErrors = Expressions.getCountableErrors(complianceError.parameterName, ruleset) val countableErrors = Expressions.getCountableErrors(complianceError.parameterName, ruleset)
@ -174,6 +179,7 @@ class UniqueValidator(val ruleset: Ruleset) {
" ${countableErrors.joinToString(", ")}" " ${countableErrors.joinToString(", ")}"
rulesetErrors.add(text, RulesetErrorSeverity.WarningOptionsOnly, uniqueContainer, unique) rulesetErrors.add(text, RulesetErrorSeverity.WarningOptionsOnly, uniqueContainer, unique)
} }
return rulesetErrors
} }
private val resourceUniques = setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources, private val resourceUniques = setOf(UniqueType.ProvidesResources, UniqueType.ConsumesResources,
@ -186,21 +192,37 @@ class UniqueValidator(val ruleset: Ruleset) {
UniqueType.ConditionalWhenBelowAmountStatResource, UniqueType.ConditionalWhenBelowAmountStatResource,
) )
private fun addConditionalErrors( @Readonly
private fun getUniqueTypeSpecificErrors(
prefix: String, unique: Unique, uniqueContainer: IHasUniques?, reportRulesetSpecificErrors: Boolean
): RulesetErrorList {
@LocalState val rulesetErrors = RulesetErrorList(ruleset)
when (unique.type) {
UniqueType.RuinsUpgrade -> {
if (reportRulesetSpecificErrors && !anyAncientRuins)
rulesetErrors.add("$prefix is pointless - there are no ancient ruins", RulesetErrorSeverity.Warning, uniqueContainer, unique)
}
else -> {}
}
return rulesetErrors
}
@Readonly
private fun getConditionalErrors(
conditional: Unique, conditional: Unique,
rulesetErrors: RulesetErrorList,
prefix: String, prefix: String,
unique: Unique, unique: Unique,
uniqueContainer: IHasUniques?, uniqueContainer: IHasUniques?,
reportRulesetSpecificErrors: Boolean reportRulesetSpecificErrors: Boolean
) { ): RulesetErrorList {
@LocalState val rulesetErrors = RulesetErrorList(ruleset)
if (unique.hasFlag(UniqueFlag.NoConditionals)) { if (unique.hasFlag(UniqueFlag.NoConditionals)) {
rulesetErrors.add( rulesetErrors.add(
"$prefix contains the conditional \"${conditional.text}\"," + "$prefix contains the conditional \"${conditional.text}\"," +
" but the unique does not accept conditionals!", " but the unique does not accept conditionals!",
RulesetErrorSeverity.Error, uniqueContainer, unique RulesetErrorSeverity.Error, uniqueContainer, unique
) )
return return rulesetErrors
} }
if (conditional.type == null) { if (conditional.type == null) {
@ -219,7 +241,7 @@ class UniqueValidator(val ruleset: Ruleset) {
text, text,
RulesetErrorSeverity.Warning, uniqueContainer, unique RulesetErrorSeverity.Warning, uniqueContainer, unique
) )
return return rulesetErrors
} }
if (conditional.type.targetTypes.none { it.modifierType != UniqueTarget.ModifierType.None }) if (conditional.type.targetTypes.none { it.modifierType != UniqueTarget.ModifierType.None })
@ -274,30 +296,21 @@ class UniqueValidator(val ruleset: Ruleset) {
complianceError.errorSeverity.getRulesetErrorSeverity(), uniqueContainer, unique complianceError.errorSeverity.getRulesetErrorSeverity(), uniqueContainer, unique
) )
addExpressionParseErrors(complianceError, rulesetErrors, uniqueContainer, unique) rulesetErrors += getExpressionParseErrors(complianceError, uniqueContainer, unique)
} }
addDeprecationAnnotationErrors(conditional, "$prefix contains modifier \"${conditional.text}\" which", rulesetErrors, uniqueContainer) addDeprecationAnnotationErrors(conditional, "$prefix contains modifier \"${conditional.text}\" which", uniqueContainer)
}
return rulesetErrors
private fun addUniqueTypeSpecificErrors(
rulesetErrors: RulesetErrorList, prefix: String, unique: Unique, uniqueContainer: IHasUniques?, reportRulesetSpecificErrors: Boolean
) {
when(unique.type) {
UniqueType.RuinsUpgrade -> {
if (reportRulesetSpecificErrors && !anyAncientRuins)
rulesetErrors.add("$prefix is pointless - there are no ancient ruins", RulesetErrorSeverity.Warning, uniqueContainer, unique)
}
else -> return
}
} }
@Readonly
private fun addDeprecationAnnotationErrors( private fun addDeprecationAnnotationErrors(
unique: Unique, unique: Unique,
prefix: String, prefix: String,
rulesetErrors: RulesetErrorList,
uniqueContainer: IHasUniques? uniqueContainer: IHasUniques?
) { ): RulesetErrorList {
@LocalState val rulesetErrors = RulesetErrorList(ruleset)
val deprecationAnnotation = unique.getDeprecationAnnotation() val deprecationAnnotation = unique.getDeprecationAnnotation()
if (deprecationAnnotation != null) { if (deprecationAnnotation != null) {
val replacementUniqueText = unique.getReplacementText(ruleset) val replacementUniqueText = unique.getReplacementText(ruleset)
@ -312,12 +325,13 @@ class UniqueValidator(val ruleset: Ruleset) {
} }
// Check for deprecated Countables // Check for deprecated Countables
if (unique.type == null) return if (unique.type == null) return rulesetErrors
val countables = val countables =
unique.type.parameterTypeMap.withIndex() unique.type.parameterTypeMap.withIndex()
.filter { UniqueParameterType.Countable in it.value } .filter { UniqueParameterType.Countable in it.value }
.map { unique.params[it.index] } .map { unique.params[it.index] }
.mapNotNull { Countables.getMatching(it, ruleset) } .mapNotNull { Countables.getMatching(it, ruleset) }
for (countable in countables) { for (countable in countables) {
val deprecation = countable.getDeprecationAnnotation() ?: continue val deprecation = countable.getDeprecationAnnotation() ?: continue
// This is less flexible than unique.getReplacementText(ruleset) // This is less flexible than unique.getReplacementText(ruleset)
@ -329,14 +343,18 @@ class UniqueValidator(val ruleset: Ruleset) {
else RulesetErrorSeverity.ErrorOptionsOnly // User visible in new game and red in options else RulesetErrorSeverity.ErrorOptionsOnly // User visible in new game and red in options
rulesetErrors.add(text, severity, uniqueContainer, unique) rulesetErrors.add(text, severity, uniqueContainer, unique)
} }
return rulesetErrors
} }
/** Maps uncompliant parameters to their required types */ /** Maps uncompliant parameters to their required types */
@Readonly
private fun getComplianceErrors( private fun getComplianceErrors(
unique: Unique, unique: Unique,
): List<UniqueComplianceError> { ): List<UniqueComplianceError> {
if (unique.type == null) return emptyList() if (unique.type == null) return emptyList()
val errorList = ArrayList<UniqueComplianceError>() @LocalState val errorList = ArrayList<UniqueComplianceError>()
for ((index, param) in unique.params.withIndex()) { for ((index, param) in unique.params.withIndex()) {
// Trying to catch the error at #11404 // Trying to catch the error at #11404
if (unique.type.parameterTypeMap.size != unique.params.size) { if (unique.type.parameterTypeMap.size != unique.params.size) {
@ -361,11 +379,13 @@ class UniqueValidator(val ruleset: Ruleset) {
return errorList return errorList
} }
private val paramTypeErrorSeverityCache = HashMap<UniqueParameterType, HashMap<String, UniqueType.UniqueParameterErrorSeverity?>>() @Cache private val paramTypeErrorSeverityCache = HashMap<UniqueParameterType, HashMap<String, UniqueType.UniqueParameterErrorSeverity?>>()
@Readonly
private fun getParamTypeErrorSeverityCached(uniqueParameterType: UniqueParameterType, param: String): UniqueType.UniqueParameterErrorSeverity? { private fun getParamTypeErrorSeverityCached(uniqueParameterType: UniqueParameterType, param: String): UniqueType.UniqueParameterErrorSeverity? {
if (!paramTypeErrorSeverityCache.containsKey(uniqueParameterType)) if (!paramTypeErrorSeverityCache.containsKey(uniqueParameterType))
paramTypeErrorSeverityCache[uniqueParameterType] = hashMapOf() paramTypeErrorSeverityCache[uniqueParameterType] = hashMapOf()
val uniqueParamCache = paramTypeErrorSeverityCache[uniqueParameterType]!!
@LocalState val uniqueParamCache = paramTypeErrorSeverityCache[uniqueParameterType]!!
if (uniqueParamCache.containsKey(param)) return uniqueParamCache[param] if (uniqueParamCache.containsKey(param)) return uniqueParamCache[param]
@ -374,6 +394,7 @@ class UniqueValidator(val ruleset: Ruleset) {
return severity return severity
} }
@Readonly
private fun checkUntypedUnique( private fun checkUntypedUnique(
unique: Unique, unique: Unique,
tryFixUnknownUniques: Boolean, tryFixUnknownUniques: Boolean,
@ -389,7 +410,7 @@ class UniqueValidator(val ruleset: Ruleset) {
) )
// Support purely filtering Uniques without actual implementation // Support purely filtering Uniques without actual implementation
if (isFilteringUniqueAllowed(unique, reportRulesetSpecificErrors)) return RulesetErrorList() if (isFilteringUniqueAllowed(unique, reportRulesetSpecificErrors)) return RulesetErrorList(ruleset)
if (tryFixUnknownUniques) { if (tryFixUnknownUniques) {
val fixes = tryFixUnknownUnique(unique, uniqueContainer, prefix) val fixes = tryFixUnknownUnique(unique, uniqueContainer, prefix)
@ -403,6 +424,7 @@ class UniqueValidator(val ruleset: Ruleset) {
) )
} }
@Readonly
private fun isFilteringUniqueAllowed(unique: Unique, reportRulesetSpecificErrors: Boolean): Boolean { private fun isFilteringUniqueAllowed(unique: Unique, reportRulesetSpecificErrors: Boolean): Boolean {
// Isolate this decision, to allow easy change of approach // Isolate this decision, to allow easy change of approach
// This says: Must have no conditionals or parameters, and is used in any "filtering" parameter of another Unique // This says: Must have no conditionals or parameters, and is used in any "filtering" parameter of another Unique
@ -411,6 +433,7 @@ class UniqueValidator(val ruleset: Ruleset) {
return unique.text in allUniqueParameters // referenced at least once from elsewhere return unique.text in allUniqueParameters // referenced at least once from elsewhere
} }
@Readonly
private fun tryFixUnknownUnique(unique: Unique, uniqueContainer: IHasUniques?, prefix: String): RulesetErrorList { private fun tryFixUnknownUnique(unique: Unique, uniqueContainer: IHasUniques?, prefix: String): RulesetErrorList {
val similarUniques = UniqueType.entries.filter { val similarUniques = UniqueType.entries.filter {
getRelativeTextDistance( getRelativeTextDistance(
@ -441,13 +464,14 @@ class UniqueValidator(val ruleset: Ruleset) {
}.prependIndent("\t") }.prependIndent("\t")
RulesetErrorList.of(text, RulesetErrorSeverity.OK, ruleset, uniqueContainer, unique) RulesetErrorList.of(text, RulesetErrorSeverity.OK, ruleset, uniqueContainer, unique)
} }
else -> RulesetErrorList() else -> RulesetErrorList(ruleset)
} }
} }
companion object { companion object {
const val whichDoesNotFitParameterType = "which does not fit parameter type" const val whichDoesNotFitParameterType = "which does not fit parameter type"
@Readonly
internal fun getUniqueContainerPrefix(uniqueContainer: IHasUniques?) = internal fun getUniqueContainerPrefix(uniqueContainer: IHasUniques?) =
(if (uniqueContainer is IRulesetObject) "${uniqueContainer.originRuleset}: " else "") + (if (uniqueContainer is IRulesetObject) "${uniqueContainer.originRuleset}: " else "") +
(if (uniqueContainer == null) "The" else "(${uniqueContainer.getUniqueTarget().name}) ${uniqueContainer.name}'s") + (if (uniqueContainer == null) "The" else "(${uniqueContainer.getUniqueTarget().name}) ${uniqueContainer.name}'s") +