Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,68 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
}

private fun checkRecord(record: RecordType) {
record.fields.forEach { checkModelType(it.type) }
record.fields.forEach {
checkModelType(it.type)
checkCycle(record, it)
}
}

private fun checkCycle(rootRecord: RecordType, rootField: RecordType.Field) {
fun impl(field: RecordType.Field, visited: List<Type>, encounteredOptional: Boolean = false) {
val typeReference = (field.type as? ResolvedTypeReference) ?: return
val type = typeReference.type
val record: RecordType
val newVisited: List<Type>
var isOptional = encounteredOptional || typeReference.isOptional
when (type) {
is RecordType -> {
record = type
newVisited = visited + record
}
is AliasType -> {
val reference = type.fullyResolvedType ?: return
val actualType = reference.type
if (actualType !is RecordType) {
return
}
record = actualType
newVisited = visited + listOf(type, actualType)
isOptional = isOptional || reference.isOptional
}
else -> return
}

if (record == rootRecord) {
val declaration = rootField.declaration
val path = newVisited.joinToString(" ► ") {
buildString {
append(it.humanReadableName)
if (it is AliasType) {
append(" (typealias)")
}
}
}
if (isOptional) {
declaration.reportWarning(controller) {
message("Record fields should not be cyclical, because they might not be serializable")
highlight("cycle: $path", declaration.location)
}
} else {
declaration.reportError(controller) {
message("Required record fields must not be cyclical, because they cannot be serialized")
highlight("illegal cycle: $path", declaration.location)
}
}
return
}
if (record in visited) {
// we ran into a cycle from a different record
return
}
record.fields.forEach { impl(it, newVisited, isOptional) }
}

impl(rootField, listOf(rootRecord))
}

private fun checkService(service: ServiceType) {
Expand Down
87 changes: 87 additions & 0 deletions semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,93 @@ class SemanticModelTest {
service to emptyList(),
)
}

@Test
fun `cannot have cyclic records`() {
val source = """
package cycles

record Recursive {
recursive: Recursive
}

record IndirectA {
b: IndirectB
}

record IndirectB {
a: IndirectA
}

record ReferencesAll {
r: Recursive
a: IndirectA
b: IndirectB
}
""".trimIndent()
parseAndCheck(
source to List(3) { "Error: Required record fields must not be cyclical, because they cannot be serialized" }
)
}

@Test
fun `cannot have cyclic records with typealiases`() {
val source = """
package cycles

record A {
b: B
}

record B {
c: C
}

typealias C = A
""".trimIndent()
parseAndCheck(
source to List(2) { "Error: Required record fields must not be cyclical, because they cannot be serialized" }
)
}

@Test
fun `can have List or Map of same type`() {
val source = """
package cycles

record A {
children: List<A>
childrenByName: Map<String, A>
}
""".trimIndent()
parseAndCheck(
source to emptyList()
)
}

@Test
fun `cycle with optional type is warning`() {
val source = """
package cycles

record A {
b: B?
}

record B {
a: A
}

record Recursive {
recursive: R
}

typealias R = Recursive?
""".trimIndent()
parseAndCheck(
source to List(3) { "Warning: Record fields should not be cyclical, because they might not be serializable" }
)
}
}

@Nested
Expand Down