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

carueda / tscfg / 10531771158

23 Aug 2024 08:12PM UTC coverage: 96.633% (-0.3%) from 96.973%
10531771158

push

carueda
drop support for scala 2.12 and java 7

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

3 existing lines in 3 files now uncovered.

861 of 891 relevant lines covered (96.63%)

1.84 hits per line

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

93.94
/src/main/scala/tscfg/Struct.scala
1
package tscfg
2

3
import com.typesafe.config.{Config, ConfigValueType}
4
import tscfg.DefineCase._
5
import tscfg.exceptions.ObjectDefinitionException
6
import tscfg.ns.Namespace
7

8
import scala.annotation.tailrec
9
import scala.collection.{Map, mutable}
10

11
/** Supports a convenient next representation based on given TS Config object.
12
  * It supports nested member definitions utilizing the 'members' field
13
  *
14
  * @param name
15
  *   Name of the config member
16
  * @param members
17
  *   Nested config definitions
18
  * @param tsStringValue
19
  *   Captures string value to support determining dependencies in terms of RHS
20
  *   names (that is, when such a string may be referring to a @define)
21
  */
22
case class Struct(
23
    name: String,
24
    members: mutable.HashMap[String, Struct] = mutable.HashMap.empty,
25
    tsStringValue: Option[String] = None
26
) {
27

28
  // Non-None when this is a `@define`
29
  var defineCaseOpt: Option[DefineCase] = None
2✔
30

31
  def isDefine: Boolean = defineCaseOpt.isDefined
2✔
32

33
  def isExtends: Boolean = defineCaseOpt match {
2✔
34
    case Some(_: ExtendsDefineCase)    => true
2✔
35
    case Some(_: ImplementsDefineCase) => true
2✔
36
    case _                             => false
2✔
37
  }
38

39
  def isEnum: Boolean = defineCaseOpt.exists(_.isEnum)
2✔
40

41
  def isLeaf: Boolean = members.isEmpty
2✔
42

43
  def dependencies: Set[String] = {
44
    tsStringValue.toSet ++ members.values.flatMap(_.dependencies)
2✔
45
  }
46

47
  // $COVERAGE-OFF$
48
  def format(indent: String = ""): String = {
49
    val defineStr = defineCaseOpt.map(dc => s" $dc").getOrElse("")
50
    val nameStr   = s"${if (name.isEmpty) "(root)" else name}$defineStr"
51

52
    val dependenciesStr = dependencies.toList match {
53
      case Nil => ""
54
      case l   => s" [dependencies=${l.mkString(", ")}]"
55
    }
56

57
    val nameHeading = nameStr + dependenciesStr
58

59
    if (members.isEmpty) {
60
      nameHeading
61
    }
62
    else {
63
      val indent2 = indent + "    "
64
      s"$nameHeading ->\n" + indent2 + {
65
        members
66
          .map(e => s"${e._1}: " + e._2.format(indent2))
67
          .mkString("\n" + indent2)
68
      }
69
    }
70
  }
71
  // $COVERAGE-ON$
72
}
73

