• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TouK / nussknacker / 5976637142

25 Aug 2023 01:43PM UTC coverage: 81.47% (+0.03%) from 81.438%
5976637142

push

github

Filemon279
Fix migration in 1.11

25 of 25 new or added lines in 2 files covered. (100.0%)

14865 of 18246 relevant lines covered (81.47%)

5.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.99
/interpreter/src/main/scala/pl/touk/nussknacker/engine/spel/Typer.scala
1
package pl.touk.nussknacker.engine.spel
2

3
import cats.data.Validated.{Invalid, Valid}
4
import cats.data.{NonEmptyList, Validated, ValidatedNel, Writer}
5
import cats.instances.list._
6
import cats.instances.map._
7
import cats.kernel.{Monoid, Semigroup}
8
import cats.syntax.traverse._
9
import com.typesafe.scalalogging.LazyLogging
10
import org.springframework.expression.common.{CompositeStringExpression, LiteralExpression}
11
import org.springframework.expression.spel.ast._
12
import org.springframework.expression.spel.{SpelNode, standard}
13
import org.springframework.expression.{EvaluationContext, Expression}
14
import pl.touk.nussknacker.engine.TypeDefinitionSet
15
import pl.touk.nussknacker.engine.api.Context
16
import pl.touk.nussknacker.engine.api.context.ValidationContext
17
import pl.touk.nussknacker.engine.api.dict.DictRegistry
18
import pl.touk.nussknacker.engine.api.expression._
19
import pl.touk.nussknacker.engine.api.generics.ExpressionParseError
20
import pl.touk.nussknacker.engine.api.typed.supertype.{CommonSupertypeFinder, NumberTypesPromotionStrategy, SupertypeClassResolutionStrategy}
21
import pl.touk.nussknacker.engine.api.typed.typing._
22
import pl.touk.nussknacker.engine.definition.ProcessDefinitionExtractor.ExpressionDefinition
23
import pl.touk.nussknacker.engine.dict.{KeysDictTyper, LabelsDictTyper, SpelDictTyper}
24
import pl.touk.nussknacker.engine.expression.NullExpression
25
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.IllegalOperationError._
26
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.MissingObjectError.{ConstructionOfUnknown, NoPropertyError, NonReferenceError, UnresolvedReferenceError}
27
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.OperatorError._
28
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.PartTypeError
29
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.SelectionProjectionError.{IllegalProjectionError, IllegalSelectionError, IllegalSelectionTypeError}
30
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.TernaryOperatorError.{InvalidTernaryOperator, TernaryOperatorMismatchTypesError, TernaryOperatorNotBooleanError}
31
import pl.touk.nussknacker.engine.spel.SpelExpressionParseError.UnsupportedOperationError.{BeanReferenceError, MapWithExpressionKeysError, ModificationError}
32
import pl.touk.nussknacker.engine.spel.Typer._
33
import pl.touk.nussknacker.engine.spel.ast.SpelAst.SpelNodeId
34
import pl.touk.nussknacker.engine.spel.ast.SpelNodePrettyPrinter
35
import pl.touk.nussknacker.engine.spel.internal.EvaluationContextPreparer
36
import pl.touk.nussknacker.engine.spel.typer.{MapLikePropertyTyper, MethodReferenceTyper, TypeReferenceTyper}
37
import pl.touk.nussknacker.engine.util.MathUtils
38

39
import scala.annotation.tailrec
40
import scala.reflect.runtime._
41
import scala.util.{Failure, Success, Try}
42

