diff --git a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt index 068930e6..10ff95c5 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt @@ -61,11 +61,9 @@ internal fun dump(command: DumpCommand, terminal: Terminal, controller: Diagnost } // build up the semantic model from the AST - SemanticModelBuilder.build(fileNodes, controller) + val samtPackage = SemanticModelBuilder.build(fileNodes, controller) if (dumpAll || command.dumpTypes) { - terminal.println("Types:") - terminal.println("Not yet implemented") - // Type dumper will be added here + terminal.println(TypePrinter.dump(samtPackage)) } } diff --git a/cli/src/main/kotlin/tools/samt/cli/TypePrinter.kt b/cli/src/main/kotlin/tools/samt/cli/TypePrinter.kt new file mode 100644 index 00000000..6c45402a --- /dev/null +++ b/cli/src/main/kotlin/tools/samt/cli/TypePrinter.kt @@ -0,0 +1,56 @@ +package tools.samt.cli + +import com.github.ajalt.mordant.rendering.TextColors.* +import com.github.ajalt.mordant.rendering.TextStyles.bold +import tools.samt.semantic.Package + +internal object TypePrinter { + fun dump(samtPackage: Package): String = buildString { + appendLine(blue(samtPackage.name.ifEmpty { "" })) + for (enum in samtPackage.enums) { + appendLine(" ${bold("enum")} ${yellow(enum.name)}") + } + for (record in samtPackage.records) { + appendLine(" ${bold("record")} ${yellow(record.name)}") + } + for (alias in samtPackage.aliases) { + appendLine(" ${bold("alias")} ${yellow(alias.name)} = ${gray(alias.fullyResolvedType?.humanReadableName ?: "Unknown")}") + } + for (service in samtPackage.services) { + appendLine(" ${bold("service")} ${yellow(service.name)}") + } + for (provider in samtPackage.providers) { + appendLine(" ${bold("provider")} ${yellow(provider.name)}") + } + for (consumer in samtPackage.consumers) { + appendLine(" ${bold("consumer")} for ${yellow(consumer.provider.humanReadableName)}") + } + + val childDumps: List = samtPackage.subPackages.map { dump(it) } + + childDumps.forEachIndexed { childIndex, child -> + var firstLine = true + child.lineSequence().forEach { line -> + if (line.isNotEmpty()) { + if (childIndex != childDumps.lastIndex) { + if (firstLine) { + append("${white("├─")}$line") + } else { + append("${white("│ ")}$line") + } + } else { + if (firstLine) { + append("${white("└─")}$line") + } else { + append(" $line") + } + } + + appendLine() + } + + firstLine = false + } + } + } +} diff --git a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt index 99bcab8e..7bcd173b 100644 --- a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt @@ -26,10 +26,21 @@ class ASTPrinterTest { enum E { A, B, C } + alias B : E + @Description("This is a service") service MyService { testmethod(foo: A): B } + + provide MyEndpoint { + implements MyService + transport HTTP + } + + consume MyEndpoint { + uses MyService + } """.trimIndent()) val dump = ASTPrinter.dump(fileNode) @@ -80,19 +91,36 @@ class ASTPrinterTest { │ ├─IdentifierNode A <11:10> │ ├─IdentifierNode B <11:13> │ └─IdentifierNode C <11:16> - └─ServiceDeclarationNode <14:1> - ├─IdentifierNode MyService <14:9> - ├─RequestResponseOperationNode <15:3> - │ ├─IdentifierNode testmethod <15:3> - │ ├─OperationParameterNode <15:14> - │ │ ├─IdentifierNode foo <15:14> - │ │ └─BundleIdentifierNode A <15:19> - │ │ └─IdentifierNode A <15:19> - │ └─BundleIdentifierNode B <15:23> - │ └─IdentifierNode B <15:23> - └─AnnotationNode <13:1> - ├─IdentifierNode Description <13:2> - └─StringNode "This is a service" <13:14> + ├─TypeAliasNode <13:1> + │ ├─IdentifierNode B <13:7> + │ └─BundleIdentifierNode E <13:11> + │ └─IdentifierNode E <13:11> + ├─ServiceDeclarationNode <16:1> + │ ├─IdentifierNode MyService <16:9> + │ ├─RequestResponseOperationNode <17:3> + │ │ ├─IdentifierNode testmethod <17:3> + │ │ ├─OperationParameterNode <17:14> + │ │ │ ├─IdentifierNode foo <17:14> + │ │ │ └─BundleIdentifierNode A <17:19> + │ │ │ └─IdentifierNode A <17:19> + │ │ └─BundleIdentifierNode B <17:23> + │ │ └─IdentifierNode B <17:23> + │ └─AnnotationNode <15:1> + │ ├─IdentifierNode Description <15:2> + │ └─StringNode "This is a service" <15:14> + ├─ProviderDeclarationNode <20:1> + │ ├─IdentifierNode MyEndpoint <20:9> + │ ├─ProviderImplementsNode <21:3> + │ │ └─BundleIdentifierNode MyService <21:14> + │ │ └─IdentifierNode MyService <21:14> + │ └─ProviderTransportNode <22:3> + │ └─IdentifierNode HTTP <22:13> + └─ConsumerDeclarationNode <25:1> + ├─BundleIdentifierNode MyEndpoint <25:9> + │ └─IdentifierNode MyEndpoint <25:9> + └─ConsumerUsesNode <26:3> + └─BundleIdentifierNode MyService <26:8> + └─IdentifierNode MyService <26:8> """.trimIndent().trim(), dumpWithoutColorCodes.trimIndent().trim()) } diff --git a/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt new file mode 100644 index 00000000..56dd9560 --- /dev/null +++ b/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt @@ -0,0 +1,79 @@ +package tools.samt.cli + +import tools.samt.common.DiagnosticController +import tools.samt.common.SourceFile +import tools.samt.lexer.Lexer +import tools.samt.parser.FileNode +import tools.samt.parser.Parser +import tools.samt.semantic.SemanticModelBuilder +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class TypePrinterTest { + @Test + fun `correctly formats an AST dump`() { + val stuffPackage = parse(""" + package test.stuff + + record A { + name: String (10..20, pattern("hehe")) + age: Int (0..150) + } + + enum E { A, B, C } + + @Description("This is a service") + service MyService { + testmethod(foo: A): E + } + + provide MyEndpoint { + implements MyService + transport HTTP + } + """.trimIndent()) + val consumerPackage = parse(""" + package test.other.company + + record Empty + + consume test.stuff.MyEndpoint { + uses test.stuff.MyService + } + """.trimIndent()) + + val controller = DiagnosticController(URI("file:///tmp")) + val samtPackage = SemanticModelBuilder.build(listOf(stuffPackage, consumerPackage), controller) + assertFalse(controller.hasErrors()) + + val dump = TypePrinter.dump(samtPackage) + val dumpWithoutColorCodes = dump.replace(Regex("\u001B\\[[;\\d]*m"), "") + + assertEquals(""" + + └─test + ├─stuff + │ enum E + │ record A + │ service MyService + │ provider MyEndpoint + └─other + └─company + record Empty + consumer for MyEndpoint + """.trimIndent().trim(), dumpWithoutColorCodes.trimIndent().trim()) + } + + private fun parse(source: String): FileNode { + val filePath = URI("file:///tmp/ASTPrinterTest.samt") + val sourceFile = SourceFile(filePath, source) + val diagnosticController = DiagnosticController(URI("file:///tmp")) + val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile) + val stream = Lexer.scan(source.reader(), diagnosticContext) + val fileTree = Parser.parse(sourceFile, stream, diagnosticContext) + assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") + return fileTree + } +} diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt index bb694db4..77de07ab 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt @@ -45,6 +45,11 @@ class SamtDeclarationLookup private constructor() : SamtSemanticLookup protected constructor() { markTypeReference(type.valueType) } + is AliasType, is ConsumerType, is EnumType, is ProviderType, @@ -61,12 +62,17 @@ abstract class SamtSemanticLookup protected constructor() { is EnumDeclarationNode -> markEnumDeclaration(samtPackage.getTypeByNode(statement)) is RecordDeclarationNode -> markRecordDeclaration(samtPackage.getTypeByNode(statement)) is ServiceDeclarationNode -> markServiceDeclaration(samtPackage.getTypeByNode(statement)) - is TypeAliasNode -> Unit + is TypeAliasNode -> markTypeAliasDeclaration(samtPackage.getTypeByNode(statement)) is PackageDeclarationNode -> markPackageDeclaration(statement) is ImportNode -> markImport(statement,samtPackage.typeByNode[statement] ?: UnknownType) } } + protected open fun markTypeAliasDeclaration(aliasType: AliasType) { + markAnnotations(aliasType.declaration.annotations) + markTypeReference(aliasType.aliasedType) + } + protected open fun markServiceDeclaration(serviceType: ServiceType) { markAnnotations(serviceType.declaration.annotations) for (operation in serviceType.operations) { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt index 46add33a..f7c8f3c5 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt @@ -28,6 +28,7 @@ class SamtSemanticTokens private constructor() : SamtSemanticLookup this[location] = Metadata(TokenType.type) is ProviderType -> this[location] = Metadata(TokenType.type) is RecordType -> this[location] = Metadata(TokenType.`class`) is ServiceType -> this[location] = Metadata(TokenType.`interface`) diff --git a/semantic/src/main/kotlin/tools/samt/semantic/Package.kt b/semantic/src/main/kotlin/tools/samt/semantic/Package.kt index fa8e26e5..ce37db5f 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/Package.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/Package.kt @@ -7,11 +7,11 @@ class Package(val name: String) { val records: MutableList = mutableListOf() - @Suppress("MemberVisibilityCanBePrivate") - val enums: MutableList = mutableListOf() // Will be read in the future + val enums: MutableList = mutableListOf() val services: MutableList = mutableListOf() val providers: MutableList = mutableListOf() val consumers: MutableList = mutableListOf() + val aliases: MutableList = mutableListOf() val typeByNode: MutableMap = mutableMapOf() @@ -73,6 +73,12 @@ class Package(val name: String) { typeByNode[consumer.declaration] = consumer } + operator fun plusAssign(alias: AliasType) { + aliases.add(alias) + types[alias.name] = alias + typeByNode[alias.declaration] = alias + } + operator fun contains(identifier: IdentifierNode): Boolean = types.containsKey(identifier.name) diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt index ba08c2bb..05490178 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt @@ -26,12 +26,111 @@ class SemanticModelBuilder private constructor( val fileScopeBySource = files.associate { it.sourceFile to createFileScope(it) } resolveTypes(fileScopeBySource) + resolveAliases() postProcessor.process(global) return global } + private fun resolveAliases() { + val workingSet = global.allSubPackages.flatMap { it.aliases }.toMutableSet() + + do { + var didRemove = false + for (alias in workingSet.toList()) { + if (alias.fullyResolvedType != null) continue + val fullyResolvedType = getFullyResolvedType(alias.aliasedType) + if (fullyResolvedType != null) { + alias.fullyResolvedType = fullyResolvedType + workingSet.remove(alias) + didRemove = true + } + } + } while (didRemove) + + for (unresolvableAlias in workingSet) { + controller.getOrCreateContext(unresolvableAlias.declaration.location.source).error { + message("Could not resolve alias '${unresolvableAlias.name}', are there circular references?") + highlight("unresolved alias", unresolvableAlias.declaration.name.location) + } + } + } + + private fun getFullyResolvedType(typeReference: TypeReference): ResolvedTypeReference? { + check(typeReference is ResolvedTypeReference) + fun merge(base: ResolvedTypeReference, inner: ResolvedTypeReference): ResolvedTypeReference { + if (base.isOptional && inner.isOptional) { + controller.getOrCreateContext(base.fullNode.location.source).warn { + message("Type is already optional, ignoring '?'") + highlight("duplicate optional", base.fullNode.location) + highlight("declared optional here", inner.fullNode.location) + } + } + val overlappingConstraints = base.constraints.filter { baseConstraint -> inner.constraints.any { innerConstraint -> baseConstraint::class == innerConstraint::class } } + for (overlappingConstraint in overlappingConstraints) { + controller.getOrCreateContext(base.fullNode.location.source).error { + message("Cannot have multiple constraints of the same type") + val baseConstraint = base.constraints.first { it::class == overlappingConstraint::class } + highlight("duplicate constraint", baseConstraint.node.location) + + val innerConstraint = base.constraints.first { it::class == overlappingConstraint::class } + highlight("previously declared here", innerConstraint.node.location) + } + } + val mergedOptional = base.isOptional || inner.isOptional + val mergedConstraints = base.constraints + inner.constraints + return base.copy(type = inner.type, isOptional = mergedOptional, constraints = mergedConstraints) + } + + return when (val type = typeReference.type) { + is LiteralType, + is EnumType, + is RecordType, + UnknownType -> typeReference + is AliasType -> type.fullyResolvedType?.let { merge(typeReference, it) } + is ListType -> { + val elementType = getFullyResolvedType(type.elementType) + if (elementType != null) { + typeReference.copy(type = type.copy(elementType = elementType)) + } else { + null + } + } + is MapType -> { + val keyType = getFullyResolvedType(type.keyType) + val valueType = getFullyResolvedType(type.valueType) + if (keyType != null && valueType != null) { + typeReference.copy(type = type.copy(keyType = keyType, valueType = valueType)) + } else { + null + } + } + is ServiceType -> { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Alias cannot reference service") + highlight("alias", typeReference.typeNode.location) + } + typeReference + } + is ProviderType -> { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Alias cannot reference provider") + highlight("alias", typeReference.typeNode.location) + } + typeReference + } + is PackageType -> { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Alias cannot reference package") + highlight("alias", typeReference.typeNode.location) + } + typeReference + } + is ConsumerType -> error("Consumer type cannot be referenced by name, this should never happen") + } + } + private fun resolveTypes(fileScopeBySource: Map) { fun TypeReference.resolve(): ResolvedTypeReference { check(this is UnresolvedTypeReference) { "Type reference must be unresolved" } @@ -41,6 +140,9 @@ class SemanticModelBuilder private constructor( } for (subPackage in global.allSubPackages) { + for (alias in subPackage.aliases) { + alias.aliasedType = alias.aliasedType.resolve() + } for (record in subPackage.records) { for (field in record.fields) { field.type = field.type.resolve() diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt index 7243c2f7..34516b2b 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt @@ -63,20 +63,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont private inline fun checkServiceType(typeReference: TypeReference, block: (serviceType: ServiceType) -> Unit) { check(typeReference is ResolvedTypeReference) - if (typeReference.constraints.isNotEmpty()) { - controller.getOrCreateContext(typeReference.fullNode.location.source).error { - message("Cannot have constraints on service") - for (constraint in typeReference.constraints) { - highlight("illegal constraint", constraint.node.location) - } - } - } - if (typeReference.isOptional) { - controller.getOrCreateContext(typeReference.fullNode.location.source).error { - message("Cannot have optional service") - highlight("illegal optional", typeReference.fullNode.location) - } - } + checkBlankTypeReference(typeReference, "service") when (val type = typeReference.type) { is ServiceType -> { block(type) @@ -94,20 +81,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont private inline fun checkProviderType(typeReference: TypeReference, block: (providerType: ProviderType) -> Unit) { check(typeReference is ResolvedTypeReference) - if (typeReference.constraints.isNotEmpty()) { - controller.getOrCreateContext(typeReference.fullNode.location.source).error { - message("Cannot have constraints on provider") - for (constraint in typeReference.constraints) { - highlight("illegal constraint", constraint.node.location) - } - } - } - if (typeReference.isOptional) { - controller.getOrCreateContext(typeReference.fullNode.location.source).error { - message("Cannot have optional provider") - highlight("illegal optional", typeReference.fullNode.location) - } - } + checkBlankTypeReference(typeReference, "provider") when (val type = typeReference.type) { is ProviderType -> { block(type) @@ -123,6 +97,28 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont } } + /** A blank type reference has no constraints or optional marker */ + private fun checkBlankTypeReference(typeReference: ResolvedTypeReference, what: String): Boolean { + var isBlank = true + if (typeReference.constraints.isNotEmpty()) { + isBlank = false + controller.getOrCreateContext(typeReference.fullNode.location.source).error { + message("Cannot have constraints on $what") + for (constraint in typeReference.constraints) { + highlight("illegal constraint", constraint.node.location) + } + } + } + if (typeReference.isOptional) { + isBlank = false + controller.getOrCreateContext(typeReference.fullNode.location.source).error { + message("Cannot have optional $what") + highlight("illegal optional", typeReference.fullNode.location) + } + } + return isBlank + } + private fun checkRecord(record: RecordType) { record.fields.forEach { checkModelType(it.type) } } diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPreProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPreProcessor.kt index 068388eb..4cfeb153 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPreProcessor.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPreProcessor.kt @@ -158,10 +158,12 @@ internal class SemanticModelPreProcessor(private val controller: DiagnosticContr } is TypeAliasNode -> { - controller.getOrCreateContext(statement.location.source).error { - message("Type aliases are not yet supported") - highlight("unsupported feature", statement.location) - } + reportDuplicateDeclaration(parentPackage, statement) + parentPackage += AliasType( + name = statement.name.name, + aliasedType = UnresolvedTypeReference(statement.type), + declaration = statement + ) } is PackageDeclarationNode, diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelReferenceResolver.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelReferenceResolver.kt index 6a58ee34..a57e0363 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelReferenceResolver.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelReferenceResolver.kt @@ -78,6 +78,17 @@ internal class SemanticModelReferenceResolver( highlight("illegal nested constraint", expression.location) } } + for (constraintInstances in constraints.groupBy { it::class }.values) { + if (constraintInstances.size > 1) { + controller.getOrCreateContext(expression.location.source).error { + message("Cannot have multiple constraints of the same type") + highlight("first constraint", constraintInstances.first().node.location) + for (duplicateConstraints in constraintInstances.drop(1)) { + highlight("duplicate constraint", duplicateConstraints.node.location) + } + } + } + } return baseType.copy(constraints = constraints, fullNode = expression) } diff --git a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt index d80b9a63..4fea4bad 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt @@ -131,7 +131,18 @@ data class MapType( override val humanReadableName: String = "Map<${keyType.humanReadableName}, ${valueType.humanReadableName}>" } -data class RecordType( +class AliasType( + val name: String, + /** The type this alias stands for, could be another alias */ + var aliasedType: TypeReference, + /** The fully resolved type, will not contain any type aliases anymore, just the underlying merged type */ + var fullyResolvedType: ResolvedTypeReference? = null, + override val declaration: TypeAliasNode, +) : CompoundType, UserDeclared { + override val humanReadableName: String = name +} + +class RecordType( val name: String, val fields: List, override val declaration: RecordDeclarationNode, @@ -145,7 +156,7 @@ data class RecordType( override val humanReadableName: String = name } -data class EnumType( +class EnumType( val name: String, val values: List, override val declaration: EnumDeclarationNode, @@ -153,7 +164,7 @@ data class EnumType( override val humanReadableName: String = name } -data class ServiceType( +class ServiceType( val name: String, val operations: List, override val declaration: ServiceDeclarationNode, @@ -169,7 +180,7 @@ data class ServiceType( ): UserDeclared } - data class RequestResponseOperation( + class RequestResponseOperation( override val name: String, override val parameters: List, override val declaration: RequestResponseOperationNode, @@ -178,7 +189,7 @@ data class ServiceType( val isAsync: Boolean, ) : Operation - data class OnewayOperation( + class OnewayOperation( override val name: String, override val parameters: List, override val declaration: OnewayOperationNode, @@ -187,32 +198,32 @@ data class ServiceType( override val humanReadableName: String = name } -data class ProviderType( +class ProviderType( val name: String, val implements: List, - val transport: Transport, + @Suppress("unused") val transport: Transport, override val declaration: ProviderDeclarationNode, ) : CompoundType, UserDeclared { - data class Implements( + class Implements( var service: TypeReference, var operations: List, val node: ProviderImplementsNode, ) - data class Transport( + class Transport( val name: String, - val configuration: Any?, + @Suppress("unused") val configuration: Any?, ) override val humanReadableName: String = name } -data class ConsumerType( +class ConsumerType( var provider: TypeReference, var uses: List, override val declaration: ConsumerDeclarationNode, ) : CompoundType, UserDeclared { - data class Uses( + class Uses( var service: TypeReference, var operations: List, val node: ConsumerUsesNode, diff --git a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt index 03163d6b..7ff54400 100644 --- a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt +++ b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt @@ -77,12 +77,16 @@ class SemanticModelTest { implements D transport HTTP } + + enum E { } + alias E : Int """.trimIndent() parseAndCheck( source to listOf( "Error: 'A' is already declared", "Error: 'B' is already declared", - "Error: 'C' is already declared" + "Error: 'C' is already declared", + "Error: 'E' is already declared", ) ) } @@ -270,6 +274,24 @@ class SemanticModelTest { ) } + @Test + fun `cannot use same constraint multiple times`() { + val source = """ + package tooManyConstraints + + record Complex { + string: String (pattern("a-z"), pattern("A-Z")) + float: Float (1..100, range(1.5..*)) + } + """.trimIndent() + parseAndCheck( + source to listOf( + "Error: Cannot have multiple constraints of the same type", + "Error: Cannot have multiple constraints of the same type", + ) + ) + } + @Test fun `range must be valid`() { val source = """ @@ -479,28 +501,59 @@ class SemanticModelTest { } @Nested - inner class NotImplementedFeatures { + inner class Aliases { + @Test - fun `cannot use extends keyword`() { + fun `can use type aliases`() { val source = """ package color - record Color extends Int + alias UShort: Int (0..256) + + record Color { + r: UShort + g: UShort + b: UShort + } """.trimIndent() parseAndCheck( - source to listOf("Error: Record extends are not yet supported") + source to emptyList() ) } @Test - fun `cannot use type aliases`() { + fun `cannot use type aliases with cyclic references`() { val source = """ package color - alias Color: Int + alias A : Int + alias B : A // Int + alias C : B // Int + alias D : F // Cycle! + alias E : D // Cycle! + alias F : E // Cycle! """.trimIndent() parseAndCheck( - source to listOf("Error: Type aliases are not yet supported") + source to listOf( + "Error: Could not resolve alias 'D', are there circular references?", + "Error: Could not resolve alias 'E', are there circular references?", + "Error: Could not resolve alias 'F', are there circular references?", + ) + ) + } + } + + @Nested + inner class NotImplementedFeatures { + @Test + fun `cannot use extends keyword`() { + val source = """ + package color + + record Color extends Int + """.trimIndent() + parseAndCheck( + source to listOf("Error: Record extends are not yet supported") ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 48d9739a..e20de388 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,7 +13,7 @@ dependencyResolutionManagement { val kotlin = "1.8.21" val shadow = "8.1.1" val jCommander = "1.82" - val mordant = "2.0.0-beta12" + val mordant = "2.0.0-beta13" val kotlinxSerialization = "1.5.0" val kover = "0.6.1" val lsp4j = "0.20.1" diff --git a/specification/examples/todo-service/todo-provider-file.samt b/specification/examples/todo-service/todo-provider-file.samt index ab0acbbc..74f749ad 100644 --- a/specification/examples/todo-service/todo-provider-file.samt +++ b/specification/examples/todo-service/todo-provider-file.samt @@ -1,5 +1,3 @@ -import tools.samt.examples.todo.TodoManager - package tools.samt.examples.todo provide TodosFileInput { diff --git a/specification/examples/todo-service/todo-provider-http.samt b/specification/examples/todo-service/todo-provider-http.samt index 45ffd0b3..4a3a16fd 100644 --- a/specification/examples/todo-service/todo-provider-http.samt +++ b/specification/examples/todo-service/todo-provider-http.samt @@ -1,5 +1,3 @@ -import tools.samt.examples.todo.TodoManager - package tools.samt.examples.todo provide TodoEndpointHTTP {