diff --git a/.gitignore b/.gitignore index 06465313969..9b80a58f1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.idea/ +.idea/* +!.idea/icon.svg target/ *.iml .*orig diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 00000000000..d2b45f3d311 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1 @@ + diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala index 8e4cec7ee42..0c016839665 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/AssignabilityDeterminer.scala @@ -32,6 +32,9 @@ object AssignabilityDeterminer { def isAssignableStrict(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = isAssignable(from, to, StrictConversionChecker) + def isAssignableWithoutConversion(from: TypingResult, to: TypingResult): ValidatedNel[String, Unit] = + isAssignable(from, to, WithoutConversionChecker) + private def isAssignable(from: TypingResult, to: TypingResult, conversionChecker: ConversionChecker) = { (from, to) match { case (_, Unknown) => ().validNel @@ -223,6 +226,19 @@ object AssignabilityDeterminer { } + private object WithoutConversionChecker extends ConversionChecker { + + override def isConvertable( + from: SingleTypingResult, + to: TypedClass + ): ValidatedNel[String, Unit] = { + val errMsgPrefix = + s"${from.runtimeObjType.display} is not the same as ${to.display}" + condNel(from.withoutValue == to.withoutValue, (), errMsgPrefix) + } + + } + private object StrictConversionChecker extends ConversionChecker { override def isConvertable( diff --git a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala index 1aff3f9a890..343af1e201d 100644 --- a/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala +++ b/components-api/src/main/scala/pl/touk/nussknacker/engine/api/typed/typing.scala @@ -40,6 +40,12 @@ object typing { final def canBeStrictlyConvertedTo(typingResult: TypingResult): Boolean = AssignabilityDeterminer.isAssignableStrict(this, typingResult).isValid + /** + * Checks if the conversion to a given typingResult can be made without any conversion. + */ + final def canBeConvertedWithoutConversionTo(typingResult: TypingResult): Boolean = + AssignabilityDeterminer.isAssignableWithoutConversion(this, typingResult).isValid + def valueOpt: Option[Any] def withoutValue: TypingResult diff --git a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala index 995a8b6afe6..23344aab195 100644 --- a/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala +++ b/scenario-compiler/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala @@ -224,7 +224,17 @@ private[spel] class Typer( // TODO: validate indexer key - the only valid key is an integer - but its more complicated with references withTypedChildren(_ => valid(param)) case TypedClass(clazz, keyParam :: valueParam :: Nil) if clazz.isAssignableFrom(classOf[java.util.Map[_, _]]) => - withTypedChildren(_ => valid(valueParam)) + withTypedChildren { + // Spel implementation of map indexer (in class org.springframework.expression.spel.ast.Indexer, line 154) tries to convert + // indexer to key type of map, but this conversion can be accomplished only if key type of map is known to spel. + // Currently .asMap extension is implemented in such a way, that spel does not know key type of the resulting map + // (that is when spel evaluates this expression it only knows that it got map, but does not know its type parameters). + // It would be hard to change implementation of .asMap extension so we partially turn off this feature of indexer conversion + // by allowing in typing only situations when map key type and indexer type are the same (though we have to allow + // indexing with unknown type) + case indexKey :: Nil if indexKey.canBeConvertedWithoutConversionTo(keyParam) => valid(valueParam) + case _ => invalid(IllegalIndexingOperation) + } case d: TypedDict => dictTyper.typeDictValue(d, e).map(toNodeResult) case union: TypedUnion => typeUnion(e, union) case TypedTaggedValue(underlying, _) => typeIndexer(e, underlying) diff --git a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala index 3455fad4c08..c59145cb0ad 100644 --- a/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala +++ b/scenario-compiler/src/test/scala/pl/touk/nussknacker/engine/spel/SpelExpressionSpec.scala @@ -906,6 +906,28 @@ class SpelExpressionSpec extends AnyFunSuite with Matchers with ValidatedValuesD parse[java.math.BigDecimal]("-1.1", ctx) shouldBe Symbol("valid") } + test("should not validate map indexing if index type and map key type are different") { + parse[Any]("""{{key: "a", value: 5}}.toMap[0]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1, value: 5}}.toMap["0"]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1.toLong, value: 5}}.toMap[0]""") shouldBe Symbol("invalid") + parse[Any]("""{{key: 1, value: 5}}.toMap[0.toLong]""") shouldBe Symbol("invalid") + } + + test("should validate map indexing if index type and map key type are the same") { + parse[Any]("""{{key: 1, value: 5}}.toMap[0]""") shouldBe Symbol("valid") + } + + test("should handle map indexing with unknown key type") { + val context = Context("sth").withVariables( + Map( + "unknownString" -> ContainerOfUnknown("a"), + ) + ) + + evaluate[Int]("""{{key: "a", value: 5}}.toMap[#unknownString.value]""", context) shouldBe 5 + evaluate[Integer]("""{{key: "b", value: 5}}.toMap[#unknownString.value]""", context) shouldBe null + } + test("validate ternary operator") { parse[Long]("'d'? 3 : 4", ctx) should not be Symbol("valid") parse[String]("1 > 2 ? 12 : 23", ctx) should not be Symbol("valid")