43
private[spel] class Typer(commonSupertypeFinder: CommonSupertypeFinder,
44
                          dictTyper: SpelDictTyper, strictMethodsChecking: Boolean,
45
                          staticMethodInvocationsChecking: Boolean,
46
                          typeDefinitionSet: TypeDefinitionSet,
47
                          evaluationContextPreparer: EvaluationContextPreparer,
48
                          methodExecutionForUnknownAllowed: Boolean,
49
                          dynamicPropertyAccessAllowed: Boolean) extends LazyLogging {
50

51
  import ast.SpelAst._
52

53
  private lazy val evaluationContext: EvaluationContext = evaluationContextPreparer.prepareEvaluationContext(Context(""), Map.empty)
54

55
  private val methodReferenceTyper = new MethodReferenceTyper(typeDefinitionSet, methodExecutionForUnknownAllowed)
36✔
56

57
  private lazy val typeReferenceTyper = new TypeReferenceTyper(evaluationContext, typeDefinitionSet)
58

59
  type TypingR[T] = Writer[List[ExpressionParseError], T]
60
  type NodeTypingResult = TypingR[CollectedTypingResult]
61

62
  def typeExpression(expr: Expression, ctx: ValidationContext): ValidatedNel[ExpressionParseError, CollectedTypingResult] = {
63
    val (errors, result) = doTypeExpression(expr, ctx)
36✔
64
    NonEmptyList.fromList(errors).map(Invalid(_)).getOrElse(Valid(result))
36✔
65
  }
66

67
  def doTypeExpression(expr: Expression, ctx: ValidationContext): (List[ExpressionParseError], CollectedTypingResult) = {
68
    expr match {
69
      case e: standard.SpelExpression =>
70
        typeExpression(e, ctx)
36✔
71
      case e: CompositeStringExpression =>
4✔
72
        val (errors, _) = e.getExpressions.toList.map(doTypeExpression(_, ctx)).sequence
4✔
73
        // We drop intermediate results here:
74
        // * It's tricky to combine it as each of the subexpressions has it's own abstract tree with positions relative to the subexpression's starting position
75
        // * CompositeStringExpression is dedicated to template SpEL expressions. It cannot be nested (as templates cannot be nested)
76
        // * Currently we don't use intermediate typing results outside of Typer
77
        (errors, CollectedTypingResult.withEmptyIntermediateResults(TypingResultWithContext(Typed[String])))
4✔
78
      case _: LiteralExpression =>
79
        (Nil, CollectedTypingResult.withEmptyIntermediateResults(TypingResultWithContext(Typed[String])))
4✔
80
      case _: NullExpression =>
81
        (Nil, CollectedTypingResult.withEmptyIntermediateResults(TypingResultWithContext(Typed[String])))
×
82
    }
83
  }
84

85
  private def typeExpression(spelExpression: standard.SpelExpression, ctx: ValidationContext): (List[ExpressionParseError], CollectedTypingResult) = {
86
    val ast = spelExpression.getAST
36✔
87
    val (errors, collectedResult) = typeNode(ctx, ast, TypingContext(List.empty, Map.empty)).run
36✔
88
    logger.whenTraceEnabled {
89
      val printer = new SpelNodePrettyPrinter(n => collectedResult.intermediateResults.get(SpelNodeId(n)).map(_.display).getOrElse("NOT_TYPED"))
90
      logger.trace(s"typed nodes: ${printer.print(ast)}, errors: ${errors.mkString(", ")}")
91
    }
92
    (errors, collectedResult)
36✔
93
  }
94

95
  private def typeNode(validationContext: ValidationContext, node: SpelNode, current: TypingContext): NodeTypingResult = {
96

97
    def toNodeResult(typ: TypingResult) = current.toResult(TypedNode(node, TypingResultWithContext(typ)))
36✔
98

99
    def validNodeResult(typ: TypingResult) = valid(toNodeResult(typ))
36✔
100

101
    def invalidNodeResult(err: ExpressionParseError) = invalid(err).map(toNodeResult)
×
102

103
    val withTypedChildren = typeChildren(validationContext, node, current) _
26✔
104

105
    def fixedWithNewCurrent(newCurrent: TypingContext) = typeChildrenAndReturnFixed(validationContext, node, newCurrent) _
6✔
106

107
    val fixed = fixedWithNewCurrent(current)
36✔
108

109
    def withChildrenOfType[Parts: universe.TypeTag](result: TypingResult) = {
110
      val w = valid(result)
4✔
111
      withTypedChildren {
4✔
112
        case list if list.forall(_.canBeSubclassOf(Typed.fromDetailedType[Parts])) => w
4✔
113
        case _ => w.tell(List(PartTypeError))
2✔
114
      }
115
    }
116

117
    def withTwoChildrenOfType[A: universe.TypeTag, R: universe.TypeTag](op: (A, A) => R) = {
118
      val castExpectedType = CastTypedValue[A]()
10✔
119
      val resultType = Typed.fromDetailedType[R]
10✔
120
      withTypedChildren {
10✔
121
        case castExpectedType(left) :: castExpectedType(right) :: Nil =>
122
          val typeFromOp = for {
123
            leftValue <- left.valueOpt
10✔
124
            rightValue <- right.valueOpt
2✔
125
            res = op(leftValue, rightValue)
2✔
126
          } yield Typed.fromInstance(res)
2✔
127
          valid(typeFromOp.getOrElse(resultType))
10✔
128
        case _ =>
129
          invalid(PartTypeError, fallbackType = resultType)
2✔
130
      }
131
    }
132

133
    def catchUnexpectedErrors(block: => NodeTypingResult): NodeTypingResult = Try(block) match {
36✔
134
      case Success(value) =>
135
        value
36✔
136
      case Failure(e) =>
137
        throw new SpelCompilationException(node, e)
2✔
138
    }
139

140
    def typeUnion(e: Indexer, possibleTypes: Set[SingleTypingResult]): NodeTypingResult = {
141
      val typedPossibleTypes = possibleTypes.map(possibleType => typeIndexer(e, possibleType)).toList
×
142

143
      val typingResult = typedPossibleTypes.sequence.map(_.map(_.finalResult.typingResult).toSet).map(typingResults => Typed.apply(typingResults))
×
144
      typingResult.map(toNodeResult)
×
145
    }
146

147
    @tailrec
148
    def typeIndexer(e: Indexer, typingResult: TypingResult): NodeTypingResult = {
149
      typingResult match {
150
        case TypedClass(clazz, param :: Nil) if clazz.isAssignableFrom(classOf[java.util.List[_]]) || clazz.isAssignableFrom(classOf[Array[Object]]) => validNodeResult(param)
9✔
151
        case TypedClass(clazz, keyParam :: valueParam :: Nil) if clazz.isAssignableFrom(classOf[java.util.Map[_, _]]) => validNodeResult(valueParam)
4✔
152
        case d: TypedDict => dictTyper.typeDictValue(d, e).map(toNodeResult)
8✔
153
        case TypedUnion(possibleTypes) => typeUnion(e, possibleTypes)
×
154
        case TypedTaggedValue(underlying, _) => typeIndexer(e, underlying)
×
155
        case _ =>
4✔
156
          val w = validNodeResult(Unknown)
4✔
157
          if (dynamicPropertyAccessAllowed) w else w.tell(List(DynamicPropertyAccessError))
4✔
158
      }
159
    }
160

161
    catchUnexpectedErrors(node match {
36✔
162

163
      case e: Assign =>
164
        invalidNodeResult(ModificationError)
×
165
      case e: BeanReference =>
166
        invalidNodeResult(BeanReferenceError)
×
167
      case e: CompoundExpression => e.children match {
34✔
168
        case first :: rest =>
34✔
169
          val validatedLastType = rest.foldLeft(typeNode(validationContext, first, current)) {
34✔
170
            case (prevW, next) =>
171
              prevW.flatMap { prevResult =>
34✔
172
                typeNode(validationContext, next, current.pushOnStack(prevResult))
34✔
173
              }
174
          }
175
          validatedLastType.map { lastType =>
34✔
176
            CollectedTypingResult(lastType.intermediateResults + (SpelNodeId(e) -> lastType.finalResult), lastType.finalResult)
34✔
177
          }
178
        //should not happen as CompoundExpression doesn't allow this...
179
        case Nil => validNodeResult(Unknown)
×
180
      }
181

182
      case e: ConstructorReference => withTypedChildren { _ =>
6✔
183
        val className = e.getChild(0).toStringAST
6✔
184
        val classToUse = Try(evaluationContext.getTypeLocator.findType(className)).toOption
6✔
185
        //TODO: validate constructor parameters...
186
        val clazz = classToUse.flatMap(kl => typeDefinitionSet.get(kl).map(_.clazzName))
6✔
187
        clazz match {
188
          case Some(typedClass) => valid(typedClass)
6✔
189
          case None => invalid(ConstructionOfUnknown(classToUse))
2✔
190
        }
191
      }
192

193
      case e: Elvis => withTypedChildren(l => valid(Typed(l.toSet)))
×
194
      //TODO: what should be here?
195
      case e: FunctionReference => validNodeResult(Unknown)
4✔
196

197
      //TODO: what should be here?
198
      case e: Identifier => validNodeResult(Unknown)
6✔
199
      //TODO: what should be here?
200
      case e: Indexer =>
201
        current.stackHead
202
          .map(valid).getOrElse(invalid(IllegalIndexingOperation))
×
203
          .flatMap(typeIndexer(e, _))
16✔
204

205
      case e: Literal => validNodeResult(Typed.fromInstance(e.getLiteralValue.getValue))
36✔
206

207
      case e: InlineList => withTypedChildren { children =>
26✔
208
        val localSupertypeFinder = new CommonSupertypeFinder(SupertypeClassResolutionStrategy.AnySuperclass, true)
26✔
209
        def getSupertype(a: TypingResult, b: TypingResult): TypingResult =
210
          localSupertypeFinder.commonSupertype(a, b)(NumberTypesPromotionStrategy.ToSupertype)
24✔
211

212
        //We don't want Typed.empty here, as currently it means it won't validate for any signature
213
        val elementType = if (children.isEmpty) Unknown else children.reduce(getSupertype)
24✔
214
        valid(Typed.genericTypeClass[java.util.List[_]](List(elementType)))
26✔
215
      }
216

217
      case e: InlineMap =>
32✔
218
        val zipped = e.children.zipWithIndex
32✔
219
        val keys = zipped.filter(_._2 % 2 == 0).map(_._1)
32✔
220
        val values = zipped.filter(_._2 % 2 == 1).map(_._1)
32✔
221
        val literalKeys = keys
222
          .collect {
32✔
223
            case a: PropertyOrFieldReference => a.getName
26✔
224
            case b: StringLiteral => b.getLiteralValue.getValue.toString
16✔
225
          }
226

227
        typeChildrenNodes(validationContext, node, values, current) { typedValues =>
32✔
228
          if (literalKeys.size != keys.size) {
32✔
229
            invalid(MapWithExpressionKeysError)
×
230
          } else {
231
            valid(TypedObjectTypingResult(literalKeys.zip(typedValues).toMap))
32✔
232
          }
233
        }
234
      case e: MethodReference =>
235
        extractMethodReference(e, validationContext, node, current)
22✔
236

237
      case e: OpEQ => checkEqualityLikeOperation(validationContext, e, current, isEquality = true)
16✔
238
      case e: OpNE => checkEqualityLikeOperation(validationContext, e, current, isEquality = false)
12✔
239

240
      case e: OpAnd => withTwoChildrenOfType[Boolean, Boolean](_ && _)
3✔
241
      case e: OpOr => withTwoChildrenOfType[Boolean, Boolean](_ || _)
×
242
      case e: OpGE => withTwoChildrenOfType(MathUtils.greaterOrEqual)
2✔
243
      case e: OpGT => withTwoChildrenOfType(MathUtils.greater)
6✔
244
      case e: OpLE => withTwoChildrenOfType(MathUtils.lesserOrEqual)
2✔
245
      case e: OpLT => withTwoChildrenOfType(MathUtils.lesser)
1✔
246

247
      case e: OpDec => checkSingleOperandArithmeticOperation(validationContext, e, current)(MathUtils.minus(_, 1))
×
248
      case e: OpInc => checkSingleOperandArithmeticOperation(validationContext, e, current)(MathUtils.plus(_, 1))
2✔
249

250
      case e: OpDivide =>
16✔
251
        val op = (x: Number, y: Number) =>
252
          if (y.doubleValue() == 0) Invalid(DivisionByZeroError(e.toStringAST))
2✔
253
          else Valid(MathUtils.divide(x, y))
2✔
254
        checkTwoOperandsArithmeticOperation(validationContext, e, current)(Some(op))(NumberTypesPromotionStrategy.ForMathOperation)
16✔
255

256
      case e: OpMinus =>
257
        withTypedChildren {
10✔
258
          case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) =>
2✔
259
            val supertype = commonSupertypeFinder.commonSupertype(left, right)(NumberTypesPromotionStrategy.ForMathOperation).withoutValue
2✔
260
            operationOnTypesValue[Number, Number, Number](left, right, supertype)((n1, n2) => Valid(MathUtils.minus(n1, n2)))
2✔
261
          case left :: right :: Nil if left == right =>
2✔
262
            invalid(OperatorNonNumericError(e.getOperatorName, left))
2✔
263
          case left :: right :: Nil =>
264
            invalid(OperatorMismatchTypeError(e.getOperatorName, left, right))
2✔
265
          case left :: Nil if left.canBeSubclassOf(Typed[Number]) =>
10✔
266
            val resultType = left.withoutValue
10✔
267
            val result = operationOnTypesValue[Number, Number](left)(MathUtils.negate).getOrElse(resultType)
10✔
268
            valid(result)
10✔
269
          case left :: Nil =>
270
            invalid(OperatorNonNumericError(e.getOperatorName, left))
×
271
          case Nil =>
272
            invalid(EmptyOperatorError(e.getOperatorName))
×
273
          case _ => throw new IllegalStateException("should not happen")
×
274
        }
275
      case e: OpModulus =>
2✔
276
        val op = (x: Number, y: Number) =>
277
          if (y.doubleValue() == 0) Invalid(ModuloZeroError(e.toStringAST))
2✔
278
          else Valid(MathUtils.remainder(x, y))
2✔
279
        checkTwoOperandsArithmeticOperation(validationContext, e, current)(Some(op))(NumberTypesPromotionStrategy.ForMathOperation)
2✔
280
      case e: OpMultiply =>
281
        checkTwoOperandsArithmeticOperation(validationContext, e, current)(Some((n1, n2) => Valid(MathUtils.multiply(n1, n2))))(NumberTypesPromotionStrategy.ForMathOperation)
6✔
282
      case e: OperatorPower =>
283
        checkTwoOperandsArithmeticOperation(validationContext, e, current)(None)(NumberTypesPromotionStrategy.ForPowerOperation)
2✔
284

285
      case e: OpPlus => withTypedChildren {
18✔
286
        case left :: right :: Nil if left == Unknown || right == Unknown =>
18✔
287
          valid(Unknown)
10✔
288
        case left :: right :: Nil if left.canBeSubclassOf(Typed[String]) || right.canBeSubclassOf(Typed[String]) =>
16✔
289
          operationOnTypesValue[Any, Any, String](left, right, Typed[String])((l, r) => Valid(l.toString + r.toString))
8✔
290
        case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) =>
4✔
291
          val supertype = commonSupertypeFinder.commonSupertype(left, right)(NumberTypesPromotionStrategy.ForMathOperation).withoutValue
4✔
292
          operationOnTypesValue[Number, Number, Number](left, right, supertype)((n1, n2) => Valid(MathUtils.plus(n1, n2)))
3✔
293
        case left :: right :: Nil =>
294
          invalid(OperatorMismatchTypeError(e.getOperatorName, left, right))
2✔
295
        case left :: Nil if left.canBeSubclassOf(Typed[Number]) =>
2✔
296
          valid(left)
2✔
297
        case left :: Nil =>
298
          invalid(OperatorNonNumericError(e.getOperatorName, left))
×
299
        case Nil =>
300
          invalid(EmptyOperatorError(e.getOperatorName))
×
301
        case _ => throw new IllegalStateException("should not happen")
×
302
      }
303
      case e: OperatorBetween => fixed(Typed[Boolean])
×
304
      case e: OperatorInstanceof => fixed(Typed[Boolean])
×
305
      case e: OperatorMatches => withChildrenOfType[String](Typed[Boolean])
×
306
      case e: OperatorNot => withChildrenOfType[Boolean](Typed[Boolean])
4✔
307

308
      case e: Projection =>
309
        for {
310
          iterateType <- current.stackHead.map(valid).getOrElse(invalid(IllegalProjectionError))
2✔
311
          elementType <- extractIterativeType(iterateType)
4✔
312
          result <- typeChildren(validationContext, node, current.pushOnStack(elementType)) {
4✔
313
            case result :: Nil =>
314
              valid(Typed.genericTypeClass[java.util.List[_]](List(result)))
4✔
315
            case other =>
316
              invalid(IllegalSelectionTypeError(other))
×
317
          }
318
        } yield result
319

320
      case e: PropertyOrFieldReference =>
321
        current.stackHead.map(extractProperty(e, _))
34✔
322
          .getOrElse(invalid(NonReferenceError(e.toStringAST)))
4✔
323
          .map(toNodeResult)
34✔
324
      //TODO: what should be here?
325
      case e: QualifiedIdentifier => fixed(Unknown)
6✔
326

327
      case e: Selection =>
328
        for {
329
          iterateType <- current.stackHead.map(valid).getOrElse(invalid(IllegalSelectionError))
4✔
330
          elementType <- extractIterativeType(iterateType)
4✔
331
          selectionType = resolveSelectionTypingResult(e, iterateType, elementType)
4✔
332
          result <- typeChildren(validationContext, node, current.pushOnStack(elementType)) {
4✔
333
            case result :: Nil if result.canBeSubclassOf(Typed[Boolean]) =>
4✔
334
              valid(selectionType)
4✔
335
            case other =>
336
              invalid(IllegalSelectionTypeError(other), selectionType)
×
337
          }
338
        } yield result
339
      case e: Ternary => withTypedChildren {
8✔
340
        case condition :: onTrue :: onFalse :: Nil =>
341
          val superType = commonSupertypeFinder.commonSupertype(onTrue, onFalse)(NumberTypesPromotionStrategy.ToSupertype)
8✔
342
          for {
343
            _ <- Option(condition).filter(_.canBeSubclassOf(Typed[Boolean])).map(valid).getOrElse(invalid(TernaryOperatorNotBooleanError(condition)))
5✔
344
            result <- if (superType == Typed.empty) invalid(TernaryOperatorMismatchTypesError(onTrue, onFalse)) else valid(superType)
5✔
345
          } yield result
346
        case _ =>
347
          invalid(InvalidTernaryOperator) // shouldn't happen
×
348
      }
349

350
      case e: TypeReference =>
351
        if (staticMethodInvocationsChecking) {
14✔
352
          typeReferenceTyper.typeTypeReference(e)
353
            .map(typedClass => current.toResult(TypedNode(e, TypingResultWithContext.withStaticContext(typedClass))))
14✔
354
        } else {
355
          validNodeResult(Unknown)
2✔
356
        }
357

358
      case e: VariableReference =>
36✔
359
        //only sane way of getting variable name :|
360
        val name = e.toStringAST.substring(1)
36✔
361
        validationContext.get(name).orElse(current.stackHead.filter(_ => name == "this"))
10✔
362
          .map(valid).getOrElse(invalid(UnresolvedReferenceError(name)))
23✔
363
          .map(toNodeResult)
36✔
364
    })
