diff --git a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt index da1ec894..1e26c0dc 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt @@ -4,7 +4,7 @@ import tools.samt.common.DiagnosticController import tools.samt.common.DiagnosticException import tools.samt.lexer.Lexer import tools.samt.parser.Parser -import tools.samt.semantic.SemanticModelBuilder +import tools.samt.semantic.SemanticModel internal fun compile(command: CompileCommand, controller: DiagnosticController) { val sourceFiles = command.files.readSamtSourceFiles(controller) @@ -40,7 +40,7 @@ internal fun compile(command: CompileCommand, controller: DiagnosticController) } // build up the semantic model from the AST - SemanticModelBuilder.build(fileNodes, controller) + SemanticModel.build(fileNodes, controller) // Code Generators will be called here -} \ No newline at end of file +} diff --git a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt index 10ff95c5..cb4f95c1 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt @@ -5,7 +5,7 @@ import tools.samt.common.DiagnosticController import tools.samt.common.DiagnosticException import tools.samt.lexer.Lexer import tools.samt.parser.Parser -import tools.samt.semantic.SemanticModelBuilder +import tools.samt.semantic.SemanticModel internal fun dump(command: DumpCommand, terminal: Terminal, controller: DiagnosticController) { val sourceFiles = command.files.readSamtSourceFiles(controller) @@ -61,7 +61,7 @@ internal fun dump(command: DumpCommand, terminal: Terminal, controller: Diagnost } // build up the semantic model from the AST - val samtPackage = SemanticModelBuilder.build(fileNodes, controller) + val samtPackage = SemanticModel.build(fileNodes, controller).global if (dumpAll || command.dumpTypes) { terminal.println(TypePrinter.dump(samtPackage)) diff --git a/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt index 57faba60..46b7b5aa 100644 --- a/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/TypePrinterTest.kt @@ -5,7 +5,7 @@ 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 tools.samt.semantic.SemanticModel import java.net.URI import kotlin.test.Test import kotlin.test.assertEquals @@ -47,7 +47,7 @@ class TypePrinterTest { """.trimIndent()) val controller = DiagnosticController(URI("file:///tmp")) - val samtPackage = SemanticModelBuilder.build(listOf(stuffPackage, consumerPackage), controller) + val samtPackage = SemanticModel.build(listOf(stuffPackage, consumerPackage), controller).global assertFalse(controller.hasErrors()) val dump = TypePrinter.dump(samtPackage) 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 d9a5a486..7b026faf 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtDeclarationLookup.kt @@ -7,7 +7,7 @@ import tools.samt.parser.FileNode import tools.samt.parser.IdentifierNode import tools.samt.semantic.* -class SamtDeclarationLookup private constructor() : SamtSemanticLookup() { +class SamtDeclarationLookup private constructor(userMetadata: UserMetadata) : SamtSemanticLookup(userMetadata) { override fun markType(node: ExpressionNode, type: Type) { super.markType(node, type) @@ -56,7 +56,7 @@ class SamtDeclarationLookup private constructor() : SamtSemanticLookup { private val files = mutableMapOf() - var globalPackage: Package? = null + var semanticModel: SemanticModel? = null private set private var semanticController: DiagnosticController = DiagnosticController(path) @@ -34,7 +33,7 @@ class SamtFolder(val path: URI) : Iterable { fun buildSemanticModel() { semanticController = DiagnosticController(path) - globalPackage = SemanticModelBuilder.build(mapNotNull { it.fileNode }, semanticController) + semanticModel = SemanticModel.build(mapNotNull { it.fileNode }, semanticController) } private fun getMessages(path: URI): List { diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt index f1142e89..ff7e6eab 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt @@ -57,6 +57,7 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable { } definitionProvider = Either.forLeft(true) referencesProvider = Either.forLeft(true) + hoverProvider = Either.forLeft(true) } InitializeResult(capabilities) } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtReferencesLookup.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtReferencesLookup.kt index dc4fe6d9..223064f1 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtReferencesLookup.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtReferencesLookup.kt @@ -5,12 +5,9 @@ import tools.samt.parser.BundleIdentifierNode import tools.samt.parser.ExpressionNode import tools.samt.parser.FileNode import tools.samt.parser.IdentifierNode -import tools.samt.semantic.Package -import tools.samt.semantic.ServiceType -import tools.samt.semantic.Type -import tools.samt.semantic.UserDeclared +import tools.samt.semantic.* -class SamtReferencesLookup private constructor() : SamtSemanticLookup>() { +class SamtReferencesLookup private constructor(userMetadata: UserMetadata) : SamtSemanticLookup>(userMetadata) { private fun addUsage(declaration: UserDeclared, usage: Location) { if (this[declaration] == null) { this[declaration] = mutableListOf() @@ -36,10 +33,10 @@ class SamtReferencesLookup private constructor() : SamtSemanticLookup>): SamtReferencesLookup { - val lookup = SamtReferencesLookup() - for ((fileInfo, samtPackage) in filesAndPackages) { - lookup.analyze(fileInfo, samtPackage) + fun analyze(filesAndPackages: List>, userMetadata: UserMetadata): SamtReferencesLookup { + val lookup = SamtReferencesLookup(userMetadata) + for ((fileInfo, filePackage) in filesAndPackages) { + lookup.analyze(fileInfo, filePackage) } return lookup } diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticLookup.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticLookup.kt index b702c465..63971306 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticLookup.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticLookup.kt @@ -3,14 +3,14 @@ package tools.samt.ls import tools.samt.parser.* import tools.samt.semantic.* -abstract class SamtSemanticLookup protected constructor() { - protected fun analyze(fileNode: FileNode, samtPackage: Package) { +abstract class SamtSemanticLookup protected constructor(protected val userMetadata: UserMetadata) { + protected fun analyze(fileNode: FileNode, filePackage: Package) { for (import in fileNode.imports) { - markStatement(samtPackage, import) + markStatement(filePackage, import) } - markStatement(samtPackage, fileNode.packageDeclaration) + markStatement(filePackage, fileNode.packageDeclaration) for (statement in fileNode.statements) { - markStatement(samtPackage, statement) + markStatement(filePackage, statement) } } @@ -69,19 +69,19 @@ abstract class SamtSemanticLookup protected constructor() { } protected open fun markTypeAliasDeclaration(aliasType: AliasType) { - markAnnotations(aliasType.declaration.annotations) + markAnnotations(aliasType.annotations) markTypeReference(aliasType.aliasedType) } protected open fun markServiceDeclaration(serviceType: ServiceType) { - markAnnotations(serviceType.declaration.annotations) + markAnnotations(serviceType.annotations) for (operation in serviceType.operations) { markOperationDeclaration(operation) } } protected open fun markOperationDeclaration(operation: ServiceType.Operation) { - markAnnotations(operation.declaration.annotations) + markAnnotations(operation.annotations) for (parameter in operation.parameters) { markOperationParameterDeclaration(parameter) } @@ -95,24 +95,24 @@ abstract class SamtSemanticLookup protected constructor() { } protected open fun markOperationParameterDeclaration(parameter: ServiceType.Operation.Parameter) { - markAnnotations(parameter.declaration.annotations) + markAnnotations(parameter.annotations) markTypeReference(parameter.type) } protected open fun markRecordDeclaration(recordType: RecordType) { - markAnnotations(recordType.declaration.annotations) + markAnnotations(recordType.annotations) for (field in recordType.fields) { markRecordFieldDeclaration(field) } } protected open fun markRecordFieldDeclaration(field: RecordType.Field) { - markAnnotations(field.declaration.annotations) + markAnnotations(field.annotations) markTypeReference(field.type) } protected open fun markEnumDeclaration(enumType: EnumType) { - markAnnotations(enumType.declaration.annotations) + markAnnotations(enumType.annotations) } protected open fun markProviderDeclaration(providerType: ProviderType) { 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 f7c8f3c5..51783014 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtSemanticTokens.kt @@ -5,38 +5,43 @@ import tools.samt.common.Location import tools.samt.parser.* import tools.samt.semantic.* -class SamtSemanticTokens private constructor() : SamtSemanticLookup() { +class SamtSemanticTokens private constructor(userMetadata: UserMetadata) : SamtSemanticLookup(userMetadata) { override fun markType(node: ExpressionNode, type: Type) { super.markType(node, type) - val location = if (node is BundleIdentifierNode) { - node.components.last().location + val location: Location + if (node is BundleIdentifierNode) { + location = node.components.last().location + node.components.dropLast(1).forEach { + this[it.location] = Metadata(TokenType.namespace) + } } else { - node.location + location = node.location } + val modifier = (type as? UserDeclared)?.getDeprecationModifier() ?: TokenModifier.none when (type) { - is ConsumerType -> this[location] = Metadata(TokenType.type) + is ConsumerType -> this[location] = Metadata(TokenType.type, modifier) - is EnumType -> this[location] = Metadata(TokenType.enum) + is EnumType -> this[location] = Metadata(TokenType.enum, modifier) is ListType -> { this[type.node.base.location] = - Metadata(TokenType.type, TokenModifier.defaultLibrary) + Metadata(TokenType.type, modifier and TokenModifier.defaultLibrary) } is MapType -> { this[type.node.base.location] = - Metadata(TokenType.type, TokenModifier.defaultLibrary) + Metadata(TokenType.type, modifier and TokenModifier.defaultLibrary) } - is AliasType -> 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`) + is AliasType -> this[location] = Metadata(getAliasTokenType(type), modifier) + is ProviderType -> this[location] = Metadata(TokenType.type, modifier) + is RecordType -> this[location] = Metadata(TokenType.`class`, modifier) + is ServiceType -> this[location] = Metadata(TokenType.`interface`, modifier) is LiteralType -> this[location] = - Metadata(TokenType.type, TokenModifier.defaultLibrary) + Metadata(TokenType.type, modifier and TokenModifier.defaultLibrary) - is PackageType -> this[location] = Metadata(TokenType.namespace) - UnknownType -> this[location] = Metadata(TokenType.type) + is PackageType -> this[location] = Metadata(TokenType.namespace, modifier) + UnknownType -> this[location] = Metadata(TokenType.type, modifier) } } @@ -56,39 +61,39 @@ class SamtSemanticTokens private constructor() : SamtSemanticLookup TokenType.enum + is RecordType -> TokenType.`class` + is ServiceType -> TokenType.`interface` + else -> TokenType.type + } + + private fun UserDeclared.getDeprecationModifier() = + if (userMetadata.getDeprecation(this) != null) { + TokenModifier.deprecated + } else { + TokenModifier.none + } + + data class Metadata(val type: TokenType, val modifier: TokenModifier = TokenModifier.none) @Suppress("EnumEntryName") @@ -171,7 +196,8 @@ class SamtSemanticTokens private constructor() : SamtSemanticLookup, List>> = - CompletableFuture.supplyAsync { - val path = params.textDocument.uri.toPathUri() + CompletableFuture.supplyAsync { + val path = params.textDocument.uri.toPathUri() - val fileInfo = workspace.getFile(path) ?: return@supplyAsync Either.forRight(emptyList()) + val fileInfo = workspace.getFile(path) ?: return@supplyAsync Either.forRight(emptyList()) - val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync Either.forRight(emptyList()) - val globalPackage: Package = workspace.getRootPackage(path) ?: return@supplyAsync Either.forRight(emptyList()) + val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync Either.forRight(emptyList()) + val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync Either.forRight(emptyList()) + val globalPackage: Package = semanticModel.global - val token = fileInfo.tokens.findAt(params.position) ?: return@supplyAsync Either.forRight(emptyList()) + val token = fileInfo.tokens.findAt(params.position) ?: return@supplyAsync Either.forRight(emptyList()) - val samtPackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) + val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) - val typeLookup = SamtDeclarationLookup.analyze(fileNode, samtPackage) - val type = typeLookup[token.location] ?: return@supplyAsync Either.forRight(emptyList()) + val typeLookup = SamtDeclarationLookup.analyze(fileNode, filePackage, semanticModel.userMetadata) + val type = typeLookup[token.location] ?: return@supplyAsync Either.forRight(emptyList()) - val definition = type.declaration - val location = definition.location + val definition = type.declaration + val location = definition.location - val targetLocation = when (definition) { - is NamedDeclarationNode -> definition.name.location - is OperationNode -> definition.name.location - else -> error("Unexpected definition type") - } - val locationLink = LocationLink().apply { - targetUri = location.source.path.toString() - targetRange = location.toRange() - targetSelectionRange = targetLocation.toRange() + val targetLocation = when (definition) { + is NamedDeclarationNode -> definition.name.location + is OperationNode -> definition.name.location + else -> error("Unexpected definition type") + } + val locationLink = LocationLink().apply { + targetUri = location.source.path.toString() + targetRange = location.toRange() + targetSelectionRange = targetLocation.toRange() + } + return@supplyAsync Either.forRight(listOf(locationLink)) } - return@supplyAsync Either.forRight(listOf(locationLink)) - } override fun references(params: ReferenceParams): CompletableFuture> = - CompletableFuture.supplyAsync { - val path = params.textDocument.uri.toPathUri() + CompletableFuture.supplyAsync { + val path = params.textDocument.uri.toPathUri() + + val relevantFileInfo = workspace.getFile(path) ?: return@supplyAsync emptyList() + val relevantFileNode = relevantFileInfo.fileNode ?: return@supplyAsync emptyList() + val token = relevantFileInfo.tokens.findAt(params.position) ?: return@supplyAsync emptyList() + + val (_, files, semanticModel) = workspace.getFolderSnapshot(path) ?: return@supplyAsync emptyList() + if (semanticModel == null) return@supplyAsync emptyList() + + val globalPackage = semanticModel.global + val typeLookup = SamtDeclarationLookup.analyze( + relevantFileNode, + globalPackage.resolveSubPackage(relevantFileInfo.fileNode.packageDeclaration.name), + semanticModel.userMetadata + ) + val type = typeLookup[token.location] ?: return@supplyAsync emptyList() + + val filesAndPackages = buildList { + for (fileInfo in files) { + val fileNode: FileNode = fileInfo.fileNode ?: continue + val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) + add(fileNode to filePackage) + } + } - val relevantFileInfo = workspace.getFile(path) ?: return@supplyAsync emptyList() - val relevantFileNode = relevantFileInfo.fileNode ?: return@supplyAsync emptyList() - val token = relevantFileInfo.tokens.findAt(params.position) ?: return@supplyAsync emptyList() + val typeReferencesLookup = SamtReferencesLookup.analyze(filesAndPackages, semanticModel.userMetadata) - val (_, files, globalPackage) = workspace.getFolderSnapshot(path) ?: return@supplyAsync emptyList() - if (globalPackage == null) return@supplyAsync emptyList() + val references = typeReferencesLookup[type] ?: emptyList() - val typeLookup = SamtDeclarationLookup.analyze(relevantFileNode, globalPackage.resolveSubPackage(relevantFileInfo.fileNode.packageDeclaration.name)) - val type = typeLookup[token.location] ?: return@supplyAsync emptyList() + return@supplyAsync references.map { Location(it.source.path.toString(), it.toRange()) } + } - val filesAndPackages = buildList { - for (fileInfo in files) { - val fileNode: FileNode = fileInfo.fileNode ?: continue - val samtPackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) - add(fileNode to samtPackage) + override fun semanticTokensFull(params: SemanticTokensParams): CompletableFuture = + CompletableFuture.supplyAsync { + val path = params.textDocument.uri.toPathUri() + + val fileInfo = workspace.getFile(path) ?: return@supplyAsync SemanticTokens(emptyList()) + + val tokens: List = fileInfo.tokens + val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync SemanticTokens(emptyList()) + val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync SemanticTokens(emptyList()) + val filePackage = semanticModel.global.resolveSubPackage(fileNode.packageDeclaration.name) + + val semanticTokens = SamtSemanticTokens.analyze(fileNode, filePackage, semanticModel.userMetadata) + + var lastLine = 0 + var lastStartChar = 0 + + val encodedData = buildList { + for (token in tokens) { + val (tokenType, modifier) = semanticTokens[token.location] ?: continue + val (_, start, end) = token.location + val line = start.row + val deltaLine = line - lastLine + val startChar = start.col + val deltaStartChar = if (deltaLine == 0) startChar - lastStartChar else startChar + val length = end.charIndex - start.charIndex + add(deltaLine) + add(deltaStartChar) + add(length) + add(tokenType.ordinal) + add(modifier.bitmask) + lastLine = line + lastStartChar = startChar + } } + + SemanticTokens(encodedData) } - val typeReferencesLookup = SamtReferencesLookup.analyze(filesAndPackages) + override fun hover(params: HoverParams): CompletableFuture = CompletableFuture.supplyAsync { + val path = params.textDocument.uri.toPathUri() + + val fileInfo = workspace.getFile(path) ?: return@supplyAsync null - val references = typeReferencesLookup[type] ?: emptyList() + val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync null + val semanticModel = workspace.getSemanticModel(path) ?: return@supplyAsync null + val globalPackage: Package = semanticModel.global - return@supplyAsync references.map { Location(it.source.path.toString(), it.toRange()) } + val token = fileInfo.tokens.findAt(params.position) ?: return@supplyAsync null + val filePackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) + + val typeLookup = SamtDeclarationLookup.analyze(fileNode, filePackage, semanticModel.userMetadata) + val type = typeLookup[token.location] ?: return@supplyAsync null + + val description = buildString { + appendLine("```samt") + appendLine(type.peekDeclaration()) + appendLine("```") + appendLine("---") + appendLine(semanticModel.userMetadata.getDescription(type).orEmpty()) + } + Hover().apply { + contents = Either.forRight(MarkupContent(MarkupKind.MARKDOWN, description)) + range = token.location.toRange() } + } - override fun semanticTokensFull(params: SemanticTokensParams): CompletableFuture = - CompletableFuture.supplyAsync { - val path = params.textDocument.uri.toPathUri() - - val fileInfo = workspace.getFile(path) ?: return@supplyAsync SemanticTokens(emptyList()) - - val tokens: List = fileInfo.tokens - val fileNode: FileNode = fileInfo.fileNode ?: return@supplyAsync SemanticTokens(emptyList()) - val globalPackage: Package = workspace.getRootPackage(path) ?: return@supplyAsync SemanticTokens(emptyList()) - val samtPackage = globalPackage.resolveSubPackage(fileNode.packageDeclaration.name) - - val semanticTokens = SamtSemanticTokens.analyze(fileNode, samtPackage) - - var lastLine = 0 - var lastStartChar = 0 - - val encodedData = buildList { - for (token in tokens) { - val (tokenType, modifier) = semanticTokens[token.location] ?: continue - val (_, start, end) = token.location - val line = start.row - val deltaLine = line - lastLine - val startChar = start.col - val deltaStartChar = if (deltaLine == 0) startChar - lastStartChar else startChar - val length = end.charIndex - start.charIndex - add(deltaLine) - add(deltaStartChar) - add(length) - add(tokenType.ordinal) - add(modifier.bitmask) - lastLine = line - lastStartChar = startChar + private fun UserDeclared.peekDeclaration(): String { + fun List.toParameterList(): String = joinToString(", ") { it.peekDeclaration() } + + return when (this) { + is AliasType -> "${getHumanReadableName()} $humanReadableName" + is EnumType -> "${getHumanReadableName()} $humanReadableName" + is RecordType.Field -> "$name: ${type.humanReadableName}" + is ServiceType.OnewayOperation -> "${getHumanReadableName()} $name(${parameters.toParameterList()})" + is ServiceType.RequestResponseOperation -> buildString { + if (isAsync) { + append(getHumanReadableName()) + append(' ') + } + append(name) + append('(') + append(parameters.toParameterList()) + append(')') + returnType?.let { + append(": ") + append(it.humanReadableName) + } + if (raisesTypes.isNotEmpty()) { + append(' ') + append(getHumanReadableName()) + append(' ') + raisesTypes.joinTo(this, ", ") { it.humanReadableName } } } - - SemanticTokens(encodedData) + is ServiceType.Operation.Parameter -> "$name: ${type.humanReadableName}" + is RecordType -> "${getHumanReadableName()} $humanReadableName" + is ServiceType -> "${getHumanReadableName()} $humanReadableName" + is ConsumerType -> "${getHumanReadableName()} $humanReadableName" + is ProviderType -> "${getHumanReadableName()} $humanReadableName" } + } override fun connect(client: LanguageClient) { this.client = client diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt index d24c26cc..691357a7 100644 --- a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt +++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt @@ -3,7 +3,7 @@ package tools.samt.ls import org.eclipse.lsp4j.PublishDiagnosticsParams import org.eclipse.lsp4j.services.LanguageClient import tools.samt.common.DiagnosticMessage -import tools.samt.semantic.Package +import tools.samt.semantic.SemanticModel import java.net.URI class SamtWorkspace { @@ -11,7 +11,7 @@ class SamtWorkspace { private val changedFolders = mutableSetOf() private val removedFiles = mutableSetOf() - fun getFolderSnapshot(path: URI): FolderSnapshot? = getFolder(path)?.let { FolderSnapshot(it.path, it.toList(), it.globalPackage) } + fun getFolderSnapshot(path: URI): FolderSnapshot? = getFolder(path)?.let { FolderSnapshot(it.path, it.toList(), it.semanticModel) } fun addFolder(folder: SamtFolder) { val newPath = folder.path @@ -58,7 +58,7 @@ class SamtWorkspace { } } - fun getRootPackage(path: URI): Package? = getFolder(path)?.globalPackage + fun getSemanticModel(path: URI): SemanticModel? = getFolder(path)?.semanticModel fun getPendingMessages(): Map> = changedFolders.flatMap { folder -> folder.getAllMessages().toList() @@ -76,7 +76,7 @@ class SamtWorkspace { private fun getFolder(path: URI): SamtFolder? = folders[path] ?: folders.values.singleOrNull { path.startsWith(it.path) } } -data class FolderSnapshot(val path: URI, val files: List, val globalPackage: Package?) +data class FolderSnapshot(val path: URI, val files: List, val semanticModel: SemanticModel?) fun LanguageClient.updateWorkspace(workspace: SamtWorkspace) { diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtDeclarationLookupTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtDeclarationLookupTest.kt index adc638b5..263f66d4 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtDeclarationLookupTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtDeclarationLookupTest.kt @@ -4,7 +4,7 @@ import tools.samt.common.DiagnosticController import tools.samt.common.SourceFile import tools.samt.lexer.Lexer import tools.samt.parser.* -import tools.samt.semantic.SemanticModelBuilder +import tools.samt.semantic.SemanticModel import java.net.URI import kotlin.test.Test import kotlin.test.assertFalse @@ -140,11 +140,12 @@ class SamtDeclarationLookupTest { fileTree } - val samtPackage = SemanticModelBuilder.build(fileTree, diagnosticController) + val semanticModel = SemanticModel.build(fileTree, diagnosticController) + val samtPackage = semanticModel.global for ((fileNode, expectedMetadata) in fileTree.zip(sourceAndExpectedMessages.map { it.second })) { val filePackage = samtPackage.resolveSubPackage(fileNode.packageDeclaration.name) - val definitionLookup = SamtDeclarationLookup.analyze(fileNode, filePackage) + val definitionLookup = SamtDeclarationLookup.analyze(fileNode, filePackage, semanticModel.userMetadata) for (expected in expectedMetadata) { val actual = definitionLookup[expected.testLocation.getLocation(fileNode.sourceFile)] assertNotNull(actual, "No definition found for ${expected.range}") diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtReferencesLookupTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtReferencesLookupTest.kt index 295d689a..7c95a0e6 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtReferencesLookupTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtReferencesLookupTest.kt @@ -5,7 +5,7 @@ import tools.samt.common.SourceFile import tools.samt.lexer.Lexer import tools.samt.parser.Parser import tools.samt.semantic.Package -import tools.samt.semantic.SemanticModelBuilder +import tools.samt.semantic.SemanticModel import java.net.URI import kotlin.test.* @@ -104,9 +104,10 @@ class SamtReferencesLookupTest { fileTree } - val samtPackage = SemanticModelBuilder.build(fileTree, diagnosticController) + val semanticModel = SemanticModel.build(fileTree, diagnosticController) + val samtPackage = semanticModel.global val filesAndPackages = fileTree.map { it to samtPackage.resolveSubPackage(it.packageDeclaration.name) } - return Pair(samtPackage, SamtReferencesLookup.analyze(filesAndPackages)) + return Pair(samtPackage, SamtReferencesLookup.analyze(filesAndPackages, semanticModel.userMetadata)) } } diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtSemanticTokensTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtSemanticTokensTest.kt index bbbce1d4..4b8b380d 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtSemanticTokensTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtSemanticTokensTest.kt @@ -4,7 +4,7 @@ import tools.samt.common.DiagnosticController import tools.samt.common.SourceFile import tools.samt.lexer.Lexer import tools.samt.parser.Parser -import tools.samt.semantic.SemanticModelBuilder +import tools.samt.semantic.SemanticModel import java.net.URI import kotlin.test.Test import kotlin.test.assertEquals @@ -142,6 +142,132 @@ class SamtSemanticTokensTest { ) } + @Test + fun `typealiases inherit token type`() { + val source = """ + package typealiases + + typealias Builtin = String + typealias Record = R + typealias Enum = E + typealias Service = S + typealias Provider = P + + record R { + b: Builtin + e: Enum + } + + enum E {} + + service S { + get(): Record + } + + provide P { + implements Service + + transport http + } + + consume Provider { + uses Service + } + """.trimIndent() + parseAndCheck( + source to listOf( + ExpectedMetadata("2:10" to "2:17", Meta(T.type, Mod.declaration)), + ExpectedMetadata("3:10" to "3:16", Meta(T.`class`, Mod.declaration)), + ExpectedMetadata("4:10" to "4:14", Meta(T.enum, Mod.declaration)), + ExpectedMetadata("5:10" to "5:17", Meta(T.`interface`, Mod.declaration)), + ExpectedMetadata("6:10" to "6:18", Meta(T.type, Mod.declaration)), + ExpectedMetadata("9:7" to "9:14", Meta(T.type)), + ExpectedMetadata("10:7" to "10:11", Meta(T.enum)), + ExpectedMetadata("16:11" to "16:17", Meta(T.`class`)), + ExpectedMetadata("20:15" to "20:22", Meta(T.`interface`)), + ExpectedMetadata("25:8" to "25:16", Meta(T.type)), + ExpectedMetadata("26:9" to "26:16", Meta(T.`interface`)), + ), + ) + } + + @Test + fun `correctly tokenizes deprecations`() { + val source = """ + package deprecations + + @Deprecated + enum UserType { + ADMIN, USER + } + + @Deprecated + typealias Id = Long(1..*) + + @Deprecated + record User { + id: Id + @Deprecated + type: UserType + } + + @Deprecated + service UserService { + @Deprecated + get(@Deprecated id: Id): User + } + + provide UserProvider { + implements UserService { get } + + transport http + } + """.trimIndent() + parseAndCheck( + source to listOf( + ExpectedMetadata("3:5" to "3:13", Meta(T.enum, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("8:10" to "8:12", Meta(T.type, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("11:7" to "11:11", Meta(T.`class`, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("12:8" to "12:10", Meta(T.type, Mod.deprecated)), + ExpectedMetadata("14:4" to "14:8", Meta(T.property, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("14:10" to "14:18", Meta(T.enum, Mod.deprecated)), + ExpectedMetadata("18:8" to "18:19", Meta(T.`interface`, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("20:4" to "20:7", Meta(T.method, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("20:20" to "20:22", Meta(T.parameter, Mod.declaration and Mod.deprecated)), + ExpectedMetadata("20:24" to "20:26", Meta(T.type, Mod.deprecated)), + ExpectedMetadata("20:29" to "20:33", Meta(T.`class`, Mod.deprecated)), + ExpectedMetadata("24:15" to "24:26", Meta(T.`interface`, Mod.deprecated)), + ExpectedMetadata("24:29" to "24:32", Meta(T.method, Mod.deprecated)), + ), + ) + } + + @Test + fun `correctly tokenizes fully qualified names`() { + val enumSource = """ + package test.lib + + enum Enum { + A, B + } + """.trimIndent() + val recordSource = """ + package test.impl + + record Record { + e: test.lib.Enum + } + """.trimIndent() + parseAndCheck( + enumSource to emptyList(), + recordSource to listOf( + ExpectedMetadata("3:7" to "3:11", Meta(T.namespace)), + ExpectedMetadata("3:12" to "3:15", Meta(T.namespace)), + ExpectedMetadata("3:16" to "3:20", Meta(T.enum)) + ), + ) + } + private data class ExpectedMetadata(val range: Pair, val metadata: Meta) { val testLocation = TestLocation(range) } @@ -160,11 +286,12 @@ class SamtSemanticTokensTest { fileTree } - val samtPackage = SemanticModelBuilder.build(fileTree, diagnosticController) + val semanticModel = SemanticModel.build(fileTree, diagnosticController) + val samtPackage = semanticModel.global for ((fileNode, expectedMetadata) in fileTree.zip(sourceAndExpectedMessages.map { it.second })) { val filePackage = samtPackage.resolveSubPackage(fileNode.packageDeclaration.name) - val semanticTokens = SamtSemanticTokens.analyze(fileNode, filePackage) + val semanticTokens = SamtSemanticTokens.analyze(fileNode, filePackage, semanticModel.userMetadata) for (expected in expectedMetadata) { val actual = semanticTokens[expected.testLocation.getLocation(fileNode.sourceFile)] assertEquals(expected.metadata, actual, "Metadata for ${expected.range} did not match") diff --git a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt index aed71d14..6ff0556c 100644 --- a/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt +++ b/language-server/src/test/kotlin/tools/samt/ls/SamtWorkspaceTest.kt @@ -133,15 +133,15 @@ class SamtWorkspaceTest { } @Test - fun `package can be found after buildSemanticModel`() { + fun `semanticModel can be found after buildSemanticModel`() { val workspace = SamtWorkspace() workspace.addFolder(SamtFolder("file:///tmp/test".toPathUri())) val file1 = parseFile(SourceFile ("file:///tmp/test/foo.samt".toPathUri(), "package foo.bar record Foo {}")) workspace.setFile(file1) - assertNull(workspace.getRootPackage(file1.path)) + assertNull(workspace.getSemanticModel(file1.path)) workspace.buildSemanticModel() - val rootPackage = workspace.getRootPackage(file1.path) - assertNotNull(rootPackage) + val semanticModel = workspace.getSemanticModel(file1.path) + assertNotNull(semanticModel) } @Test diff --git a/parser/src/main/kotlin/tools/samt/parser/Nodes.kt b/parser/src/main/kotlin/tools/samt/parser/Nodes.kt index ba8d9c76..e8c99791 100644 --- a/parser/src/main/kotlin/tools/samt/parser/Nodes.kt +++ b/parser/src/main/kotlin/tools/samt/parser/Nodes.kt @@ -3,206 +3,204 @@ package tools.samt.parser import tools.samt.common.Location import tools.samt.common.SourceFile -sealed class Node(val location: Location) +sealed interface Node { + val location: Location +} + +sealed interface AnnotatedNode : Node { + val annotations: List +} class FileNode( - location: Location, + override val location: Location, val sourceFile: SourceFile, val imports: List, val packageDeclaration: PackageDeclarationNode, val statements: List, -) : Node(location) +) : Node -sealed class StatementNode( - location: Location, -) : Node(location) +sealed interface StatementNode : Node -sealed class NamedDeclarationNode( - val name: IdentifierNode, - location: Location, -) : StatementNode(location) +sealed interface NamedDeclarationNode : StatementNode { + val name: IdentifierNode +} -sealed class ImportNode( - location: Location, - val name: BundleIdentifierNode, -) : StatementNode(location) +sealed interface ImportNode : StatementNode { + val name: BundleIdentifierNode +} class TypeImportNode( - location: Location, - name: BundleIdentifierNode, + override val location: Location, + override val name: BundleIdentifierNode, val alias: IdentifierNode?, -) : ImportNode(location, name) +) : ImportNode class WildcardImportNode( - location: Location, - name: BundleIdentifierNode, -) : ImportNode(location, name) + override val location: Location, + override val name: BundleIdentifierNode, +) : ImportNode class PackageDeclarationNode( - location: Location, + override val location: Location, val name: BundleIdentifierNode, -) : StatementNode(location) +) : StatementNode class RecordDeclarationNode( - location: Location, - name: IdentifierNode, + override val location: Location, + override val name: IdentifierNode, val extends: List = emptyList(), val fields: List, - val annotations: List, -) : NamedDeclarationNode(name, location) + override val annotations: List, +) : NamedDeclarationNode, AnnotatedNode class RecordFieldNode( - location: Location, + override val location: Location, val name: IdentifierNode, val type: ExpressionNode, - val annotations: List, -) : Node(location) + override val annotations: List, +) : Node, AnnotatedNode class EnumDeclarationNode( - location: Location, - name: IdentifierNode, + override val location: Location, + override val name: IdentifierNode, val values: List, - val annotations: List, -) : NamedDeclarationNode(name, location) + override val annotations: List, +) : NamedDeclarationNode, AnnotatedNode class TypeAliasNode( - location: Location, - name: IdentifierNode, + override val location: Location, + override val name: IdentifierNode, val type: ExpressionNode, - val annotations: List, -) : NamedDeclarationNode(name, location) + override val annotations: List, +) : NamedDeclarationNode, AnnotatedNode class ServiceDeclarationNode( - location: Location, - name: IdentifierNode, + override val location: Location, + override val name: IdentifierNode, val operations: List, - val annotations: List, -) : NamedDeclarationNode(name, location) + override val annotations: List, +) : NamedDeclarationNode, AnnotatedNode -sealed class OperationNode( - location: Location, - val name: IdentifierNode, - val parameters: List, - val annotations: List, -) : Node(location) +sealed interface OperationNode : Node, AnnotatedNode { + val name: IdentifierNode + val parameters: List +} class OperationParameterNode( - location: Location, + override val location: Location, val name: IdentifierNode, val type: ExpressionNode, - val annotations: List, -) : Node(location) + override val annotations: List, +) : Node, AnnotatedNode class RequestResponseOperationNode( - location: Location, - name: IdentifierNode, - parameters: List, + override val location: Location, + override val name: IdentifierNode, + override val parameters: List, val returnType: ExpressionNode?, val raises: List, val isAsync: Boolean, - annotations: List, -) : OperationNode(location, name, parameters, annotations) + override val annotations: List, +) : OperationNode class OnewayOperationNode( - location: Location, - name: IdentifierNode, - parameters: List, - annotations: List, -) : OperationNode(location, name, parameters, annotations) + override val location: Location, + override val name: IdentifierNode, + override val parameters: List, + override val annotations: List, +) : OperationNode class ProviderDeclarationNode( - location: Location, - name: IdentifierNode, + override val location: Location, + override val name: IdentifierNode, val implements: List, val transport: ProviderTransportNode, -) : NamedDeclarationNode(name, location) +) : NamedDeclarationNode class ProviderImplementsNode( - location: Location, + override val location: Location, val serviceName: BundleIdentifierNode, val serviceOperationNames: List, -) : Node(location) +) : Node class ProviderTransportNode( - location: Location, + override val location: Location, val protocolName: IdentifierNode, val configuration: ObjectNode?, -) : Node(location) +) : Node class ConsumerDeclarationNode( - location: Location, + override val location: Location, val providerName: BundleIdentifierNode, val usages: List, -) : StatementNode(location) +) : StatementNode class ConsumerUsesNode( - location: Location, + override val location: Location, val serviceName: BundleIdentifierNode, val serviceOperationNames: List, -) : Node(location) +) : Node class AnnotationNode( - location: Location, + override val location: Location, val name: IdentifierNode, val arguments: List, -) : Node(location) +) : Node -sealed class ExpressionNode( - location: Location, -) : Node(location) +sealed interface ExpressionNode : Node class CallExpressionNode( - location: Location, + override val location: Location, val base: ExpressionNode, val arguments: List, -) : ExpressionNode(location) +) : ExpressionNode class GenericSpecializationNode( - location: Location, + override val location: Location, val base: ExpressionNode, val arguments: List, -) : ExpressionNode(location) +) : ExpressionNode class OptionalDeclarationNode( - location: Location, + override val location: Location, val base: ExpressionNode, -) : ExpressionNode(location) +) : ExpressionNode class RangeExpressionNode( - location: Location, + override val location: Location, val left: ExpressionNode, val right: ExpressionNode, -) : ExpressionNode(location) +) : ExpressionNode class ObjectNode( - location: Location, + override val location: Location, val fields: List, -) : ExpressionNode(location) +) : ExpressionNode class ObjectFieldNode( - location: Location, + override val location: Location, val name: IdentifierNode, val value: ExpressionNode, -) : Node(location) +) : Node class ArrayNode( - location: Location, + override val location: Location, val values: List, -) : ExpressionNode(location) +) : ExpressionNode class WildcardNode( - location: Location, -) : ExpressionNode(location) + override val location: Location, +) : ExpressionNode class IdentifierNode( - location: Location, + override val location: Location, val name: String, -) : ExpressionNode(location) +) : ExpressionNode open class BundleIdentifierNode( - location: Location, + override val location: Location, val components: List, -) : ExpressionNode(location) { +) : ExpressionNode { val name: String get() = components.joinToString(".") { it.name } } @@ -213,28 +211,26 @@ class ImportBundleIdentifierNode( val isWildcard: Boolean, ) : BundleIdentifierNode(location, components) -sealed class NumberNode( - location: Location, -) : ExpressionNode(location) { - abstract val value: Number +sealed interface NumberNode : ExpressionNode { + val value: Number } class IntegerNode( - location: Location, + override val location: Location, override val value: Long, -) : NumberNode(location) +) : NumberNode class FloatNode( - location: Location, + override val location: Location, override val value: Double, -) : NumberNode(location) +) : NumberNode class BooleanNode( - location: Location, + override val location: Location, val value: Boolean, -) : ExpressionNode(location) +) : ExpressionNode class StringNode( - location: Location, + override val location: Location, val value: String, -) : ExpressionNode(location) +) : ExpressionNode diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt index 9c92c374..4c6df4b2 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt @@ -7,6 +7,18 @@ import tools.samt.parser.NamedDeclarationNode import tools.samt.parser.TypeImportNode import tools.samt.parser.WildcardImportNode +class SemanticModel( + val global: Package, + val userMetadata: UserMetadata, +) { + companion object { + fun build(files: List, controller: DiagnosticController): SemanticModel { + // Sort by path to ensure deterministic order + return SemanticModelBuilder(files.sortedBy { it.sourceFile.path }, controller).build() + } + } +} + /** * Goals of the semantic model: * - Model the entire package structure of the project @@ -14,7 +26,7 @@ import tools.samt.parser.WildcardImportNode * - Resolve all references to types * - Resolve all references to their declarations in the AST * */ -class SemanticModelBuilder private constructor( +internal class SemanticModelBuilder ( private val files: List, private val controller: DiagnosticController, ) { @@ -22,8 +34,9 @@ class SemanticModelBuilder private constructor( private val preProcessor = SemanticModelPreProcessor(controller) private val postProcessor = SemanticModelPostProcessor(controller) private val referenceResolver = SemanticModelReferenceResolver(controller, global) + private val annotationProcessor = SemanticModelAnnotationProcessor(controller) - private fun build(): Package { + fun build(): SemanticModel { preProcessor.fillPackage(global, files) val fileScopeBySource = files.associate { it.sourceFile to createFileScope(it) } @@ -33,7 +46,7 @@ class SemanticModelBuilder private constructor( postProcessor.process(global) - return global + return SemanticModel(global, annotationProcessor.process(global)) } private fun resolveAliases() { @@ -267,11 +280,4 @@ class SemanticModelBuilder private constructor( return FileScope(filePackage, typeLookup) } - - companion object { - fun build(files: List, controller: DiagnosticController): Package { - // Sort by path to ensure deterministic order - return SemanticModelBuilder(files.sortedBy { it.sourceFile.path }, controller).build() - } - } } diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelAnnotationProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelAnnotationProcessor.kt new file mode 100644 index 00000000..92e256e5 --- /dev/null +++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelAnnotationProcessor.kt @@ -0,0 +1,111 @@ +package tools.samt.semantic + +import tools.samt.common.DiagnosticController +import tools.samt.parser.AnnotationNode +import tools.samt.parser.StringNode + +internal class SemanticModelAnnotationProcessor( + private val controller: DiagnosticController +) { + fun process(global: Package): UserMetadata { + val descriptions = mutableMapOf() + val deprecations = mutableMapOf() + for (element in global.getAnnotatedElements()) { + for (annotation in element.annotations) { + val context = controller.getOrCreateContext(annotation.location.source) + when (val name = annotation.name.name) { + "Description" -> { + if (element in descriptions) { + context.error { + message("Duplicate @Description annotation") + highlight("duplicate annotation", annotation.location) + highlight("previous annotation", element.annotations.first { it.name.name == "Description" }.location) + } + } + descriptions[element] = getDescription(annotation) + } + "Deprecated" -> { + if (element in deprecations) { + context.error { + message("Duplicate @Deprecated annotation") + highlight("duplicate annotation", annotation.location) + highlight("previous annotation", element.annotations.first { it.name.name == "Deprecated" }.location) + } + } + deprecations[element] = getDeprecation(annotation) + } + else -> { + context.error { + message("Unknown annotation @${name}, allowed annotations are @Description and @Deprecated") + highlight("invalid annotation", annotation.location) + } + } + } + } + } + return UserMetadata(descriptions, deprecations) + } + + private fun Package.getAnnotatedElements(): List = buildList { + this@getAnnotatedElements.allSubPackages.forEach { + addAll(it.records) + it.records.flatMapTo(this, RecordType::fields) + addAll(it.enums) + addAll(it.aliases) + addAll(it.services) + val operations = it.services.flatMap(ServiceType::operations) + addAll(operations) + operations.flatMapTo(this, ServiceType.Operation::parameters) + } + } + + private fun getDescription(annotation: AnnotationNode): String { + check(annotation.name.name == "Description") + val arguments = annotation.arguments + val context = controller.getOrCreateContext(annotation.location.source) + if (arguments.isEmpty()) { + context.error { + message("Missing argument for @Description") + highlight("invalid annotation", annotation.location) + } + return "" + } + if (arguments.size > 1) { + val errorLocation = arguments[1].location.copy(end = arguments.last().location.end) + context.error { + message("@Description expects exactly one string argument") + highlight("extraneous arguments", errorLocation) + } + } + return when (val description = arguments.first()) { + is StringNode -> description.value + else -> { + context.error { + message("Argument for @Description must be a string") + highlight("invalid argument type", description.location) + } + "" + } + } + } + + private fun getDeprecation(annotation: AnnotationNode): UserMetadata.Deprecation { + check(annotation.name.name == "Deprecated") + val context = controller.getOrCreateContext(annotation.location.source) + val description = annotation.arguments.firstOrNull() + if (description != null && description !is StringNode) { + context.error { + message("Argument for @Deprecated must be a string") + highlight("invalid argument type", description.location) + } + } + if (annotation.arguments.size > 1) { + val errorLocation = annotation.arguments[1].location.copy(end = annotation.arguments.last().location.end) + context.error { + message("@Deprecated expects at most one string argument") + highlight("extraneous arguments", errorLocation) + } + } + return UserMetadata.Deprecation((description as? StringNode)?.value) + } +} diff --git a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt index 4fea4bad..806683d3 100644 --- a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt +++ b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt @@ -116,6 +116,11 @@ sealed interface UserDeclared { val declaration: Node } +sealed interface Annotated : UserDeclared { + override val declaration: AnnotatedNode + val annotations: List get() = declaration.annotations +} + data class ListType( val elementType: TypeReference, val node: GenericSpecializationNode, @@ -138,7 +143,7 @@ class AliasType( /** 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 { +) : CompoundType, Annotated { override val humanReadableName: String = name } @@ -146,12 +151,12 @@ class RecordType( val name: String, val fields: List, override val declaration: RecordDeclarationNode, -) : CompoundType, UserDeclared { - data class Field( +) : CompoundType, Annotated { + class Field( val name: String, var type: TypeReference, - val declaration: RecordFieldNode, - ) + override val declaration: RecordFieldNode, + ) : Annotated override val humanReadableName: String = name } @@ -160,7 +165,7 @@ class EnumType( val name: String, val values: List, override val declaration: EnumDeclarationNode, -) : CompoundType, UserDeclared { +) : CompoundType, Annotated { override val humanReadableName: String = name } @@ -168,16 +173,16 @@ class ServiceType( val name: String, val operations: List, override val declaration: ServiceDeclarationNode, -) : CompoundType, UserDeclared { - sealed interface Operation : UserDeclared { +) : CompoundType, Annotated { + sealed interface Operation : Annotated { val name: String val parameters: List override val declaration: OperationNode - data class Parameter( + class Parameter( val name: String, var type: TypeReference, override val declaration: OperationParameterNode, - ): UserDeclared + ): UserDeclared, Annotated } class RequestResponseOperation( diff --git a/semantic/src/main/kotlin/tools/samt/semantic/UserMetadata.kt b/semantic/src/main/kotlin/tools/samt/semantic/UserMetadata.kt new file mode 100644 index 00000000..5da96d15 --- /dev/null +++ b/semantic/src/main/kotlin/tools/samt/semantic/UserMetadata.kt @@ -0,0 +1,9 @@ +package tools.samt.semantic + +class UserMetadata(private val descriptions: Map, private val deprecations: Map) { + data class Deprecation(val message: String?) + + fun getDescription(element: UserDeclared): String? = descriptions[element] + + fun getDeprecation(element: UserDeclared): Deprecation? = deprecations[element] +} diff --git a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt index e39efaf9..c0a4e012 100644 --- a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt +++ b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt @@ -832,9 +832,348 @@ class SemanticModelTest { } } + @Nested + inner class Annotations { + @Test + fun `can retrieve descriptions`() { + val source = """ + package annotations + + @Description("enum description") + enum UserType { + ADMIN, USER + } + + @Description("typealias description") + typealias Id = Long(1..*) + + @Description("record description") + record User { + @Description("field description") + id: Id + } + + @Description("service description") + service UserService { + @Description("operation description") + get(@Description("parameter description") id: Id): User + } + """.trimIndent() + val model = parseAndCheck( + source to emptyList() + ) + val samtPackage = model.global.subPackages.single() + val metadata = model.userMetadata + assertEquals("enum description", metadata.getDescription(samtPackage.enums.single())) + assertEquals("typealias description", metadata.getDescription(samtPackage.aliases.single())) + val record = samtPackage.records.single() + assertEquals("record description", metadata.getDescription(record)) + assertEquals("field description", metadata.getDescription(record.fields.single())) + val service = samtPackage.services.single() + assertEquals("service description", metadata.getDescription(service)) + val operation = service.operations.single() + assertEquals("operation description", metadata.getDescription(operation)) + val parameter = operation.parameters.single() + assertEquals("parameter description", metadata.getDescription(parameter)) + } + + @Test + fun `can retrieve deprecations`() { + val source = """ + package annotations + + @Deprecated("enum deprecation") + enum UserType { + ADMIN, USER + } + + @Deprecated("typealias deprecation") + typealias Id = Long(1..*) + + @Deprecated("record deprecation") + record User { + @Deprecated("field description") + id: Id + @Deprecated + type: UserType + } + + @Deprecated("service deprecation") + service UserService { + @Deprecated("operation deprecation") + get(@Deprecated("parameter deprecation") id: Id): User + } + """.trimIndent() + val model = parseAndCheck( + source to emptyList() + ) + val samtPackage = model.global.subPackages.single() + val metadata = model.userMetadata + assertEquals(UserMetadata.Deprecation("enum deprecation"), metadata.getDeprecation(samtPackage.enums.single())) + assertEquals(UserMetadata.Deprecation("typealias deprecation"), metadata.getDeprecation(samtPackage.aliases.single())) + val record = samtPackage.records.single() + assertEquals(UserMetadata.Deprecation("record deprecation"), metadata.getDeprecation(record)) + assertEquals(listOf(UserMetadata.Deprecation("field description"), UserMetadata.Deprecation(null)), record.fields.map { metadata.getDeprecation(it) }) + val service = samtPackage.services.single() + assertEquals(UserMetadata.Deprecation("service deprecation"), metadata.getDeprecation(service)) + val operation = service.operations.single() + assertEquals(UserMetadata.Deprecation("operation deprecation"), metadata.getDeprecation(operation)) + val parameter = operation.parameters.single() + assertEquals(UserMetadata.Deprecation("parameter deprecation"), metadata.getDeprecation(parameter)) + } + + @Test + fun `@Description without argument is an error`() { + val source = """ + package annotations + + @Description + enum UserType { + ADMIN, USER + } + + @Description + typealias Id = Long(1..*) + + @Description + record User { + @Description + id: Id + } + + @Description + service UserService { + @Description + get(@Description id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: Missing argument for @Description" } + ) + } + + @Test + fun `wrong argument type for @Description is an error`() { + val source = """ + package annotations + + @Description(1) + enum UserType { + ADMIN, USER + } + + @Description(true) + typealias Id = Long(1..*) + + @Description({}) + record User { + @Description(1) + id: Id + } + + @Description([]) + service UserService { + @Description(1.5) + get(@Description(String) id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: Argument for @Description must be a string" } + ) + } + + @Test + fun `wrong argument type for @Deprecated is an error`() { + val source = """ + package annotations + + @Deprecated(1) + enum UserType { + ADMIN, USER + } + + @Deprecated(true) + typealias Id = Long(1..*) + + @Deprecated({}) + record User { + @Deprecated(1) + id: Id + } + + @Deprecated([]) + service UserService { + @Deprecated(1.5) + get(@Deprecated(String) id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: Argument for @Deprecated must be a string" } + ) + } + + @Test + fun `extraneous arguments for @Description are an error`() { + val source = """ + package annotations + + @Description("enum description", "") + enum UserType { + ADMIN, USER + } + + @Description("typealias description", "") + typealias Id = Long(1..*) + + @Description("record description", "") + record User { + @Description("field description", "") + id: Id + } + + @Description("service description", "") + service UserService { + @Description("operation description", "") + get(@Description("parameter description", "") id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: @Description expects exactly one string argument" } + ) + } + + @Test + fun `extraneous arguments for @Deprecated are an error`() { + val source = """ + package annotations + + @Deprecated("enum deprecation", "") + enum UserType { + ADMIN, USER + } + + @Deprecated("typealias deprecation", "") + typealias Id = Long(1..*) + + @Deprecated("record deprecation", "") + record User { + @Deprecated("field deprecation", "") + id: Id + } + + @Deprecated("service deprecation", "") + service UserService { + @Deprecated("operation deprecation", "") + get(@Deprecated("parameter deprecation", "") id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: @Deprecated expects at most one string argument" } + ) + } + + @Test + fun `unknown annotations are an error`() { + val source = """ + package annotations + + @Deprescription + enum UserType { + ADMIN, USER + } + + @Deprescription + typealias Id = Long(1..*) + + @Deprescription + record User { + @Deprescription + id: Id + } + + @Deprescription + service UserService { + @Deprescription + get(@Deprescription id: Id): User + } + """.trimIndent() + parseAndCheck( + source to List(7) { "Error: Unknown annotation @Deprescription, allowed annotations are @Description and @Deprecated" } + ) + } + + @Test + fun `duplicate annotations are an error`() { + val deprecated = """ + package annotations + + @Deprecated + @Deprecated + enum UserType { + ADMIN, USER + } + + @Deprecated + @Deprecated + typealias Id = Long(1..*) + + @Deprecated + @Deprecated + record User { + @Deprecated + @Deprecated + id: Id + } + + @Deprecated + @Deprecated + service UserService { + @Deprecated + @Deprecated + get(@Deprecated @Deprecated id: Id): User + } + """.trimIndent() + parseAndCheck( + deprecated to List(7) { "Error: Duplicate @Deprecated annotation" } + ) + val description = """ + package annotations + + @Description("test") + @Description("test") + enum UserType { + ADMIN, USER + } + + @Description("test") + @Description("test") + typealias Id = Long(1..*) + + @Description("test") + @Description("test") + record User { + @Description("test") + @Description("test") + id: Id + } + + @Description("test") + @Description("test") + service UserService { + @Description("test") + @Description("test") + get(@Description("test") @Description("test") id: Id): User + } + """ + parseAndCheck( + description to List(7) { "Error: Duplicate @Description annotation" } + ) + } + } + private fun parseAndCheck( vararg sourceAndExpectedMessages: Pair>, - ) { + ): SemanticModel { val diagnosticController = DiagnosticController(URI("file:///tmp")) val fileTree = sourceAndExpectedMessages.mapIndexed { index, (source) -> val filePath = URI("file:///tmp/SemanticModelTest-${index}.samt") @@ -848,7 +1187,7 @@ class SemanticModelTest { val parseMessageCount = diagnosticController.contexts.associate { it.source.content to it.messages.size } - SemanticModelBuilder.build(fileTree, diagnosticController) + val semanticModel = SemanticModel.build(fileTree, diagnosticController) for ((source, expectedMessages) in sourceAndExpectedMessages) { val messages = diagnosticController.contexts @@ -858,5 +1197,6 @@ class SemanticModelTest { .map { "${it.severity}: ${it.message}" } assertEquals(expectedMessages, messages) } + return semanticModel } }