diff --git a/README.md b/README.md index a72105f7..0ce20aac 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Get started with SAMT, learn fundamental concepts or extend SAMT with a custom g - [Getting Started](https://github.com/samtkit/core/wiki/Getting-Started) - [Modeling Concepts](https://github.com/samtkit/core/wiki/Modeling-Concepts) +- [SAMT Template](https://github.com/samtkit/template) - [Visual Studio Code Plugin](https://marketplace.visualstudio.com/items?itemName=samt.samt) ### Advanced @@ -31,7 +32,8 @@ Get started with SAMT, learn fundamental concepts or extend SAMT with a custom g ## Development Setup -SAMT is written in [Kotlin](https://kotlinlang.org/) and uses [Gradle](https://gradle.org/) as a build tool, for the best developer experience we recommend using [IntelliJ](https://www.jetbrains.com/idea/). +SAMT is written in [Kotlin](https://kotlinlang.org/) and uses [Gradle](https://gradle.org/) as a build tool, +for the best developer experience we recommend using [IntelliJ IDEA](https://www.jetbrains.com/idea/). If you want to start SAMT locally, simply clone the repository and compile it using Gradle: @@ -39,23 +41,15 @@ If you want to start SAMT locally, simply clone the repository and compile it us ./gradlew assemble ``` -You can also compile the CLI module locally: +And then use this locally compiled SAMT to compile your SAMT files: ```shell -./gradlew :cli:shadowJar +java -jar ./cli/build/libs/samt-cli.jar compile ./specification/examples/todo-service/*.samt ``` -And then compile SAMT files using this locally compiled version: - -```shell -java -jar ./cli/build/libs/samt-cli.jar ./specification/examples/todo-service/*.samt -``` - -If you're more interested in the [SAMT Visual Studio Code plugin](https://github.com/samtkit/vscode) or the related language server, you can also compile it locally as well: - -```shell -./gradlew :language-server:shadowJar -``` +If you are interested in learning about the functionality and operation of the [SAMT Visual Studio Code Plugin](https://github.com/samtkit/vscode) +or methods for running and debugging the related language server on your local machine, +related documentation is available in the [SAMT VS Code Wiki](https://github.com/samtkit/vscode/wiki). ## Contributing diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index da1d582a..9b5f693d 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -7,7 +7,9 @@ kotlin { } dependencies { - implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:1.8.21") + val kotlin = "1.8.21" + implementation("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:$kotlin") + implementation("org.jetbrains.kotlin.plugin.serialization:org.jetbrains.kotlin.plugin.serialization.gradle.plugin:$kotlin") } repositories { diff --git a/buildSrc/src/main/kotlin/samt-core.kotlin-conventions.gradle.kts b/buildSrc/src/main/kotlin/samt-core.kotlin-conventions.gradle.kts index c7213f13..7bb7f2ae 100644 --- a/buildSrc/src/main/kotlin/samt-core.kotlin-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/samt-core.kotlin-conventions.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("jvm") + kotlin("plugin.serialization") apply false } apply(plugin = "kover") diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index a302b696..7253ab76 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -2,11 +2,13 @@ plugins { application id("samt-core.kotlin-conventions") alias(libs.plugins.shadow) + kotlin("plugin.serialization") } dependencies { implementation(libs.jCommander) implementation(libs.mordant) + implementation(libs.kotlinx.serialization.json) implementation(project(":common")) implementation(project(":lexer")) implementation(project(":parser")) diff --git a/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt b/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt index 77453cbc..6abffdfa 100644 --- a/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt +++ b/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt @@ -5,7 +5,7 @@ import com.github.ajalt.mordant.rendering.TextStyles.bold import com.github.ajalt.mordant.rendering.TextStyles.underline import tools.samt.parser.* -object ASTPrinter { +internal object ASTPrinter { fun dump(node: Node): String = buildString { val topLevelStyle = (red + bold + underline) val expressionStyle = (blue + bold) @@ -53,8 +53,6 @@ object ASTPrinter { firstLine = false } } - - appendLine() } private fun dumpInfo(node: Node): String? = when (node) { diff --git a/cli/src/main/kotlin/tools/samt/cli/App.kt b/cli/src/main/kotlin/tools/samt/cli/App.kt index e9a03b43..014b5bc3 100644 --- a/cli/src/main/kotlin/tools/samt/cli/App.kt +++ b/cli/src/main/kotlin/tools/samt/cli/App.kt @@ -1,17 +1,19 @@ package tools.samt.cli import com.beust.jcommander.JCommander - import com.github.ajalt.mordant.terminal.Terminal import tools.samt.common.DiagnosticController -import tools.samt.common.SourceFile -import tools.samt.semantic.SemanticModelBuilder -import java.io.File fun main(args: Array) { val cliArgs = CliArgs() + val compileCommand = CompileCommand() + val dumpCommand = DumpCommand() + val wrapperCommand = WrapperCommand() val jCommander = JCommander.newBuilder() .addObject(cliArgs) + .addCommand("compile", compileCommand) + .addCommand("dump", dumpCommand) + .addCommand("wrapper", wrapperCommand) .programName("./samtw") .build() jCommander.parse(*args) @@ -20,77 +22,30 @@ fun main(args: Array) { return } - val t = Terminal() + val terminal = Terminal() val workingDirectory = System.getProperty("user.dir") - val filePaths = cliArgs.files - - val startTimestamp = System.currentTimeMillis() - val diagnosticController = DiagnosticController(workingDirectory).also { controller -> - - // must specify at least one SAMT file - if (filePaths.isEmpty()) { - controller.reportGlobalError("No files specified") - return@also - } - - // attempt to load each source file - val sourceFiles = buildList { - for (path in filePaths) { - val file = File(path) - - if (!file.exists()) { - controller.reportGlobalError("File '$path' does not exist") - continue - } - - if (!file.canRead()) { - controller.reportGlobalError("File '$path' cannot be read, bad file permissions?") - continue - } - - if (file.extension != "samt") { - controller.reportGlobalError("File '$path' must end in .samt") - continue - } - - val source = file.readText() - add(SourceFile(file.canonicalPath, source)) - } - } - - // if any source files failed to load, exit - if (controller.hasErrors()) { - return@also - } - - // attempt to parse each source file into an AST - val fileNodes = buildList { - for (source in sourceFiles) { - val context = controller.createContext(source) - val fileNode = parseSourceFile(source, context) - if (fileNode != null) { - add(fileNode) - } - } - } - // if any source files failed to parse, exit - if (controller.hasErrors()) { - return@also - } + val controller = DiagnosticController(workingDirectory) - // if the user requested the AST to be dumped, do so - if (cliArgs.dumpAst) { - fileNodes.forEach { - t.print(ASTPrinter.dump(it)) - } + val startTimestamp = System.currentTimeMillis() + when (jCommander.parsedCommand) { + "compile" -> compile(compileCommand, controller) + "dump" -> dump(dumpCommand, terminal, controller) + "wrapper" -> wrapper(wrapperCommand, terminal, controller) + else -> { + jCommander.usage() + return } - - // build up the semantic model from the AST - SemanticModelBuilder.build(fileNodes, controller) } - val currentTimestamp = System.currentTimeMillis() - t.println(DiagnosticFormatter.format(diagnosticController, startTimestamp, currentTimestamp, terminalWidth = t.info.width)) + + terminal.println( + DiagnosticFormatter.format( + controller, + startTimestamp, + currentTimestamp, + terminalWidth = terminal.info.width + ) + ) } diff --git a/cli/src/main/kotlin/tools/samt/cli/CliArgs.kt b/cli/src/main/kotlin/tools/samt/cli/CliArgs.kt index 02dae6eb..75d11099 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliArgs.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliArgs.kt @@ -1,14 +1,45 @@ package tools.samt.cli import com.beust.jcommander.Parameter +import com.beust.jcommander.Parameters class CliArgs { @Parameter(names = ["-h", "--help"], description = "Display help", help = true) var help: Boolean = false +} + +@Parameters(commandDescription = "Compile SAMT files") +class CompileCommand { + @Parameter(description = "Files to compile, defaults to all .samt files in the current directory") + var files: List = mutableListOf() +} + +@Parameters(commandDescription = "Dump SAMT files in various formats for debugging purposes") +class DumpCommand { + @Parameter(names = ["--tokens"], description = "Dump a visual representation of the token stream") + var dumpTokens: Boolean = false - @Parameter(names = ["--dump-ast"], description = "Dump a visual representation of the AST", help = true) + @Parameter(names = ["--ast"], description = "Dump a visual representation of the AST") var dumpAst: Boolean = false - @Parameter(description = "Files") + @Parameter(names = ["--types"], description = "Dump a visual representation of the resolved types") + var dumpTypes: Boolean = false + + @Parameter(description = "Files to dump, defaults to all .samt files in the current directory") var files: List = mutableListOf() } + +@Parameters(commandDescription = "Initialize or update the SAMT wrapper") +class WrapperCommand { + @Parameter(names = ["--version"], description = "The SAMT version to use, defaults to the latest version published on GitHub") + var version: String = "latest" + + @Parameter(names = ["--version-source"], description = "The location from where the latest version will be fetched from, defaults to the GitHub API. The result must be a JSON object with a 'tag_name' field containing the version string") + var latestVersionSource: String = "https://api.github.com/repos/samtkit/core/releases/latest" + + @Parameter(names = ["--init"], description = "Downloads all required files and initializes the SAMT wrapper") + var init: Boolean = false + + @Parameter(names = ["--init-source"], description = "The location from where the initial 'samtw', 'samtw.bat' and 'samt-wrapper.properties' will be downloaded from") + var initSource: String = "https://raw.githubusercontent.com/samtkit/core/main/wrapper" +} diff --git a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt index 8d0a2f8e..74235f37 100644 --- a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt +++ b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt @@ -1,25 +1,46 @@ package tools.samt.cli -import tools.samt.common.DiagnosticContext +import tools.samt.common.DiagnosticController import tools.samt.common.DiagnosticException -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 -fun parseSourceFile(source: SourceFile, context: DiagnosticContext): FileNode? { - val tokenStream = Lexer.scan(source.content.reader(), context) +internal fun compile(command: CompileCommand, controller: DiagnosticController) { + val sourceFiles = command.files.readSamtSourceFiles(controller) - if (context.hasErrors()) { - return null + if (controller.hasErrors()) { + return } - val fileNode = try { - Parser.parse(source, tokenStream, context) - } catch (e: DiagnosticException) { - // error message is added to the diagnostic console, so it can be ignored here - return null + // attempt to parse each source file into an AST + val fileNodes = buildList { + for (source in sourceFiles) { + val context = controller.createContext(source) + val tokenStream = Lexer.scan(source.content.reader(), context) + + if (context.hasErrors()) { + continue + } + + val fileNode = try { + Parser.parse(source, tokenStream, context) + } catch (e: DiagnosticException) { + // error message is added to the diagnostic console, so it can be ignored here + continue + } + + add(fileNode) + } } - return fileNode -} + // if any source files failed to parse, exit + if (controller.hasErrors()) { + return + } + + // build up the semantic model from the AST + SemanticModelBuilder.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 new file mode 100644 index 00000000..25ead32c --- /dev/null +++ b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt @@ -0,0 +1,71 @@ +package tools.samt.cli + +import com.github.ajalt.mordant.terminal.Terminal +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 + +internal fun dump(command: DumpCommand, terminal: Terminal, controller: DiagnosticController) { + val sourceFiles = command.files.readSamtSourceFiles(controller) + + if (controller.hasErrors()) { + return + } + + val dumpAll = !command.dumpTokens && !command.dumpAst && !command.dumpTypes + + // attempt to parse each source file into an AST + val fileNodes = buildList { + for (source in sourceFiles) { + val context = controller.createContext(source) + + if (dumpAll || command.dumpTokens) { + // create duplicate scan because sequence can only be iterated once + val tokenStream = Lexer.scan(source.content.reader(), context) + terminal.println("Tokens for ${source.absolutePath}:") + terminal.println(TokenPrinter.dump(tokenStream)) + // clear the diagnostic messages so that messages aren't duplicated + context.messages.clear() + } + + val tokenStream = Lexer.scan(source.content.reader(), context) + + if (context.hasErrors()) { + continue + } + + val fileNode = try { + Parser.parse(source, tokenStream, context) + } catch (e: DiagnosticException) { + // error message is added to the diagnostic console, so it can be ignored here + continue + } + + if (dumpAll || command.dumpAst) { + terminal.println(ASTPrinter.dump(fileNode)) + } + + if (context.hasErrors()) { + continue + } + + add(fileNode) + } + } + + // if any source files failed to parse, exit + if (controller.hasErrors()) { + return + } + + // build up the semantic model from the AST + SemanticModelBuilder.build(fileNodes, controller) + + if (dumpAll || command.dumpTypes) { + terminal.println("Types:") + terminal.println("Not yet implemented") + // Type dumper will be added here + } +} diff --git a/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt new file mode 100644 index 00000000..e4dae8ab --- /dev/null +++ b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt @@ -0,0 +1,35 @@ +package tools.samt.cli + +import tools.samt.common.DiagnosticController +import tools.samt.common.SourceFile +import java.io.File + +internal fun List.readSamtSourceFiles(controller: DiagnosticController): List { + val files = map { File(it) }.ifEmpty { gatherSamtFiles(controller.workingDirectoryAbsolutePath) } + + return buildList { + for (file in files) { + if (!file.exists()) { + controller.reportGlobalError("File '${file.canonicalPath}' does not exist") + continue + } + + if (!file.canRead()) { + controller.reportGlobalError("File '${file.canonicalPath}' cannot be read, bad file permissions?") + continue + } + + if (file.extension != "samt") { + controller.reportGlobalError("File '${file.canonicalPath}' must end in .samt") + continue + } + + add(SourceFile(file.canonicalPath, content = file.readText())) + } + } +} + +internal fun gatherSamtFiles(directory: String): List { + val dir = File(directory) + return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList() +} diff --git a/cli/src/main/kotlin/tools/samt/cli/CliWrapper.kt b/cli/src/main/kotlin/tools/samt/cli/CliWrapper.kt new file mode 100644 index 00000000..342179e0 --- /dev/null +++ b/cli/src/main/kotlin/tools/samt/cli/CliWrapper.kt @@ -0,0 +1,110 @@ +package tools.samt.cli + +import com.github.ajalt.mordant.terminal.Terminal +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import tools.samt.common.DiagnosticController +import java.io.File +import java.net.URL + +internal fun wrapper(command: WrapperCommand, terminal: Terminal, controller: DiagnosticController) { + val workingDirectory = File(controller.workingDirectoryAbsolutePath) + val dotSamtDirectory = File(workingDirectory, ".samt") + + if (command.init) { + terminal.println("Initializing the SAMT wrapper in this directory...") + dotSamtDirectory.mkdirs() + + fun downloadFile(file: String, targetDirectory: File, executable: Boolean) { + try { + val fileName = if (command.initSource.endsWith("/")) { + "${command.initSource}$file" + } else { + "${command.initSource}/$file" + } + + val targetFile = File(targetDirectory, file) + val fileExisted = targetFile.exists() + + URL(fileName).openStream().use { input -> + targetFile.outputStream().use { output -> + input.copyTo(output) + } + } + if (executable) { + targetFile.setExecutable(true) + } + + if (fileExisted) { + controller.reportGlobalWarning("Downloaded $file from $fileName, overwriting existing file") + } else { + controller.reportGlobalInfo("Downloaded $file from $fileName") + } + } catch (e: Exception) { + controller.reportGlobalError("Failed to download $file from ${command.initSource} (exception: $e)") + } + } + + downloadFile("samtw", workingDirectory, executable = true) + downloadFile("samtw.bat", workingDirectory, executable = true) + downloadFile("samt-wrapper.properties", dotSamtDirectory, executable = false) + + controller.reportGlobalInfo("The SAMT wrapper has been initialized in this directory") + } + + val wrapperPropertiesFile = File(dotSamtDirectory, "samt-wrapper.properties") + + if (!wrapperPropertiesFile.exists()) { + controller.reportGlobalError("The SAMT wrapper has not been initialized in this directory, run './samtw wrapper --init' to initialize it") + return + } + + terminal.println("Checking for SAMT wrapper update...") + val newVersion = when (command.version) { + "latest" -> { + try { + URL(command.latestVersionSource).openStream().use { input -> + val response = json.decodeFromString(input.reader().readText()) + response.tagName + } + } catch (e: Exception) { + controller.reportGlobalError("Failed to query the latest version of the SAMT wrapper from ${command.latestVersionSource} (exception: $e)") + return + } + } + + else -> command.version + } + + val wrapperProperties = wrapperPropertiesFile.readText() + + var currentVersion: String? = null + for (line in wrapperProperties.lineSequence()) { + if (line.startsWith("samtVersion=")) { + currentVersion = line.substringAfter("=").trim() + } + } + + if (currentVersion == null) { + controller.reportGlobalError("Failed to parse the current version of the SAMT wrapper from ${wrapperPropertiesFile.absolutePath}") + return + } + + if (currentVersion == newVersion) { + controller.reportGlobalInfo("The SAMT wrapper is already up-to-date") + return + } + + wrapperPropertiesFile.writeText(wrapperProperties.replace(currentVersion, newVersion)) + controller.reportGlobalInfo("The SAMT wrapper has been updated from $currentVersion to $newVersion") +} + +private val json = Json { ignoreUnknownKeys = true } + +@Serializable +internal data class LatestSamtVersionResponse( + @SerialName("tag_name") + val tagName: String, +) diff --git a/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt b/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt index 4cc6feed..8e0b2067 100644 --- a/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt +++ b/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt @@ -7,7 +7,7 @@ import com.github.ajalt.mordant.terminal.Terminal import tools.samt.common.* import java.io.File -class DiagnosticFormatter( +internal class DiagnosticFormatter( private val diagnosticController: DiagnosticController, private val startTimestamp: Long, private val currentTimestamp: Long, @@ -75,9 +75,9 @@ class DiagnosticFormatter( val duration = currentTimestamp - startTimestamp appendLine("─".repeat(terminalWidth)) if (errorCount == 0) { - append((green + bold)("BUILD SUCCESSFUL") + " in ${duration}ms") + append((green + bold)("SUCCESSFUL") + " in ${duration}ms") } else { - append((red + bold)("BUILD FAILED") + " in ${duration}ms") + append((red + bold)("FAILED") + " in ${duration}ms") } if (errorCount > 0 || warningCount > 0) { diff --git a/cli/src/main/kotlin/tools/samt/cli/TokenPrinter.kt b/cli/src/main/kotlin/tools/samt/cli/TokenPrinter.kt new file mode 100644 index 00000000..58aa9b18 --- /dev/null +++ b/cli/src/main/kotlin/tools/samt/cli/TokenPrinter.kt @@ -0,0 +1,27 @@ +package tools.samt.cli + +import com.github.ajalt.mordant.rendering.TextColors.* +import tools.samt.lexer.* + +internal object TokenPrinter { + fun dump(tokens: Sequence): String = buildString { + var currentRow = 0 + for (token in tokens) { + if (token.location.start.row != currentRow) { + appendLine() + currentRow = token.location.start.row + } + val color = when (token) { + is ValueToken -> green + is StructureToken -> yellow + is StaticToken -> blue + is EndOfFileToken -> gray + } + append(color(token.getHumanReadableName())) + append(' ') + if (token is EndOfFileToken) { + break + } + } + } +} diff --git a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt index 17f968f8..641c7419 100644 --- a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt @@ -1,6 +1,5 @@ package tools.samt.cli -import tools.samt.common.DiagnosticContext import tools.samt.common.DiagnosticController import tools.samt.common.SourceFile import tools.samt.lexer.Lexer diff --git a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt index edb2b230..e8b2596f 100644 --- a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt +++ b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt @@ -34,7 +34,7 @@ class DiagnosticFormatterTest { INFO: This is a global info ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 1 warning(s)) + FAILED in 0ms (1 error(s), 1 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -76,7 +76,7 @@ class DiagnosticFormatterTest { ---> test.txt ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 1 warning(s)) + FAILED in 0ms (1 error(s), 1 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -112,7 +112,7 @@ class DiagnosticFormatterTest { |> 4 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -151,7 +151,7 @@ class DiagnosticFormatterTest { │ ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -200,7 +200,7 @@ class DiagnosticFormatterTest { 7 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -263,7 +263,7 @@ class DiagnosticFormatterTest { 14 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -309,7 +309,7 @@ class DiagnosticFormatterTest { 4 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -354,7 +354,7 @@ class DiagnosticFormatterTest { = help: help annotation ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -392,7 +392,7 @@ class DiagnosticFormatterTest { 4 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -432,7 +432,7 @@ class DiagnosticFormatterTest { 4 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } @@ -475,7 +475,7 @@ class DiagnosticFormatterTest { 4 │ } ──────────────────────────────────────── - BUILD FAILED in 0ms (1 error(s), 0 warning(s)) + FAILED in 0ms (1 error(s), 0 warning(s)) """.trimIndent().trim(), outputWithoutColors.trimIndent().trim()) } diff --git a/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt new file mode 100644 index 00000000..9496f0c8 --- /dev/null +++ b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt @@ -0,0 +1,57 @@ +package tools.samt.cli + +import tools.samt.common.DiagnosticController +import tools.samt.common.SourceFile +import tools.samt.lexer.Lexer +import tools.samt.lexer.Token +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse + +class TokenPrinterTest { + @Test + fun `correctly formats a token dump`() { + val tokenStream = parse(""" + import foo.bar.baz.* + + package test.stuff + + record A { + name: String(10..20, pattern("hehe")) + age: Int(0..150) + } + + record B {} + + service MyService { + testmethod(foo: A): B + } + """.trimIndent()) + + val dump = TokenPrinter.dump(tokenStream) + val dumpWithoutColorCodes = dump.replace(Regex("\u001B\\[[;\\d]*m"), "") + + assertEquals(""" + import foo . bar . baz . * + package test . stuff + record A { + name : String ( 10 .. 20 , pattern ( "hehe" ) ) + age : Int ( 0 .. 150 ) + } + record B { } + service MyService { + testmethod ( foo : A ) : B + } EOF + """.trimIndent().trim(), dumpWithoutColorCodes.trimIndent().trim()) + } + + private fun parse(source: String): Sequence { + val filePath = "/tmp/TokenPrinterTest.samt" + val sourceFile = SourceFile(filePath, source) + val diagnosticController = DiagnosticController("/tmp") + val diagnosticContext = diagnosticController.createContext(sourceFile) + val stream = Lexer.scan(source.reader(), diagnosticContext) + assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors") + return stream + } +} diff --git a/common/src/main/kotlin/tools/samt/common/Diagnostics.kt b/common/src/main/kotlin/tools/samt/common/Diagnostics.kt index 6272d52e..18f5f465 100644 --- a/common/src/main/kotlin/tools/samt/common/Diagnostics.kt +++ b/common/src/main/kotlin/tools/samt/common/Diagnostics.kt @@ -34,10 +34,10 @@ data class SourceFile( // the content of the source file as a string val content: String, - +) { // each line of the source file - val sourceLines: List = content.lines(), -) + val sourceLines: List = content.lines() +} class DiagnosticException(message: DiagnosticMessage) : RuntimeException(message.message) diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b31616e..8858a6d0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,14 +7,18 @@ include(":semantic") dependencyResolutionManagement { versionCatalogs { + val kotlin = "1.8.21" val shadow = "8.1.1" val jCommander = "1.82" val mordant = "2.0.0-beta12" + val kotlinxSerialization = "1.5.0" val kover = "0.6.1" create("libs") { + version("kotlin", kotlin) library("jCommander", "com.beust", "jcommander").version(jCommander) library("mordant", "com.github.ajalt.mordant", "mordant").version(mordant) + library("kotlinx.serialization-json", "org.jetbrains.kotlinx", "kotlinx-serialization-json").version(kotlinxSerialization) plugin("shadow", "com.github.johnrengelman.shadow").version(shadow) plugin("kover", "org.jetbrains.kotlinx.kover").version(kover) diff --git a/wrapper/samtw b/wrapper/samtw index ddf27724..119ccccf 100755 --- a/wrapper/samtw +++ b/wrapper/samtw @@ -1,35 +1,30 @@ #!/usr/bin/env sh -if [ ! -f ./samt-wrapper.properties ]; then - echo "samt-wrapper.properties file not found." >&2 +if [ ! -f .samt/samt-wrapper.properties ]; then + echo ".samt/samt-wrapper.properties file not found." >&2 exit 1 fi -. ./samt-wrapper.properties +. .samt/samt-wrapper.properties if ! command -v tar > /dev/null; then echo "This script requires 'tar' to be installed." >&2 exit 1 fi -mkdir -p .samt/cli +mkdir -p .samt/wrapper -if [ ! -f .samt/.gitignore ]; then - echo "*" > .samt/.gitignore -fi - -currentVersion=$(cat .samt/cli/version.txt 2> /dev/null || echo "0.0.0") +currentVersion=$(cat .samt/wrapper/version.txt 2> /dev/null || echo "0.0.0") if [ "$currentVersion" != "$samtVersion" ]; then echo "Downloading samt $samtVersion from '$distributionUrl'..." if command -v curl > /dev/null; then - if ! curl -s -L "$distributionUrl" | tar x -C .samt/cli; then + if ! curl -s -L "$distributionUrl" | tar x -C .samt/wrapper; then echo "An error occurred while downloading '$distributionUrl' archive using curl." >&2 exit 1 fi - echo "$samtVersion" > .samt/cli/version.txt elif command -v curl > /dev/null; then - if ! wget -qO- "$distributionUrl" | tar x -C .samt/cli; then + if ! wget -qO- "$distributionUrl" | tar x -C .samt/wrapper; then echo "An error occurred while downloading '$distributionUrl' archive using wget." >&2 exit 1 fi @@ -37,6 +32,7 @@ if [ "$currentVersion" != "$samtVersion" ]; then echo "samtw requires either 'curl' or 'wget' to be installed." >&2 exit 1 fi + echo "$samtVersion" > .samt/wrapper/version.txt fi -exec ".samt/cli/cli-shadow/bin/cli" "$@" +exec ".samt/wrapper/cli-shadow/bin/cli" "$@" diff --git a/wrapper/samtw.bat b/wrapper/samtw.bat index 5830d87b..6d17aadf 100644 --- a/wrapper/samtw.bat +++ b/wrapper/samtw.bat @@ -2,12 +2,12 @@ setlocal EnableDelayedExpansion -if not exist samt-wrapper.properties ( - echo "samt-wrapper.properties not found." +if not exist .samt\samt-wrapper.properties ( + echo ".samt\samt-wrapper.properties not found." exit /b 1 ) -for /f "tokens=1,2 delims==" %%G in (samt-wrapper.properties) do ( +for /f "tokens=1,2 delims==" %%G in (.samt\samt-wrapper.properties) do ( if "%%G"=="samtVersion" ( set "samtVersion=%%H" ) @@ -24,36 +24,35 @@ where tar >nul || ( exit /b 1 ) -if not exist .samt\cli mkdir .samt\cli - -if not exist .samt\.gitignore echo *> .samt\.gitignore +if not exist .samt\wrapper mkdir .samt\wrapper set "currentVersion=0.0.0" -if exist .samt\cli\version.txt ( - set /p currentVersion=<.samt\cli\version.txt +if exist .samt\wrapper\version.txt ( + set /p currentVersion=<.samt\wrapper\version.txt ) if "%currentVersion%" neq "%samtVersion%" ( echo Downloading samt %samtVersion% from '%distributionUrl%'... WHERE /q curl - if %ERRORLEVEL% EQU 0 ( - curl -L -o .samt\cli\cli.tar "%distributionUrl%" || ( - echo An error occurred while downloading '%distributionUrl%' archive using curl. >&2 - exit /b 1 - ) - - ) else ( + if %ERRORLEVEL% NEQ 0 ( echo samtw requires 'curl' to be installed. >&2 exit /b 1 ) - tar xf .samt\cli\cli.tar -C .samt\cli || ( - echo An error occurred while unpacking .samt\cli\cli.tar using tar. >&2 + curl -L -o .samt\wrapper\cli.tar "%distributionUrl%" || ( + echo An error occurred while downloading '%distributionUrl%' archive using curl. >&2 exit /b 1 ) - echo %samtVersion%> .samt\cli\version.txt + tar xf .samt\wrapper\cli.tar -C .samt\wrapper || ( + echo An error occurred while unpacking .samt\wrapper\cli.tar using tar. >&2 + exit /b 1 + ) + + del .samt\wrapper\cli.tar + + echo %samtVersion%> .samt\wrapper\version.txt ) -call ".samt\cli\cli-shadow\bin\cli.bat" %* +call ".samt\wrapper\cli-shadow\bin\cli.bat" %*