365
  }
366

367
  private def operationOnTypesValue[A, R](typ: TypingResult)
368
                                         (op: A => R): Option[TypingResult] =
369
    typ.valueOpt.map(v => Typed.fromInstance(op(v.asInstanceOf[A])))
10✔
370

371
  private def operationOnTypesValue[A, B, R](left: TypingResult, right: TypingResult, fallbackType: TypingResult)
372
                                            (op: (A, B) => Validated[ExpressionParseError, R]): TypingR[TypingResult] =
373
    (for {
374
      leftValue <- left.valueOpt
375
      rightValue <- right.valueOpt
18✔
376
      res = op(leftValue.asInstanceOf[A], rightValue.asInstanceOf[B])
8✔
377
    } yield {
378
      res.map(Typed.fromInstance)
8✔
379
        .map(valid)
8✔
380
        .valueOr(invalid(_, fallbackType))
8✔
381
    }).getOrElse(valid(fallbackType))
20✔
382

383
  //currently there is no better way than to check ast string starting with $ or ^
384
  private def resolveSelectionTypingResult(node: Selection, parentType: TypingResult, childElementType: TypingResult) = {
385
    val isSingleElementSelection = List("$", "^").map(node.toStringAST.startsWith(_)).foldLeft(false)(_ || _)
4✔
386
    if (isSingleElementSelection) childElementType else parentType
3✔
387
  }
