diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt index d586524d..9c92c374 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt @@ -2,7 +2,10 @@ package tools.samt.semantic import tools.samt.common.DiagnosticController import tools.samt.common.SourceFile -import tools.samt.parser.* +import tools.samt.parser.FileNode +import tools.samt.parser.NamedDeclarationNode +import tools.samt.parser.TypeImportNode +import tools.samt.parser.WildcardImportNode /** * Goals of the semantic model: @@ -87,7 +90,11 @@ class SemanticModelBuilder private constructor( is LiteralType, is EnumType, is RecordType, - UnknownType -> typeReference + is ServiceType, + is ProviderType, + UnknownType, + -> typeReference + is AliasType -> type.fullyResolvedType?.let { merge(typeReference, it) } is ListType -> { val elementType = getFullyResolvedType(type.elementType) @@ -106,24 +113,10 @@ class SemanticModelBuilder private constructor( null } } - is ServiceType -> { - controller.getOrCreateContext(typeReference.typeNode.location.source).error { - message("Type alias cannot reference service") - highlight("type alias", typeReference.typeNode.location) - } - typeReference - } - is ProviderType -> { - controller.getOrCreateContext(typeReference.typeNode.location.source).error { - message("Type alias cannot reference provider") - highlight("type alias", typeReference.typeNode.location) - } - typeReference - } is PackageType -> { controller.getOrCreateContext(typeReference.typeNode.location.source).error { message("Type alias cannot reference package") - highlight("type alias", typeReference.typeNode.location) + highlight("illegal package", typeReference.typeNode.location) } typeReference } diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt index 34516b2b..451b881d 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt @@ -57,6 +57,26 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont checkModelType(type.valueType) } + is AliasType -> { + val underlyingTypeReference = type.fullyResolvedType ?: return + val underlyingType = underlyingTypeReference.type + if (underlyingType is ServiceType || underlyingType is ProviderType || underlyingType is PackageType) { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Type alias refers to '${underlyingType.humanReadableName}', which cannot be used in this context") + highlight("type alias", typeReference.typeNode.location) + highlight("underlying type", underlyingTypeReference.typeNode.location) + } + } + + if (typeReference.isOptional && underlyingTypeReference.isOptional) { + controller.getOrCreateContext(typeReference.typeNode.location.source).warn { + message("Type alias refers to type which is already optional, ignoring '?'") + highlight("duplicate optional", typeReference.fullNode.location) + highlight("declared optional here", underlyingTypeReference.fullNode.location) + } + } + } + else -> Unit } } @@ -69,6 +89,21 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont block(type) } + is AliasType -> { + val aliasedTypeReference = type.fullyResolvedType ?: return + checkBlankTypeReference(aliasedTypeReference, "service") + val aliasedType = aliasedTypeReference.type + if (aliasedType is ServiceType) { + block(aliasedType) + } else { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Expected a service but type alias '${type.name}' points to '${aliasedType.humanReadableName}'") + highlight("type alias", typeReference.typeNode.location) + highlight("underlying type", aliasedTypeReference.typeNode.location) + } + } + } + is UnknownType -> Unit else -> { controller.getOrCreateContext(typeReference.typeNode.location.source).error { @@ -87,6 +122,21 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont block(type) } + is AliasType -> { + val aliasedTypeReference = type.fullyResolvedType ?: return + checkBlankTypeReference(aliasedTypeReference, "provider") + val aliasedType = aliasedTypeReference.type + if (aliasedType is ProviderType) { + block(aliasedType) + } else { + controller.getOrCreateContext(typeReference.typeNode.location.source).error { + message("Expected a provider but type alias '${type.name}' points to '${aliasedType.humanReadableName}'") + highlight("type alias", typeReference.typeNode.location) + highlight("underlying type", aliasedTypeReference.typeNode.location) + } + } + } + is UnknownType -> Unit else -> { controller.getOrCreateContext(typeReference.typeNode.location.source).error { diff --git a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt index 43746def..e39efaf9 100644 --- a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt +++ b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt @@ -521,6 +521,94 @@ class SemanticModelTest { ) } + @Test + fun `can use service alias`() { + val source = """ + package color + + typealias Foo = MyService + + service MyService { + foo(): String + } + + provide MyEndpoint { + implements Foo { foo } + + transport HTTP + } + """.trimIndent() + parseAndCheck( + source to emptyList() + ) + } + + @Test + fun `cannot use service alias in type model`() { + val source = """ + package color + + typealias Foo = MyService + + service MyService { + foo(): Foo + } + """.trimIndent() + parseAndCheck( + source to listOf("Error: Type alias refers to 'MyService', which cannot be used in this context") + ) + } + + @Test + fun `cannot use package in alias`() { + val source = """ + package color + + typealias myColor = color + """.trimIndent() + parseAndCheck( + source to listOf("Error: Type alias cannot reference package") + ) + } + + @Test + fun `duplicate optional markers within alias definitions are reported`() { + val source = """ + package people + + typealias OptionalName = String? ("a-z") + typealias OptionalDeepName = OptionalName + typealias OptionalDeeperName = OptionalDeepName? + """.trimIndent() + parseAndCheck( + source to listOf( + "Warning: Type is already optional, ignoring '?'", + ) + ) + } + + @Test + fun `duplicate optional markers when referencing alias types are reported`() { + val source = """ + package people + + typealias OptionalName = String? ("a-z") + typealias OptionalDeepName = OptionalName + + record Human { + firstName: OptionalName + lastName: OptionalName? + middleName: OptionalDeepName? + } + """.trimIndent() + parseAndCheck( + source to listOf( + "Warning: Type alias refers to type which is already optional, ignoring '?'", + "Warning: Type alias refers to type which is already optional, ignoring '?'", + ) + ) + } + @Test fun `cannot use type aliases with cyclic references`() { val source = """