• 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

91.06
/interpreter/src/main/scala/pl/touk/nussknacker/engine/types/EspTypeUtils.scala
1
package pl.touk.nussknacker.engine.types
2

3
import java.lang.reflect._
4
import java.util.Optional
5
import cats.data.{NonEmptyList, StateT}
6
import cats.data.Validated.Invalid
7
import cats.effect.IO
8
import cats.implicits.catsSyntaxSemigroup
9
import org.apache.commons.lang3.{ClassUtils, StringUtils}
10
import pl.touk.nussknacker.engine.api.generics.{GenericType, MethodTypeInfo, Parameter, TypingFunction}
11
import pl.touk.nussknacker.engine.api.process.PropertyFromGetterExtractionStrategy.{AddPropertyNextToGetter, DoNothing, ReplaceGetterWithProperty}
12
import pl.touk.nussknacker.engine.api.process.{ClassExtractionSettings, VisibleMembersPredicate}
13
import pl.touk.nussknacker.engine.api.typed.typing.{SingleTypingResult, Typed, TypedNull, TypedUnion, TypingResult, Unknown}
14
import pl.touk.nussknacker.engine.api.{Documentation, ParamName}
15
import pl.touk.nussknacker.engine.definition.TypeInfos.{ClazzDefinition, FunctionalMethodInfo, MethodInfo, StaticMethodInfo}
16

17
import java.lang.annotation.Annotation
18