388

389
  private def checkEqualityLikeOperation(validationContext: ValidationContext,
390
                                         node: Operator,
391
                                         current: TypingContext,
392
                                         isEquality: Boolean): TypingR[CollectedTypingResult] = {
393
    typeChildren(validationContext, node, current) {
20✔
394
      case TypedObjectWithValue(leftVariable, leftValue) ::
395
        TypedObjectWithValue(rightVariable, rightValue) :: Nil =>
396
        checkEqualityComparableTypes(leftVariable, rightVariable, node)
397
          .map(x => TypedObjectWithValue(x.asInstanceOf[TypedClass], leftValue == rightValue ^ !isEquality))
2✔
398
      case left :: right :: Nil =>
399
        checkEqualityComparableTypes(left, right, node)
20✔
400
      case _ =>
401
        invalid(BadOperatorConstructionError(node.getOperatorName), fallbackType = Typed[Boolean])
×
402
    }
403
  }
404

405
  private def checkEqualityComparableTypes(left: TypingResult, right: TypingResult, node: Operator): TypingR[TypingResult] = {
406
    val w = valid(Typed[Boolean])
20✔
407
    if (commonSupertypeFinder.commonSupertype(left, right)(NumberTypesPromotionStrategy.ToSupertype) != Typed.empty) {
20✔
408
      w
20✔
409
    } else
410
      w.tell(List(OperatorNotComparableError(node.getOperatorName, left, right)))
4✔
411
  }