74
object Struct {
75
  import scala.jdk.CollectionConverters._
76

77
  /** Gets all structs from the given TS Config object, sorted appropriately for
78
    * subsequent processing in ModelBuilder. Any circular reference will throw a
79
    * [[ObjectDefinitionException]].
80
    */
81
  def getMemberStructs(namespace: Namespace, conf: Config): List[Struct] = {
82
    val struct: Struct              = getStruct(conf)
2✔
83
    val memberStructs: List[Struct] = struct.members.values.toList
2✔
84

85
    // set any define to each struct:
86
    memberStructs.flatMap { setDefineCase(conf, _) }
2✔
87

88
    val (defineStructs, nonDefineStructs) = memberStructs.partition(_.isDefine)
2✔
89
    val sortedDefineStructs               = sortDefineStructs(defineStructs)
2✔
90

91
    val sortedStructs = {
92
      // but also sort the "defines" by any name (member type) dependencies:
93
      val definesSortedByNameDependencies = sortByNameDependencies(
2✔
94
        sortedDefineStructs
95
      )
96
      definesSortedByNameDependencies ++ nonDefineStructs
2✔
97
    }
98

99
    if (namespace.isRoot) {
2✔
100
      scribe.debug(
2✔
101
        s"root\n" +
UNCOV
102
          s"struct=${struct.format()}\n" +
×
103
          s"sortedStructs=\n  ${sortedStructs.map(_.format()).mkString("\n  ")}"
104
      )
105
    }
106

107
    sortedStructs
108
  }
109

110
  private def sortDefineStructs(defineStructs: List[Struct]): List[Struct] = {
111
    val sorted = mutable.LinkedHashMap.empty[String, Struct]
2✔
112

113
    // / `othersBeingAdded` allows to check for circularity
114
    def addExtendStruct(
115
        s: Struct,
116
        othersBeingAdded: List[Struct] = List.empty
117
    ): Unit = {
118
      def addExtendsOrImplements(name: String, isExternal: Boolean): Unit = {
119
        sorted.get(name) match {
2✔
120
          case Some(_) =>
121
            // ancestor already added. Add this descendent struct:
122
            sorted.put(s.name, s)
2✔
123

124
          case None =>
125
            // ancestor not added yet. Find it in base list:
126
            defineStructs.find(_.name == name) match {
2✔
127
              case Some(ancestor) =>
2✔
128
                // check for any circularity:
129
                if (othersBeingAdded.exists(_.name == ancestor.name)) {
2✔
130
                  val path = s :: othersBeingAdded
1✔
131
                  val via =
132
                    path.reverseIterator
133
                      .map("'" + _.name + "'")
134
                      .mkString(" -> ")
1✔
135
                  throw ObjectDefinitionException(
1✔
136
                    s"extension of struct '${s.name}' involves circular reference via $via"
137
                  )
138
                }
139

140
                // ok, add ancestor:
141
                addExtendStruct(ancestor, s :: othersBeingAdded)
2✔
142
                // and then add this struct:
143
                sorted.put(s.name, s)
2✔
144

145
              case None if isExternal =>
146
                sorted.put(s.name, s)
2✔
147

148
              case None =>
149
                throw ObjectDefinitionException(
×
150
                  s"struct '${s.name}' with undefined extend '$name'"
151
                )
152
            }
153
        }
154
      }
155

156
      s.defineCaseOpt.get match {
2✔
157
        case SimpleDefineCase | AbstractDefineCase | EnumDefineCase =>
158
          sorted.put(s.name, s)
2✔
159

160
        case c: ExtendsDefineCase =>
161
          addExtendsOrImplements(c.name, c.isExternal)
2✔
162

163
        case c: ImplementsDefineCase =>
164
          addExtendsOrImplements(c.name, c.isExternal)
2✔
165
      }
166
    }
167

168
    defineStructs.foreach(addExtendStruct(_))
2✔
169

170
    assert(
2✔
171
      defineStructs.size == sorted.size,
2✔
172
      s"defineStructs.size=${defineStructs.size} != sorted.size=${sorted.size}"
173
    )
174

175
    sorted.toList.map(_._2)
2✔
176
  }
177

178
  private def sortByNameDependencies(structs: List[Struct]): List[Struct] = {
179
    structs.sortWith((a, b) => {
2✔
180
      val aDeps = a.dependencies
2✔
181
      val bDeps = b.dependencies
2✔
182

183
      if (aDeps.contains(b.name)) {
2✔
184
        // a depends on b, so b should come first:
185
        false
×
186
      }
187
      else if (bDeps.contains(a.name)) {
2✔
188
        // b depends on a, so a should come first:
189
        true
2✔
190
      }
191
      else {
192
        // no dependency, so sort by name:
193
        a.name < b.name
2✔
194
      }
195
    })
196
  }
197

198
  /** Determines the joint set of all ancestor's members to allow proper
199
    * overriding in child structs.
200
    *
201
    * Note that not-circularity is verified prior to calling this function.
202
    *
203
    * @param struct
204
    *   Current (child) struct to consider
205
    * @param memberStructs
206
    *   List to find referenced structs
207
    * @param namespace
208
    *   Current known name space
209
    * @return
210
    *   Mapping from symbol to type definition if struct is an ExtendsDefineCase
211
    */
212
  def ancestorClassMembers(
213
      struct: Struct,
214
      memberStructs: List[Struct],
215
      namespace: Namespace
216
  ): Option[Map[String, model.AnnType]] = {
217

218
    def handleExtends(
219
        parentName: String,
220
        isExternal: Boolean
221
    ): Option[Map[String, model.AnnType]] = {
222
      val defineStructs = memberStructs.filter(_.isDefine)
2✔
223

224
      val greatAncestorMembers =
225
        defineStructs.find(_.name == parentName) match {
2✔
226
          case Some(parentStruct) if parentStruct.isExtends =>
2✔
227
            ancestorClassMembers(parentStruct, memberStructs, namespace)
2✔
228

229
          case Some(_) => None
2✔
230

231
          case None if isExternal => None
2✔
232

233
          case None =>
234
            throw new RuntimeException(
×
235
              s"struct '${struct.name}' with undefined extend '$parentName'"
236
            )
237
        }
238

239
      val parentMembers =
240
        namespace.getRealDefine(parentName).map(_.members) match {
2✔
241
          case Some(parentMembers) => parentMembers
2✔
242

243
          case None if isExternal => None
2✔
244

245
          case None =>
246
            throw new IllegalArgumentException(
×
247
              s"@define '${struct.name}' is invalid because '$parentName' is not @defined"
248
            )
249
        }
250

251
      // join both member maps
252
      Some(greatAncestorMembers.getOrElse(Map.empty) ++ parentMembers)
2✔
253
    }
254

255
    struct.defineCaseOpt.flatMap {
2✔
256
      case c: ExtendsDefineCase =>
257
        handleExtends(c.name, c.isExternal)
2✔
258

259
      case c: ImplementsDefineCase =>
260
        // note: handling it as an extends (todo: revisit this at some point)
261
        handleExtends(c.name, c.isExternal)
2✔
262

263
      case _ => None
2✔
264
    }
265
  }
266

267
  private def getStruct(conf: Config): Struct = {
268
    val structs = mutable.HashMap[String, Struct]("" -> Struct(""))
2✔
269

270
    def resolve(key: String): Struct = {
271
      if (!structs.contains(key)) structs.put(key, Struct(getSimple(key)))
2✔
272
      structs(key)
2✔
273
    }
274

275
    // Due to TS Config API, we traverse from the leaves to the ancestors:
276
    conf.entrySet().asScala foreach { e =>
2✔
277
      val path        = e.getKey
2✔
278
      val configValue = e.getValue
2✔
279

280
      // capture string value to determine possible "define" dependency
281
      val tsStringValue: Option[String] = e.getValue.valueType() match {
2✔
282
        case ConfigValueType.STRING => Some(configValue.unwrapped().toString)
2✔
283
        case _                      => None
2✔
284
      }
285
      scribe.debug(s"getStruct: path=$path, tsStringValue=$tsStringValue")
2✔
286
      val leaf = Struct(path, tsStringValue = tsStringValue)
2✔
287

288
      doAncestorsOf(path, leaf)
2✔
289

290
      def doAncestorsOf(childKey: String, childStruct: Struct): Unit = {
291
        val (parent, simple) = (getParent(childKey), getSimple(childKey))
2✔
292
        createParent(parent, simple, childStruct)
2✔
293

294
        @tailrec
295
        def createParent(
296
            parentKey: String,
297
            simple: String,
298
            child: Struct
299
        ): Unit = {
300
          val parentGroup = resolve(parentKey)
2✔
301
          parentGroup.members.put(simple, child)
2✔
302
          if (parentKey != "") {
2✔
303
            createParent(
2✔
304
              getParent(parentKey),
2✔
305
              getSimple(parentKey),
3✔
306
              parentGroup
307
            )
308
          }
309
        }
310
      }
3✔
311
    }
2✔
312

313
    def getParent(path: String): String = {
314
      val idx = path.lastIndexOf('.')
2✔
315
      if (idx >= 0) path.substring(0, idx) else ""
3✔
316
    }
317

318
    def getSimple(path: String): String = {
319
      val idx = path.lastIndexOf('.')
2✔
320
      if (idx >= 0) path.substring(idx + 1) else path
2✔
321
    }
322

323
    structs("")
2✔
324
  }
325

326
  private def setDefineCase(conf: Config, s: Struct): Option[DefineCase] = {
327
    val cv          = conf.getValue(s.name)
2✔
328
    val comments    = cv.origin().comments().asScala.toList
2✔
329
    val defineLines = comments.map(_.trim).filter(_.startsWith("@define"))
2✔
330
    s.defineCaseOpt = defineLines.length match {
2✔
331
      case 0 => None
2✔
332
      case 1 => DefineCase.getDefineCase(defineLines.head)
2✔
333
      case _ =>
334
        throw new IllegalArgumentException(
×
335
          s"multiple @define lines for ${s.name}."
336
        )
337
    }
338
    s.defineCaseOpt
2✔
339
  }
340
}
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