19
object EspTypeUtils {
20

21
  import pl.touk.nussknacker.engine.util.Implicits._
22

23
  def clazzDefinition(clazz: Class[_])
24
                     (implicit settings: ClassExtractionSettings): ClazzDefinition =
25
    ClazzDefinition(
34✔
26
      Typed(clazz),
34✔
27
      extractPublicMethodsAndFields(clazz, staticMethodsAndFields = false),
34✔
28
      extractPublicMethodsAndFields(clazz, staticMethodsAndFields = true)
34✔
29
    )
30

31
  private def extractPublicMethodsAndFields(clazz: Class[_], staticMethodsAndFields: Boolean)
32
                                           (implicit settings: ClassExtractionSettings): Map[String, List[MethodInfo]] = {
33
    val membersPredicate = settings.visibleMembersPredicate(clazz)
34✔
34
    val methods = extractPublicMethods(clazz, membersPredicate, staticMethodsAndFields)
34✔
35
    val fields = extractPublicFields(clazz, membersPredicate, staticMethodsAndFields).mapValuesNow(List(_))
34✔
36
    filterHiddenParameterAndReturnType(methods ++ fields)
34✔
37
  }
38

39
  private def extractPublicMethods(clazz: Class[_], membersPredicate: VisibleMembersPredicate, staticMethodsAndFields: Boolean)
40
                                  (implicit settings: ClassExtractionSettings): Map[String, List[MethodInfo]] = {
41
    /* From getMethods javadoc: If this {@code Class} object represents an interface then the returned array
42
           does not contain any implicitly declared methods from {@code Object}.
43
           The same for primitives - we assume that languages like SpEL will be able to do boxing
44
           It could be significant only for toString, as we filter out other Object methods, but to be consistent...
45
         */
46
    val additionalMethods = if (clazz.isInterface) {
34✔
47
      classOf[Object].getMethods.toList
34✔
48
    } else if (clazz.isPrimitive) {
34✔
49
      ClassUtils.primitiveToWrapper(clazz).getMethods.toList
×
50
    } else {
51
      List.empty
34✔
52
    }
53
    val publicMethods = clazz.getMethods.toList ++ additionalMethods
34✔
54

55
    val methods =
56
      if (staticMethodsAndFields) publicMethods.filter(membersPredicate.shouldBeVisible).filter(m => Modifier.isStatic(m.getModifiers))
34✔
57
      else publicMethods.filter(membersPredicate.shouldBeVisible).filter(m => !Modifier.isStatic(m.getModifiers))
34✔
58

59
    // "varargs" annotation generates two methods - one with scala style varArgs
60
    // and one with java style varargs. We want only the second one so we have
61
    // to filter them.
62
    val filteredMethods = methods.filter(extractJavaVersionOfVarArgMethod(_).isEmpty)
34✔
63

64
    val methodNameAndInfoList = filteredMethods
65
      .flatMap(extractMethod(_))
34✔
66

67
    val staticMethodInfos = methodNameAndInfoList.filter(_._2.isInstanceOf[StaticMethodInfo]).asInstanceOf[List[(String, StaticMethodInfo)]]
34✔
68
    val functionalMethodInfos = methodNameAndInfoList.filter(_._2.isInstanceOf[FunctionalMethodInfo])
34✔
69
    val groupedFunctionalMethodInfos = functionalMethodInfos.groupBy(_._1).mapValuesNow(_.map(_._2)).toMap
21✔
70

71
    deduplicateMethodsWithGenericReturnType(staticMethodInfos)
72
      .asInstanceOf[Map[String, List[MethodInfo]]]
73
      .combine(groupedFunctionalMethodInfos)
34✔
74
  }
75

76
  //We have to filter here, not in ClassExtractionSettings, as we do e.g. boxed/unboxed mapping on TypedClass level...
77
  private def filterHiddenParameterAndReturnType(infos: Map[String, List[MethodInfo]])
78
                                                (implicit settings: ClassExtractionSettings): Map[String, List[MethodInfo]] = {
79
    def typeResultVisible(t: TypingResult): Boolean = t match {
80
      case str: SingleTypingResult =>
81
        !settings.isHidden(str.objType.klass) && str.objType.params.forall(typeResultVisible)
34✔
82
      case TypedUnion(ts) => ts.forall(typeResultVisible)
4✔
83
      case TypedNull => true
×
84
      case Unknown => true
34✔
85
    }
86
    def filterOneMethod(methodInfo: MethodInfo): Boolean = {
87
      val noVarArgTypes = methodInfo.signatures.toList.flatMap(_.noVarArgs).map(_.refClazz)
34✔
88
      val varArgTypes = methodInfo.signatures.toList.flatMap(_.varArg.toList).map(_.refClazz)
34✔
89
      val resultTypes = methodInfo.signatures.toList.map(_.result)
34✔
90
      (noVarArgTypes ::: varArgTypes ::: resultTypes).forall(typeResultVisible)
34✔
91
    }
92
    infos.mapValuesNow(methodList => methodList.filter(filterOneMethod)).filter(_._2.nonEmpty)
34✔
93
  }
94

95
  /*
96
    This is tricky case. If we have generic class and concrete subclasses, e.g.
97
    - ChronoLocalDateTime<D extends ChronoLocalDate>
98
    - LocalDateTime extends ChronoLocalDateTime<LocalDate>
99
    and method with generic return type from superclass: D toLocalDate()
100
    getMethods return two toLocalDate methods:
101
      ChronoLocalDate toLocalDate()
102
      LocalDate toLocalDate()
103
    In our case the second one is correct
104
   */
105
  private def deduplicateMethodsWithGenericReturnType(methodNameAndInfoList: List[(String, StaticMethodInfo)]) = {
106
    val groupedByNameAndParameters = methodNameAndInfoList.groupBy(mi => (mi._1, mi._2.signature.noVarArgs, mi._2.signature.varArg))
34✔
107
    groupedByNameAndParameters.toList.map {
34✔
108
      case (_, methodsForParams) =>
109
        /*
110
          we want to find "most specific" class, however surprisingly it's not always possible, because we treat e.g. isLeft and left methods
111
          as equal (for javabean-like access) and e.g. in scala Either this is perfectly possible. In case we cannot find most specific
112
          class we pick arbitrary one (we sort to avoid randomness)
113
         */
114

115
        methodsForParams.find { case (_, methodInfo) =>
116
          methodsForParams.forall(mi => methodInfo.signature.result.canBeSubclassOf(mi._2.signature.result))
34✔
117
        }.getOrElse(methodsForParams.minBy(_._2.signature.result.display))
×
118
    }.toGroupedMap
34✔
119
      //we sort only to avoid randomness
120
      .mapValuesNow(_.sortBy(_.toString))
34✔
121
  }
122

123
  // SpEL is able to access getters using property name so you can write `obj.foo` instead of `obj.getFoo`
124
  private def collectMethodNames(method: Method)
125
                                (implicit settings: ClassExtractionSettings): List[String] = {
126
    val isGetter = method.getName.matches("^(get|is).+") && method.getParameterCount == 0
34✔
127
    if (isGetter) {
34✔
128
      val propertyMethod = StringUtils.uncapitalize(method.getName.replaceAll("^get|^is", ""))
34✔
129
      settings.propertyExtractionStrategy match {
34✔
130
        case AddPropertyNextToGetter    => List(method.getName, propertyMethod)
34✔
131
        case ReplaceGetterWithProperty  => List(propertyMethod)
2✔
132
        case DoNothing                  => List(method.getName)
2✔
133
      }
134
    } else {
135
      List(method.getName)
34✔
136
    }
137
  }
138

139
  private def extractMethod(method: Method)
140
                           (implicit settings: ClassExtractionSettings): List[(String, MethodInfo)] =
141
    extractAnnotation(method, classOf[GenericType]) match {
34✔
142
      case None => extractRegularMethod(method)
34✔
143
      case Some(annotation) => extractGenericMethod(method, annotation)
8✔
144
    }
145

146
  private def getTypeFunctionInstanceFromAnnotation(method: Method, genericType: GenericType): TypingFunction = {
147
    val typeFunctionClass = genericType.typingFunction()
8✔
148
    try {
8✔
149
      val typeFunctionConstructor = typeFunctionClass.getDeclaredConstructor()
8✔
150
      typeFunctionConstructor.newInstance()
8✔
151
    } catch {
152
      case e: InstantiationException =>
153
        throw new IllegalArgumentException(s"TypingFunction for ${method.getName} cannot be abstract class.", e)
×
154
      case e: InvocationTargetException =>
155
        throw new IllegalArgumentException(s"TypingFunction's constructor for ${method.getName} failed.", e)
×
156
      case e: NoSuchMethodException =>
157
        throw new IllegalArgumentException(s"Could not find parameterless constructor for method ${method.getName} or its TypingFunction was declared inside non-static class.", e)
×
158
      case e: Exception =>
159
        throw new IllegalArgumentException(s"Could not extract information about generic method ${method.getName}.", e)
×
160
    }
161
  }
162

163
  private def extractGenericMethod(method: Method, genericType: GenericType)
164
                                  (implicit settings: ClassExtractionSettings): List[(String, MethodInfo)] = {
165
    val typeFunctionInstance = getTypeFunctionInstanceFromAnnotation(method, genericType)
8✔
166

167
    val methodTypeInfo = extractGenericParameters(typeFunctionInstance, method)
8✔
168

169
    collectMethodNames(method).map(methodName => methodName -> FunctionalMethodInfo(
8✔
170
      x => typeFunctionInstance.computeResultType(x),
4✔
171
      methodTypeInfo,
172
      methodName,
173
      extractNussknackerDocs(method)
8✔
174
    ))
175
  }
176

177
  private def extractRegularMethod(method: Method)
178
                                  (implicit settings: ClassExtractionSettings): List[(String, StaticMethodInfo)] =
179
    collectMethodNames(method).map(methodName => methodName -> StaticMethodInfo(
34✔
180
      extractMethodTypeInfo(method),
34✔
181
      methodName,
182
      extractNussknackerDocs(method)
34✔
183
    ))
184

185
  private def extractPublicFields(clazz: Class[_], membersPredicate: VisibleMembersPredicate, staticMethodsAndFields: Boolean)
186
                                 (implicit settings: ClassExtractionSettings): Map[String, StaticMethodInfo] = {
187
    val interestingFields = clazz.getFields.filter(membersPredicate.shouldBeVisible)
34✔
188
    val fields =
189
      if(staticMethodsAndFields) interestingFields.filter(m => Modifier.isStatic(m.getModifiers))
34✔
190
      else interestingFields.filter(m => !Modifier.isStatic(m.getModifiers))
34✔
191
    fields.map { field =>
34✔
192
      field.getName -> StaticMethodInfo(
34✔
193
        MethodTypeInfo(Nil, None, extractFieldReturnType(field)),
34✔
194
        field.getName,
34✔
195
        extractNussknackerDocs(field)
34✔
196
      )
197
    }.toMap
34✔
198
  }
199

200
  private def extractNussknackerDocs(accessibleObject: AccessibleObject): Option[String] = {
201
    extractAnnotation(accessibleObject, classOf[Documentation]).map(_.description())
34✔
202
  }
203

204
  private def extractGenericParameters(typingFunction: TypingFunction, method: Method): NonEmptyList[MethodTypeInfo] = {
205
    val autoExtractedParameters = extractMethodTypeInfo(method)
8✔
206
    val definedParametersOption = typingFunction.signatures.toList.flatMap(_.toList)
7✔
207

208
    definedParametersOption
209
      .map(MethodTypeInfoSubclassChecker.check(_, autoExtractedParameters))
7✔
210
      .collect{ case Invalid(e) => e }
5✔
211
      .foreach { x =>
8✔
212
        val errorString = x.map(_.message).toList.mkString("; ")
2✔
213
        throw new IllegalArgumentException(s"Generic function ${method.getName} has declared parameters that are incompatible with methods signature: $errorString")
2✔
214
      }
215

216
    NonEmptyList.fromList(definedParametersOption).getOrElse(NonEmptyList.one(autoExtractedParameters))
8✔
217
  }
218

219
  private def extractMethodTypeInfo(method: Method): MethodTypeInfo = {
220
    MethodTypeInfo.fromList(for {
34✔
221
      param <- method.getParameters.toList
34✔
222
      annotationOption = extractAnnotation(param, classOf[ParamName])
34✔
223
      name = annotationOption.map(_.value).getOrElse(param.getName)
34✔
224
      paramType = extractParameterType(param)
34✔
225
    } yield Parameter(name, paramType), method.isVarArgs, extractMethodReturnType(method))
34✔
226
  }
227

228
  def extractParameterType(javaParam: java.lang.reflect.Parameter): TypingResult = {
229
    extractClass(javaParam.getParameterizedType).getOrElse(Typed(javaParam.getType))
34✔
230
  }
231

232
  private def extractFieldReturnType(field: Field): TypingResult = {
233
    extractGenericReturnType(field.getGenericType).orElse(extractClass(field.getGenericType)).getOrElse(Typed(field.getType))
17✔
234
  }
235

236
  def extractMethodReturnType(method: Method): TypingResult = {
237
    extractGenericReturnType(method.getGenericReturnType).orElse(extractClass(method.getGenericReturnType)).getOrElse(Typed(method.getReturnType))
34✔
238
  }
239

240
  private def extractGenericReturnType(typ: Type): Option[TypingResult] = {
241
    typ match {
242
      case t: ParameterizedType if t.getRawType.isInstanceOf[Class[_]] => extractGenericMonadReturnType(t, t.getRawType.asInstanceOf[Class[_]])
34✔
243
      case _ => None
34✔
244
    }
245
  }
246

247
  // This method should be used only for method's and field's return type - for method's parameters such unwrapping has no sense
248
  //
249
  // Arguments of generic types that are Scala's primitive types are always erased by Scala compiler to java.lang.Object:
250
  // * issue: https://github.com/scala/bug/issues/4214 (and discussion at https://groups.google.com/g/scala-internals/c/K2dELqajQbg/m/gV0tbjRHJ4UJ)
251
  // * commit: https://github.com/scala/scala/commit/e42733e9fe1f3af591976fbb48b66035253d85b9
252
  private def extractGenericMonadReturnType(genericReturnType: ParameterizedType, genericReturnRawType: Class[_]): Option[TypingResult] = {
253
    // see ScalaLazyPropertyAccessor
254
    if (classOf[StateT[IO, _, _]].isAssignableFrom(genericReturnRawType)) {
×
255
      val returnType = genericReturnType.getActualTypeArguments.apply(3) // it's IndexedStateT[IO, ContextWithLazyValuesProvider, ContextWithLazyValuesProvider, A]
×
256
      extractClass(returnType)
×
257
    }
258
    // see ScalaOptionOrNullPropertyAccessor
259
    else if (classOf[Option[_]].isAssignableFrom(genericReturnRawType)) {
34✔
260
      val optionGenericType = genericReturnType.getActualTypeArguments.apply(0)
34✔
261
      extractClass(optionGenericType)
34✔
262
    }
263
    // see JavaOptionalOrNullPropertyAccessor
264
    else if (classOf[Optional[_]].isAssignableFrom(genericReturnRawType)) {
34✔
265
      val optionalGenericType = genericReturnType.getActualTypeArguments.apply(0)
8✔
266
      extractClass(optionalGenericType)
8✔
267
    }
268
    else None
34✔
269
  }
270

271
  //TODO this is not correct for primitives and complicated hierarchies, but should work in most cases
272
  //http://docs.oracle.com/javase/8/docs/api/java/lang/reflect/ParameterizedType.html#getActualTypeArguments--
273
  private def extractClass(typ: Type): Option[TypingResult] = {
274
    typ match {
275
      case t: Class[_] => Some(Typed(t))
34✔
276
      case t: ParameterizedType if t.getRawType.isInstanceOf[Class[_]] => Some(extractGenericParams(t, t.getRawType.asInstanceOf[Class[_]]))
34✔
277
      case _ => None
34✔
278
    }
279
  }
280

281
  private def extractGenericParams(paramsType: ParameterizedType, paramsRawType: Class[_]): TypingResult = {
282
    Typed.genericTypeClass(paramsRawType, paramsType.getActualTypeArguments.toList.map(p => extractClass(p).getOrElse(Unknown)))
34✔
283
  }
284

285
  private def extractScalaVersionOfVarArgMethod(method: Method): Option[Method] = {
286
    val obj = method.getDeclaringClass
34✔
287
    val name = method.getName
34✔
288
    val args = method.getParameterTypes.toList
34✔
289
    args match {
290
      case noVarArgs :+ varArg if method.isVarArgs && varArg.isArray =>
34✔
291
        try {
34✔
292
          Some(obj.getMethod(name, noVarArgs :+ classOf[Seq[_]]: _*))
34✔
293
        } catch {
294
          case _: NoSuchMethodException => None
34✔
295
        }
296
      case _ => None
34✔
297
    }
298
  }
299

300
  private def extractJavaVersionOfVarArgMethod(method: Method): Option[Method] = {
301
    method.getDeclaringClass.getMethods.find(m => m.isVarArgs && (m.getParameterTypes.toList match {
34✔
302
      case noVarArgs :+ varArgArr if varArgArr.isArray =>
34✔
303
        method.getParameterTypes.toList == noVarArgs :+ classOf[Seq[_]]
34✔
304
      case _ => false
×
305
    }))
306
  }
307

308
  // "varargs" annotation creates new function that has java style varArgs
309
  // but it disregards annotations, so we have to look for original function
310
  // to extract them.
311
  private def extractAnnotation[T <: Annotation](obj: AnnotatedElement, annotationType: Class[T]): Option[T] =
312
    Option(obj.getAnnotation(annotationType)).orElse(obj match {
34✔
313
      case method: Method => extractScalaVersionOfVarArgMethod(method).flatMap(extractAnnotation(_, annotationType))
20✔
314
      // TODO: Add new case for parameters.
315
      case _ => None
34✔
316
    })
317

318
  def companionObject[T](klazz: Class[T]): T = {
319
    klazz.getField("MODULE$").get(null).asInstanceOf[T]
2✔
320
  }
321

322
}
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