412

413
  private def checkTwoOperandsArithmeticOperation(validationContext: ValidationContext, node: Operator, current: TypingContext)
414
                                                 (op: Option[(Number, Number) => Validated[ExpressionParseError, Any]])
415
                                                 (implicit numberPromotionStrategy: NumberTypesPromotionStrategy): TypingR[CollectedTypingResult] = {
416
      typeChildren(validationContext, node, current) {
16✔
417
        case left :: right :: Nil if left.canBeSubclassOf(Typed[Number]) && right.canBeSubclassOf(Typed[Number]) =>
16✔
418
          val supertype = commonSupertypeFinder.commonSupertype(left, right).withoutValue
16✔
419
          op
420
            .map(operationOnTypesValue[Number, Number, Any](left, right, supertype)(_))
16✔
421
            .getOrElse(valid(supertype))
9✔
422
        case left :: right :: Nil =>
423
          val supertype = commonSupertypeFinder.commonSupertype(left, right).withoutValue
2✔
424
          invalid(OperatorMismatchTypeError(node.getOperatorName, left, right), fallbackType = supertype)
2✔
425
        case _ =>
426
          invalid(BadOperatorConstructionError(node.getOperatorName)) // shouldn't happen
×
427
      }
428
    }
429

430
  private def checkSingleOperandArithmeticOperation(validationContext: ValidationContext, node: Operator, current: TypingContext)
431
                                                   (op: Number => Any):
432
    TypingR[CollectedTypingResult] = {
433
    typeChildren(validationContext, node, current) {
2✔
434
      case left :: Nil if left.canBeSubclassOf(Typed[Number]) =>
2✔
435
        val result = operationOnTypesValue[Number, Any](left)(op).getOrElse(left.withoutValue)
1✔
436
        valid(result)
2✔
437
      case left :: Nil =>
438
        invalid(OperatorNonNumericError(node.getOperatorName, left), fallbackType = left)
×
439
      case _ =>
440
        invalid(BadOperatorConstructionError(node.getOperatorName)) // shouldn't happen
×
441
    }
442
  }
443

444
  private def extractProperty(e: PropertyOrFieldReference, t: TypingResult): TypingR[TypingResult] = t match {
445
    case Unknown =>
10✔
446
      val w = Writer.value[List[ExpressionParseError], TypingResult](Unknown)
10✔
447
      if (methodExecutionForUnknownAllowed)
10✔
448
        w
2✔
449
      else
450
        w.tell(List(IllegalPropertyAccessError(Unknown)))
10✔
451
    case TypedNull =>
452
      invalid(IllegalPropertyAccessError(TypedNull), fallbackType = TypedNull)
2✔
453
    case s: SingleTypingResult =>
454
      extractSingleProperty(e)(s)
34✔
455
    case TypedUnion(possible) =>
4✔
456
      val l = possible.toList.map(single => extractSingleProperty(e)(single))
4✔
457
        .filter(_.written.isEmpty)
4✔
458
        .map(_.value)
4✔
459
      if (l.isEmpty) {
4✔
460
        invalid(NoPropertyError(t, e.getName))
4✔
461
      } else
462
        valid(Typed(l.toSet))
4✔
463
  }
464

465
  private def extractMethodReference(reference: MethodReference,
466
                                     validationContext: ValidationContext,
467
                                     node: SpelNode,
468
                                     context: TypingContext): TypingR[CollectedTypingResult] = {
469
    (context.stack match {
22✔
470
      case head :: tail =>
471
        valid((head, tail))
22✔
472
      case Nil =>
473
        invalid(InvalidMethodReference(reference.toStringAST))
2✔
474
          .map(TypingResultWithContext(_))
2✔
475
          .map((_, context.stack))
2✔
476
    }).flatMap {
22✔
477
      case (head, tail) =>
478
        typeChildren(validationContext, node, context.copy(stack = tail)) { typedParams =>
22✔
479
          methodReferenceTyper.typeMethodReference(typer.MethodReference(
22✔
480
            head.typingResult,
22✔
481
            head.staticContext,
22✔
482
            reference.getName,
22✔
483
            typedParams)
484
          ) match {
485
            case Right(x) => valid(x)
22✔
486
            case Left(x) if strictMethodsChecking => invalid(x)
6✔
487
            case Left(_) => valid(Unknown)
2✔
488
          }
489
        }
490
    }
491
  }
492

493
  @tailrec
494
  private def extractSingleProperty(e: PropertyOrFieldReference)
495
                                   (t: SingleTypingResult): TypingR[TypingResult] = {
496
    t match {
497
      case typedObjectWithData: TypedObjectWithData =>
498
        extractSingleProperty(e)(typedObjectWithData.objType)
4✔
499
      case typedClass: TypedClass =>
500
        propertyTypeBasedOnMethod(typedClass, e)
501
          .orElse(MapLikePropertyTyper.mapLikeValueType(typedClass))
10✔
502
          .map(valid)
26✔
503
          .getOrElse(invalid(NoPropertyError(t, e.getName)))
17✔
504
      case TypedObjectTypingResult(fields, objType, _) =>
28✔
505
        val typeBasedOnFields = fields.get(e.getName)
28✔
506
        typeBasedOnFields.orElse(propertyTypeBasedOnMethod(objType, e))
8✔
507
          .map(valid)
28✔
508
          .getOrElse(invalid(NoPropertyError(t, e.getName)))
8✔
509
      case dict: TypedDict =>
510
        dictTyper.typeDictValue(dict, e)
4✔
511
    }
512
  }
513

514
  private def propertyTypeBasedOnMethod(typedClass: TypedClass, e: PropertyOrFieldReference) = {
515
    typeDefinitionSet.get(typedClass.klass).flatMap(_.getPropertyOrFieldType(e.getName))
26✔
516
  }
517

518
  private def extractIterativeType(parent: TypingResult): TypingR[TypingResult] = parent match {
519
    case tc: SingleTypingResult if tc.objType.canBeSubclassOf(Typed[java.util.Collection[_]]) =>
4✔
520
      valid(tc.objType.params.headOption.getOrElse(Unknown))
2✔
521
    case tc: SingleTypingResult if tc.objType.canBeSubclassOf(Typed[java.util.Map[_, _]]) =>
4✔
522
      valid(TypedObjectTypingResult(Map(
4✔
523
        "key" -> tc.objType.params.headOption.getOrElse(Unknown),
4✔
524
        "value" -> tc.objType.params.drop(1).headOption.getOrElse(Unknown))))
2✔
525
    case tc: SingleTypingResult if tc.objType.klass.isArray =>
2✔
526
      valid(tc.objType.params.headOption.getOrElse(Unknown))
2✔
527
    case tc: SingleTypingResult =>
528
      invalid(IllegalProjectionSelectionError(tc))
2✔
529
    //FIXME: what if more results are present?
530
    case _ => valid(Unknown)
2✔
531
  }
532

533
  private def typeChildrenAndReturnFixed(validationContext: ValidationContext, node: SpelNode, current: TypingContext)
534
                                        (result: TypingResult): TypingR[CollectedTypingResult] = {
535
    typeChildren(validationContext, node, current)(_ => valid(result))
6✔
536
  }
537

538
  private def typeChildren(validationContext: ValidationContext, node: SpelNode, current: TypingContext)
539
                          (result: List[TypingResult] => TypingR[TypingResult])
540
  : TypingR[CollectedTypingResult] = {
541
    typeChildrenNodes(validationContext, node, node.children, current)(result)
28✔
542
  }
543

544
  private def typeChildrenNodes(validationContext: ValidationContext, node: SpelNode, children: List[SpelNode], current: TypingContext)
545
                               (result: List[TypingResult] => TypingR[TypingResult])
546
  : TypingR[CollectedTypingResult] = {
547
    children.map(typeNode(validationContext, _, current.withoutIntermediateResults)).sequence.flatMap { collectedChildrenResults =>
34✔
548
      val intermediateResultsCombination = Monoid.combineAll(current.intermediateResults :: collectedChildrenResults.map(_.intermediateResults))
34✔
549
      val intermediateTypes = collectedChildrenResults.map(_.finalResult)
34✔
550
      result(intermediateTypes.map(_.typingResult))
34✔
551
        .map(TypingResultWithContext(_))
34✔
552
        .map(TypedNode(node, _))
34✔
553
        .map(CollectedTypingResult.withIntermediateAndFinal(intermediateResultsCombination, _))
34✔
554
    }
555
  }
556

557
  private def valid[T](value: T): TypingR[T] = Writer(List.empty[ExpressionParseError], value)
36✔
558

559
  private def invalid[T](err: ExpressionParseError, fallbackType: TypingResult = Unknown): TypingR[TypingResult] =
560
    Writer(List(err), fallbackType)
12✔
561

562
  def withDictTyper(dictTyper: SpelDictTyper) =
563
    new Typer(commonSupertypeFinder, dictTyper, strictMethodsChecking = strictMethodsChecking,
8✔
564
      staticMethodInvocationsChecking, typeDefinitionSet, evaluationContextPreparer, methodExecutionForUnknownAllowed, dynamicPropertyAccessAllowed)
8✔
565

566
}
567

568
object Typer {
569
  def default(classLoader: ClassLoader, expressionConfig: ExpressionDefinition[_], spelDictTyper: SpelDictTyper, typeDefinitionSet: TypeDefinitionSet): Typer = {
570
    val evaluationContextPreparer = EvaluationContextPreparer.default(classLoader, expressionConfig)
36✔
571

572
    val strictTypeChecking = expressionConfig.strictTypeChecking
36✔
573
    val classResolutionStrategy = if (strictTypeChecking) SupertypeClassResolutionStrategy.Intersection else SupertypeClassResolutionStrategy.Union
18✔
574
    val commonSupertypeFinder = new CommonSupertypeFinder(classResolutionStrategy, strictTypeChecking)
36✔
575
    new Typer(commonSupertypeFinder,
36✔
576
      spelDictTyper,
577
      expressionConfig.strictMethodsChecking,
36✔
578
      expressionConfig.staticMethodInvocationsChecking,
36✔
579
      typeDefinitionSet,
580
      evaluationContextPreparer,
581
      expressionConfig.methodExecutionForUnknownAllowed,
36✔
582
      expressionConfig.dynamicPropertyAccessAllowed
36✔
583
    )
584
  }
585

586
  // This Semigroup is used in combining `intermediateResults: Map[SpelNodeId, TypingResult]` in Typer.
587
  // If there is no bug in Typer, collisions shouldn't happen
588
  implicit def notAcceptingMergingSemigroup: Semigroup[TypingResultWithContext] = new Semigroup[TypingResultWithContext] with LazyLogging {
34✔
589
    override def combine(x: TypingResultWithContext, y: TypingResultWithContext): TypingResultWithContext = {
590
      assert(x == y, s"Types not matching during combination of types for spel nodes: $x != $y")
×
591
      // merging the same types is not bad but it is a warning that sth went wrong e.g. typer typed something more than one time
592
      // or spel node's identity is broken
593
      logger.warn(s"Merging same types: $x for the same nodes. This shouldn't happen")
594
      x
595
    }
596
  }
597

598
  case class TypingResultWithContext private(typingResult: TypingResult, staticContext: Boolean) {
599

600
    def display: String = typingResult.display
×
601

602
  }
603

604
  object TypingResultWithContext {
605

606
    def apply(typingResult: TypingResult): TypingResultWithContext = new TypingResultWithContext(typingResult, staticContext = false)
36✔
607

608
    def withStaticContext(typingResult: TypingResult) = new TypingResultWithContext(typingResult, staticContext = true)
14✔
609

610
    private def apply(typingResult: TypingResult, staticContext: Boolean) = new TypingResultWithContext(typingResult, staticContext)
×
611

612
  }
613

614
  /**
615
    * It contains stack of types for recognition of nested node type.
616
    * intermediateResults are all results that we can collect for intermediate nodes
617
    */
618
  private case class TypingContext(stack: List[TypingResultWithContext], intermediateResults: Map[SpelNodeId, TypingResultWithContext]) {
619

620
    def pushOnStack(typingResult: TypingResult): TypingContext = copy(stack = TypingResultWithContext(typingResult) :: stack)
4✔
621

622
    def pushOnStack(typingResult: CollectedTypingResult): TypingContext =
623
      TypingContext(typingResult.finalResult :: stack, intermediateResults ++ typingResult.intermediateResults)
34✔
624

625
    def stackHead: Option[TypingResult] = stack.headOption.map(_.typingResult)
34✔
626

627
    def withoutIntermediateResults: TypingContext = copy(intermediateResults = Map.empty)
34✔
628

629
    def toResult(finalNode: TypedNode): CollectedTypingResult =
630
      CollectedTypingResult(intermediateResults + (finalNode.nodeId -> finalNode.typ), finalNode.typ)
36✔
631

632
  }
633

634
  class SpelCompilationException(node: SpelNode, cause: Throwable)
635
    extends RuntimeException(s"Can't compile SpEL expression: `${node.toStringAST}`, message: `${cause.getMessage}`.", cause)
636
}
637

638
private[spel] case class TypedNode(nodeId: SpelNodeId, typ: TypingResultWithContext)
639

640
private[spel] object TypedNode {
641

642
  def apply(node: SpelNode, typ: TypingResultWithContext): TypedNode =
643
    TypedNode(SpelNodeId(node), typ)
36✔
644

645
}
646

647
// TODO: move intermediateResults into Writer's L
648
private[spel] case class CollectedTypingResult(intermediateResults: Map[SpelNodeId, TypingResultWithContext], finalResult: TypingResultWithContext) {
649
  def typingInfo: SpelExpressionTypingInfo = SpelExpressionTypingInfo(intermediateResults.map(intermediateResult => (intermediateResult._1 -> intermediateResult._2.typingResult)), finalResult.typingResult)
36✔
650
}
651

652
private[spel] object CollectedTypingResult {
653

654
  def withEmptyIntermediateResults(finalResult: TypingResultWithContext): CollectedTypingResult =
655
    CollectedTypingResult(Map.empty, finalResult)
4✔
656

657
  def withIntermediateAndFinal(intermediateResults: Map[SpelNodeId, TypingResultWithContext], finalNode: TypedNode): CollectedTypingResult = {
658
    CollectedTypingResult(intermediateResults + (finalNode.nodeId -> finalNode.typ), finalNode.typ)
34✔
659
  }
660

661
}
662

663
case class SpelExpressionTypingInfo(intermediateResults: Map[SpelNodeId, TypingResult],
664
                                    typingResult: TypingResult) extends ExpressionTypingInfo
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc