diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index 62268c83..cb0d7d49 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -19,7 +19,7 @@ jobs:
uses: gradle/gradle-build-action@v2
- name: Build CLI
- run: ./gradlew --no-daemon :cli:shadowDistZip :cli:shadowDistTar
+ run: ./gradlew --no-daemon :cli:shadowDistZip :cli:shadowDistTar :language-server:shadowJar
- name: Rename cli-shadow to cli
run: |
@@ -32,4 +32,5 @@ jobs:
files: |
cli/build/distributions/cli.zip
cli/build/distributions/cli.tar
+ language-server/build/libs/samt-ls.jar
fail_on_unmatched_files: true
diff --git a/.idea/runConfigurations/Language_Server_Debug.xml b/.idea/runConfigurations/Language_Server_Debug.xml
new file mode 100644
index 00000000..b86218de
--- /dev/null
+++ b/.idea/runConfigurations/Language_Server_Debug.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt b/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt
index 6abffdfa..aac32b37 100644
--- a/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/ASTPrinter.kt
@@ -56,7 +56,7 @@ internal object ASTPrinter {
}
private fun dumpInfo(node: Node): String? = when (node) {
- is FileNode -> gray(node.sourceFile.absolutePath)
+ is FileNode -> gray(node.sourceFile.path.path)
is RequestResponseOperationNode -> if (node.isAsync) red("async") else null
is IdentifierNode -> yellow(node.name)
is ImportBundleIdentifierNode -> yellow(node.name) + if (node.isWildcard) yellow(".*") else ""
diff --git a/cli/src/main/kotlin/tools/samt/cli/App.kt b/cli/src/main/kotlin/tools/samt/cli/App.kt
index 014b5bc3..182558dc 100644
--- a/cli/src/main/kotlin/tools/samt/cli/App.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/App.kt
@@ -3,6 +3,7 @@ package tools.samt.cli
import com.beust.jcommander.JCommander
import com.github.ajalt.mordant.terminal.Terminal
import tools.samt.common.DiagnosticController
+import kotlin.io.path.Path
fun main(args: Array) {
val cliArgs = CliArgs()
@@ -24,7 +25,7 @@ fun main(args: Array) {
val terminal = Terminal()
- val workingDirectory = System.getProperty("user.dir")
+ val workingDirectory = Path(System.getProperty("user.dir")).toUri()
val controller = DiagnosticController(workingDirectory)
diff --git a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt
index 74235f37..da1ec894 100644
--- a/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt
@@ -16,7 +16,7 @@ internal fun compile(command: CompileCommand, controller: DiagnosticController)
// attempt to parse each source file into an AST
val fileNodes = buildList {
for (source in sourceFiles) {
- val context = controller.createContext(source)
+ val context = controller.getOrCreateContext(source)
val tokenStream = Lexer.scan(source.content.reader(), context)
if (context.hasErrors()) {
diff --git a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt
index 25ead32c..068930e6 100644
--- a/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/CliDumper.kt
@@ -19,12 +19,12 @@ internal fun dump(command: DumpCommand, terminal: Terminal, controller: Diagnost
// attempt to parse each source file into an AST
val fileNodes = buildList {
for (source in sourceFiles) {
- val context = controller.createContext(source)
+ val context = controller.getOrCreateContext(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("Tokens for ${source.path}:")
terminal.println(TokenPrinter.dump(tokenStream))
// clear the diagnostic messages so that messages aren't duplicated
context.messages.clear()
diff --git a/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt
index e4dae8ab..4a959abe 100644
--- a/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt
@@ -2,34 +2,11 @@ package tools.samt.cli
import tools.samt.common.DiagnosticController
import tools.samt.common.SourceFile
+import tools.samt.common.collectSamtFiles
+import tools.samt.common.readSamtSource
import java.io.File
-internal fun List.readSamtSourceFiles(controller: DiagnosticController): List {
- val files = map { File(it) }.ifEmpty { gatherSamtFiles(controller.workingDirectoryAbsolutePath) }
+internal fun List.readSamtSourceFiles(controller: DiagnosticController): List =
+ map { File(it) }.ifEmpty { collectSamtFiles(controller.workingDirectory) }
+ .readSamtSource(controller)
- 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
index 342179e0..a0f018d0 100644
--- a/cli/src/main/kotlin/tools/samt/cli/CliWrapper.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/CliWrapper.kt
@@ -10,7 +10,7 @@ import java.io.File
import java.net.URL
internal fun wrapper(command: WrapperCommand, terminal: Terminal, controller: DiagnosticController) {
- val workingDirectory = File(controller.workingDirectoryAbsolutePath)
+ val workingDirectory = File(controller.workingDirectory)
val dotSamtDirectory = File(workingDirectory, ".samt")
if (command.init) {
diff --git a/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt b/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt
index 8e0b2067..57e05bcd 100644
--- a/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt
+++ b/cli/src/main/kotlin/tools/samt/cli/DiagnosticFormatter.kt
@@ -5,7 +5,6 @@ import com.github.ajalt.mordant.rendering.TextColors.*
import com.github.ajalt.mordant.rendering.TextStyles.*
import com.github.ajalt.mordant.terminal.Terminal
import tools.samt.common.*
-import java.io.File
internal class DiagnosticFormatter(
private val diagnosticController: DiagnosticController,
@@ -119,11 +118,6 @@ internal class DiagnosticFormatter(
DiagnosticSeverity.Info -> formatTextForSeverity("INFO:", severity, withBold = true)
}
- private fun formatFilePathRelativeToWorkingDirectory(filePath: String): String {
- val workingDirectory = diagnosticController.workingDirectoryAbsolutePath
- return filePath.removePrefix(workingDirectory).removePrefix(File.separator)
- }
-
private fun formatGlobalMessage(message: DiagnosticGlobalMessage): String = buildString {
append(formatSeverityIndicator(message.severity))
append(" ")
@@ -140,14 +134,14 @@ internal class DiagnosticFormatter(
appendLine()
val errorSourceFilePath = if (message.highlights.isNotEmpty()) {
- message.highlights.first().location.source.absolutePath
+ message.highlights.first().location.source.path
} else {
- context.source.absolutePath
+ context.source.path
}
// -----> :
append(gray(" ---> "))
- append(formatFilePathRelativeToWorkingDirectory(errorSourceFilePath))
+ append(diagnosticController.workingDirectory.relativize(errorSourceFilePath))
if (message.highlights.isNotEmpty()) {
val firstHighlight = message.highlights.first()
val firstHighlightLocation = firstHighlight.location
@@ -173,8 +167,8 @@ internal class DiagnosticFormatter(
require(highlights.isNotEmpty())
// group highlights by source file
- val mainSourceFileAbsolutePath = highlights.first().location.source.absolutePath
- val highlightsBySourceFile = highlights.groupBy { it.location.source.absolutePath }
+ val mainSourceFileAbsolutePath = highlights.first().location.source.path
+ val highlightsBySourceFile = highlights.groupBy { it.location.source.path }
// print the highlights for the main source file
val mainSourceFileHighlights = highlightsBySourceFile.getValue(mainSourceFileAbsolutePath)
diff --git a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt
index 641c7419..11e7d86f 100644
--- a/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt
+++ b/cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt
@@ -5,6 +5,7 @@ import tools.samt.common.SourceFile
import tools.samt.lexer.Lexer
import tools.samt.parser.FileNode
import tools.samt.parser.Parser
+import java.net.URI
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -14,6 +15,7 @@ class ASTPrinterTest {
fun `correctly formats an AST dump`() {
val fileNode = parse("""
import foo.bar.baz.*
+ import foo.bar.baz.A as B
package test.stuff
@@ -22,8 +24,9 @@ class ASTPrinterTest {
age: Integer(0..150)
}
- record B {}
+ enum E { A, B, C }
+ @Description("This is a service")
service MyService {
testmethod(foo: A): B
}
@@ -39,52 +42,65 @@ class ASTPrinterTest {
│ ├─IdentifierNode foo <1:8>
│ ├─IdentifierNode bar <1:12>
│ └─IdentifierNode baz <1:16>
- ├─PackageDeclarationNode <3:1>
- │ └─BundleIdentifierNode test.stuff <3:9>
- │ ├─IdentifierNode test <3:9>
- │ └─IdentifierNode stuff <3:14>
- ├─RecordDeclarationNode <5:1>
- │ ├─IdentifierNode A <5:8>
- │ ├─RecordFieldNode <6:3>
- │ │ ├─IdentifierNode name <6:3>
- │ │ └─CallExpressionNode <6:9>
- │ │ ├─BundleIdentifierNode String <6:9>
- │ │ │ └─IdentifierNode String <6:9>
- │ │ ├─RangeExpressionNode <6:16>
- │ │ │ ├─IntegerNode 10 <6:16>
- │ │ │ └─IntegerNode 20 <6:20>
- │ │ └─CallExpressionNode <6:24>
- │ │ ├─BundleIdentifierNode pattern <6:24>
- │ │ │ └─IdentifierNode pattern <6:24>
- │ │ └─StringNode "hehe" <6:32>
- │ └─RecordFieldNode <7:3>
- │ ├─IdentifierNode age <7:3>
- │ └─CallExpressionNode <7:8>
- │ ├─BundleIdentifierNode Integer <7:8>
- │ │ └─IdentifierNode Integer <7:8>
- │ └─RangeExpressionNode <7:16>
- │ ├─IntegerNode 0 <7:16>
- │ └─IntegerNode 150 <7:19>
- ├─RecordDeclarationNode <10:1>
- │ └─IdentifierNode B <10:8>
- └─ServiceDeclarationNode <12:1>
- ├─IdentifierNode MyService <12:9>
- └─RequestResponseOperationNode <13:3>
- ├─IdentifierNode testmethod <13:3>
- ├─OperationParameterNode <13:14>
- │ ├─IdentifierNode foo <13:14>
- │ └─BundleIdentifierNode A <13:19>
- │ └─IdentifierNode A <13:19>
- └─BundleIdentifierNode B <13:23>
- └─IdentifierNode B <13:23>
+ ├─TypeImportNode <2:1>
+ │ ├─ImportBundleIdentifierNode foo.bar.baz.A <2:8>
+ │ │ ├─IdentifierNode foo <2:8>
+ │ │ ├─IdentifierNode bar <2:12>
+ │ │ ├─IdentifierNode baz <2:16>
+ │ │ └─IdentifierNode A <2:20>
+ │ └─IdentifierNode B <2:25>
+ ├─PackageDeclarationNode <4:1>
+ │ └─BundleIdentifierNode test.stuff <4:9>
+ │ ├─IdentifierNode test <4:9>
+ │ └─IdentifierNode stuff <4:14>
+ ├─RecordDeclarationNode <6:1>
+ │ ├─IdentifierNode A <6:8>
+ │ ├─RecordFieldNode <7:3>
+ │ │ ├─IdentifierNode name <7:3>
+ │ │ └─CallExpressionNode <7:9>
+ │ │ ├─BundleIdentifierNode String <7:9>
+ │ │ │ └─IdentifierNode String <7:9>
+ │ │ ├─RangeExpressionNode <7:16>
+ │ │ │ ├─IntegerNode 10 <7:16>
+ │ │ │ └─IntegerNode 20 <7:20>
+ │ │ └─CallExpressionNode <7:24>
+ │ │ ├─BundleIdentifierNode pattern <7:24>
+ │ │ │ └─IdentifierNode pattern <7:24>
+ │ │ └─StringNode "hehe" <7:32>
+ │ └─RecordFieldNode <8:3>
+ │ ├─IdentifierNode age <8:3>
+ │ └─CallExpressionNode <8:8>
+ │ ├─BundleIdentifierNode Integer <8:8>
+ │ │ └─IdentifierNode Integer <8:8>
+ │ └─RangeExpressionNode <8:16>
+ │ ├─IntegerNode 0 <8:16>
+ │ └─IntegerNode 150 <8:19>
+ ├─EnumDeclarationNode <11:1>
+ │ ├─IdentifierNode E <11:6>
+ │ ├─IdentifierNode A <11:10>
+ │ ├─IdentifierNode B <11:13>
+ │ └─IdentifierNode C <11:16>
+ └─ServiceDeclarationNode <14:1>
+ ├─IdentifierNode MyService <14:9>
+ ├─RequestResponseOperationNode <15:3>
+ │ ├─IdentifierNode testmethod <15:3>
+ │ ├─OperationParameterNode <15:14>
+ │ │ ├─IdentifierNode foo <15:14>
+ │ │ └─BundleIdentifierNode A <15:19>
+ │ │ └─IdentifierNode A <15:19>
+ │ └─BundleIdentifierNode B <15:23>
+ │ └─IdentifierNode B <15:23>
+ └─AnnotationNode <13:1>
+ ├─IdentifierNode Description <13:2>
+ └─StringNode "This is a service" <13:14>
""".trimIndent().trim(), dumpWithoutColorCodes.trimIndent().trim())
}
private fun parse(source: String): FileNode {
- val filePath = "/tmp/ASTPrinterTest.samt"
+ val filePath = URI("file:///tmp/ASTPrinterTest.samt")
val sourceFile = SourceFile(filePath, source)
- val diagnosticController = DiagnosticController("/tmp")
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
val fileTree = Parser.parse(sourceFile, stream, diagnosticContext)
assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors")
diff --git a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt
index e8b2596f..d3363f45 100644
--- a/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt
+++ b/cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt
@@ -7,8 +7,8 @@ import tools.samt.lexer.Lexer
import tools.samt.parser.EnumDeclarationNode
import tools.samt.parser.FileNode
import tools.samt.parser.Parser
+import java.net.URI
import kotlin.io.path.Path
-import kotlin.io.path.absolutePathString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -16,7 +16,7 @@ import kotlin.test.assertFalse
class DiagnosticFormatterTest {
@Test
fun `global messages`() {
- val controller = DiagnosticController("/tmp")
+ val controller = DiagnosticController(URI("file:///tmp"))
controller.reportGlobalError("This is a global error")
controller.reportGlobalWarning("This is a global warning")
controller.reportGlobalInfo("This is a global info")
@@ -40,12 +40,12 @@ class DiagnosticFormatterTest {
@Test
fun `file messages with no highlights`() {
- val baseDirectory = Path("/tmp").absolutePathString()
- val filePath = Path("/tmp", "test.txt").absolutePathString()
+ val baseDirectory = Path("/tmp").toUri()
+ val filePath = Path("/tmp", "test.txt").toUri()
val controller = DiagnosticController(baseDirectory)
val source = ""
val sourceFile = SourceFile(filePath, source)
- val context = controller.createContext(sourceFile)
+ val context = controller.getOrCreateContext(sourceFile)
context.error {
message("some error")
@@ -480,11 +480,11 @@ class DiagnosticFormatterTest {
}
private fun parse(source: String): Triple {
- val baseDirectory = Path("/tmp").absolutePathString()
- val filePath = Path("/tmp", "DiagnosticFormatterTest.samt").absolutePathString()
+ val baseDirectory = Path("/tmp").toUri()
+ val filePath = Path("/tmp", "DiagnosticFormatterTest.samt").toUri()
val sourceFile = SourceFile(filePath, source)
val diagnosticController = DiagnosticController(baseDirectory)
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
val fileTree = Parser.parse(sourceFile, stream, diagnosticContext)
assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors")
diff --git a/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt
index 9496f0c8..76193085 100644
--- a/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt
+++ b/cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt
@@ -4,6 +4,7 @@ import tools.samt.common.DiagnosticController
import tools.samt.common.SourceFile
import tools.samt.lexer.Lexer
import tools.samt.lexer.Token
+import java.net.URI
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -46,10 +47,10 @@ class TokenPrinterTest {
}
private fun parse(source: String): Sequence {
- val filePath = "/tmp/TokenPrinterTest.samt"
+ val filePath = URI("file:///tmp/TokenPrinterTest.samt")
val sourceFile = SourceFile(filePath, source)
- val diagnosticController = DiagnosticController("/tmp")
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val diagnosticContext = diagnosticController.getOrCreateContext(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 18f5f465..d212d295 100644
--- a/common/src/main/kotlin/tools/samt/common/Diagnostics.kt
+++ b/common/src/main/kotlin/tools/samt/common/Diagnostics.kt
@@ -1,5 +1,7 @@
package tools.samt.common
+import java.net.URI
+
enum class DiagnosticSeverity {
Error, Warning, Info
}
@@ -29,19 +31,19 @@ data class DiagnosticHighlight(
)
data class SourceFile(
- // absolute path to the source file
- val absolutePath: String,
+ /** Absolute path to the source file */
+ val path: URI,
- // the content of the source file as a string
+ /** Content of the source file as a string */
val content: String,
) {
- // each line of the source file
+ /** Each line of the source file */
val sourceLines: List = content.lines()
}
class DiagnosticException(message: DiagnosticMessage) : RuntimeException(message.message)
-class DiagnosticController(val workingDirectoryAbsolutePath: String) {
+class DiagnosticController(val workingDirectory: URI) {
/**
* All diagnostic contexts, one for each source file.
@@ -56,7 +58,7 @@ class DiagnosticController(val workingDirectoryAbsolutePath: String) {
/**
* Creates a new diagnostic context for the given source file or returns already existing one.
* */
- fun createContext(source: SourceFile): DiagnosticContext {
+ fun getOrCreateContext(source: SourceFile): DiagnosticContext {
val foundContext = contexts.find { it.source == source}
if (foundContext != null) return foundContext
return DiagnosticContext(source).also { contexts.add(it) }
@@ -69,10 +71,10 @@ class DiagnosticController(val workingDirectoryAbsolutePath: String) {
globalMessages.add(DiagnosticGlobalMessage(severity, message))
}
- fun hasMessages(): Boolean = contexts.any { it.hasMessages() } or globalMessages.isNotEmpty()
- fun hasErrors(): Boolean = contexts.any { it.hasErrors() } or globalMessages.any { it.severity == DiagnosticSeverity.Error }
- fun hasWarnings(): Boolean = contexts.any { it.hasWarnings() } or globalMessages.any { it.severity == DiagnosticSeverity.Warning }
- fun hasInfos(): Boolean = contexts.any { it.hasInfos() } or globalMessages.any { it.severity == DiagnosticSeverity.Info }
+ fun hasMessages(): Boolean = contexts.any { it.hasMessages() } || globalMessages.isNotEmpty()
+ fun hasErrors(): Boolean = contexts.any { it.hasErrors() } || globalMessages.any { it.severity == DiagnosticSeverity.Error }
+ fun hasWarnings(): Boolean = contexts.any { it.hasWarnings() } || globalMessages.any { it.severity == DiagnosticSeverity.Warning }
+ fun hasInfos(): Boolean = contexts.any { it.hasInfos() } || globalMessages.any { it.severity == DiagnosticSeverity.Info }
}
class DiagnosticContext(
@@ -153,7 +155,6 @@ class DiagnosticMessageBuilder(
fun build(): DiagnosticMessage {
requireNotNull(message)
- highlights.sortWith(compareBy({ it.location.start.row }, { it.location.start.col }))
return DiagnosticMessage(severity, message!!, highlights, annotations)
}
}
diff --git a/common/src/main/kotlin/tools/samt/common/Files.kt b/common/src/main/kotlin/tools/samt/common/Files.kt
new file mode 100644
index 00000000..4a638100
--- /dev/null
+++ b/common/src/main/kotlin/tools/samt/common/Files.kt
@@ -0,0 +1,38 @@
+package tools.samt.common
+
+import java.io.File
+import java.net.URI
+
+fun List.readSamtSource(controller: DiagnosticController): List {
+ return buildList {
+ for (file in this@readSamtSource) {
+
+ if (!file.exists()) {
+ controller.reportGlobalError("File '${file.path}' does not exist")
+ continue
+ }
+
+ if (!file.canRead()) {
+ controller.reportGlobalError("File '${file.path}' cannot be read, bad file permissions?")
+ continue
+ }
+
+ if (file.extension != "samt") {
+ controller.reportGlobalError("File '${file.path}' must end in .samt")
+ continue
+ }
+
+ if (!file.isFile) {
+ controller.reportGlobalError("'${file.path}' is not a file")
+ continue
+ }
+
+ add(SourceFile(file.toPath().toUri(), content = file.readText()))
+ }
+ }
+}
+
+fun collectSamtFiles(directory: URI): List {
+ val dir = File(directory)
+ return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList()
+}
diff --git a/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt b/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt
index b879964f..b1e61642 100644
--- a/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt
+++ b/common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt
@@ -1,13 +1,14 @@
package tools.samt.common
import org.junit.jupiter.api.assertThrows
+import java.net.URI
import kotlin.test.*
class DiagnosticsTest {
@Test
fun `global messages`() {
- val controller = DiagnosticController("/tmp")
+ val controller = DiagnosticController(URI("file:///tmp"))
controller.reportGlobalError("some error")
controller.reportGlobalWarning("some warning")
controller.reportGlobalInfo("some information")
@@ -29,14 +30,14 @@ class DiagnosticsTest {
@Test
fun `fatal error messages`() {
- val controller = DiagnosticController("/tmp")
- val sourcePath = "/tmp/sourceFile"
+ val controller = DiagnosticController(URI("file:///tmp"))
+ val sourcePath = URI("file:///tmp/sourceFile")
val sourceCode = """
import foo as bar
package debug
""".trimIndent()
val sourceFile = SourceFile(sourcePath, sourceCode)
- val context = controller.createContext(sourceFile)
+ val context = controller.getOrCreateContext(sourceFile)
assertThrows("some fatal error") {
context.fatal {
@@ -58,14 +59,14 @@ class DiagnosticsTest {
@Test
fun `message highlights and annotations`() {
- val controller = DiagnosticController("/tmp")
- val sourcePath = "/tmp/sourceFile"
+ val controller = DiagnosticController(URI("file:///tmp"))
+ val sourcePath = URI("file:///tmp/sourceFile")
val sourceCode = """
import foo as bar
package debug
""".trimIndent().trim()
val sourceFile = SourceFile(sourcePath, sourceCode)
- val context = controller.createContext(sourceFile)
+ val context = controller.getOrCreateContext(sourceFile)
val importStatementStartOffset = FileOffset(0, 0, 0)
val importStatementEndOffset = FileOffset(17, 0, 17)
diff --git a/common/src/test/kotlin/tools/samt/common/FilesTest.kt b/common/src/test/kotlin/tools/samt/common/FilesTest.kt
new file mode 100644
index 00000000..80900dfb
--- /dev/null
+++ b/common/src/test/kotlin/tools/samt/common/FilesTest.kt
@@ -0,0 +1,47 @@
+package tools.samt.common
+
+import kotlin.io.path.Path
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+
+class FilesTest {
+ private val testDirectory = Path("src/test/resources/test-files")
+ @BeforeTest
+ fun setup() {
+ assertTrue(testDirectory.exists() && testDirectory.isDirectory(), "Test directory does not exist")
+ }
+
+ @Test
+ fun `collectSamtFiles only returns files with samt extension`() {
+ val samtFiles = collectSamtFiles(testDirectory.toUri())
+
+ val relativeFilePaths = samtFiles.map { testDirectory.toUri().relativize(it.toPath().toUri()).toString() }.sorted()
+ assertEquals(listOf("foo.samt", "src/bar.samt", "src/baz.samt"), relativeFilePaths)
+ }
+ @Test
+ fun `readSamtSource only returns files with samt extension`() {
+ val files = listOf(
+ testDirectory.resolve("foo.samt").toFile(),
+ testDirectory.resolve("dummy.samt.txt").toFile(),
+ testDirectory.resolve("src/bar.samt").toFile(),
+ testDirectory.resolve("src/baz.samt").toFile(),
+ testDirectory.resolve("src/missing.samt").toFile(),
+ testDirectory.resolve(".samt").toFile(),
+ )
+ val controller = DiagnosticController(testDirectory.toUri())
+ val samtFiles = files.readSamtSource(controller)
+
+ val relativeFilePaths = samtFiles.map { testDirectory.toUri().relativize(it.path).toString() }.sorted()
+ assertEquals(listOf("foo.samt", "src/bar.samt", "src/baz.samt"), relativeFilePaths)
+
+ assertEquals(listOf(
+ "File '${testDirectory.resolve("dummy.samt.txt").toFile().path}' must end in .samt",
+ "File '${testDirectory.resolve("src/missing.samt").toFile().path}' does not exist",
+ "'${testDirectory.resolve(".samt").toFile().path}' is not a file",
+ ), controller.globalMessages.filter { it.severity == DiagnosticSeverity.Error }.map { it.message })
+ }
+}
diff --git a/common/src/test/kotlin/tools/samt/common/LocationTest.kt b/common/src/test/kotlin/tools/samt/common/LocationTest.kt
index ccaddb13..abec4de5 100644
--- a/common/src/test/kotlin/tools/samt/common/LocationTest.kt
+++ b/common/src/test/kotlin/tools/samt/common/LocationTest.kt
@@ -2,11 +2,12 @@ package tools.samt.common
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
+import java.net.URI
import kotlin.test.*
class LocationTest {
private val dummySource = SourceFile(
- "locations.samt", """
+ URI("file://locations.samt"), """
import foo
import bar
diff --git a/common/src/test/resources/test-files/.samt/.gitkeep b/common/src/test/resources/test-files/.samt/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/common/src/test/resources/test-files/dummy.samt.txt b/common/src/test/resources/test-files/dummy.samt.txt
new file mode 100644
index 00000000..e69de29b
diff --git a/common/src/test/resources/test-files/foo.samt b/common/src/test/resources/test-files/foo.samt
new file mode 100644
index 00000000..e69de29b
diff --git a/common/src/test/resources/test-files/src/bar.samt b/common/src/test/resources/test-files/src/bar.samt
new file mode 100644
index 00000000..e69de29b
diff --git a/common/src/test/resources/test-files/src/baz.samt b/common/src/test/resources/test-files/src/baz.samt
new file mode 100644
index 00000000..e69de29b
diff --git a/language-server/build.gradle.kts b/language-server/build.gradle.kts
new file mode 100644
index 00000000..c2ec680f
--- /dev/null
+++ b/language-server/build.gradle.kts
@@ -0,0 +1,29 @@
+plugins {
+ application
+ id("samt-core.kotlin-conventions")
+ alias(libs.plugins.shadow)
+}
+
+dependencies {
+ implementation(libs.jCommander)
+ implementation(libs.lsp4j)
+ implementation(project(":common"))
+ implementation(project(":lexer"))
+ implementation(project(":parser"))
+ implementation(project(":semantic"))
+}
+
+application {
+ // Define the main class for the application.
+ mainClass.set("tools.samt.ls.AppKt")
+}
+
+tasks {
+ shadowJar {
+ archiveBaseName.set("samt-ls")
+ archiveClassifier.set("")
+ manifest {
+ attributes("Main-Class" to "tools.samt.ls.AppKt")
+ }
+ }
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/App.kt b/language-server/src/main/kotlin/tools/samt/ls/App.kt
new file mode 100644
index 00000000..72d11a85
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/App.kt
@@ -0,0 +1,47 @@
+package tools.samt.ls
+
+import com.beust.jcommander.JCommander
+import org.eclipse.lsp4j.launch.LSPLauncher
+import java.io.InputStream
+import java.io.OutputStream
+import java.io.PrintWriter
+import java.net.Socket
+
+
+private fun startServer(inStream: InputStream, outStream: OutputStream, trace: PrintWriter? = null) {
+ SamtLanguageServer().use { server ->
+ val launcher = LSPLauncher.createServerLauncher(server, inStream, outStream, false, trace)
+ val client = launcher.remoteProxy
+ redirectLogs(client)
+ server.connect(client)
+ launcher.startListening().get()
+ }
+}
+
+fun main(args: Array) {
+ val cliArgs = CliArgs()
+ val jCommander = JCommander.newBuilder()
+ .addObject(cliArgs)
+ .programName("java -jar samt-ls.jar")
+ .build()
+ jCommander.parse(*args)
+ if (cliArgs.help) {
+ jCommander.usage()
+ return
+ }
+
+ cliArgs.clientPort?.also { port ->
+ Socket(cliArgs.clientHost, port).use {
+ println("Connecting to client at ${it.remoteSocketAddress}")
+ startServer(it.inputStream, it.outputStream, PrintWriter(System.out))
+ }
+ return
+ }
+
+ if (cliArgs.isStdio) {
+ startServer(System.`in`, System.out)
+ return
+ }
+
+ jCommander.usage()
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/CliArgs.kt b/language-server/src/main/kotlin/tools/samt/ls/CliArgs.kt
new file mode 100644
index 00000000..9abe4cea
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/CliArgs.kt
@@ -0,0 +1,22 @@
+package tools.samt.ls
+
+import com.beust.jcommander.Parameter
+import com.beust.jcommander.Parameters
+
+@Parameters(separators = "=")
+class CliArgs {
+ @Parameter(names = ["-h", "--help"], description = "Display help", help = true)
+ var help: Boolean = false
+
+ @Parameter(names = ["--client-host"])
+ var clientHost = "localhost"
+
+ /**
+ * Option is called Socket because that's what VS Code passes
+ */
+ @Parameter(names = ["--socket", "--client-port"])
+ var clientPort: Int? = null
+
+ @Parameter(names = ["--stdio"])
+ var isStdio: Boolean = false
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt b/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt
new file mode 100644
index 00000000..293866d8
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt
@@ -0,0 +1,35 @@
+package tools.samt.ls
+
+import tools.samt.common.DiagnosticContext
+import tools.samt.common.DiagnosticException
+import tools.samt.common.SourceFile
+import tools.samt.lexer.Lexer
+import tools.samt.lexer.Token
+import tools.samt.parser.FileNode
+import tools.samt.parser.Parser
+
+class FileInfo(
+ val diagnosticContext: DiagnosticContext,
+ val sourceFile: SourceFile,
+ @Suppress("unused") val tokens: List,
+ val fileNode: FileNode? = null,
+)
+
+fun parseFile(sourceFile: SourceFile): FileInfo {
+ val diagnosticContext = DiagnosticContext(sourceFile)
+
+ val tokens = Lexer.scan(sourceFile.content.reader(), diagnosticContext).toList()
+
+ if (diagnosticContext.hasErrors()) {
+ return FileInfo(diagnosticContext, sourceFile, tokens)
+ }
+
+ val fileNode = try {
+ Parser.parse(sourceFile, tokens.asSequence(), diagnosticContext)
+ } catch (e: DiagnosticException) {
+ // error message is added to the diagnostic console, so it can be ignored here
+ return FileInfo(diagnosticContext, sourceFile, tokens)
+ }
+
+ return FileInfo(diagnosticContext, sourceFile, tokens, fileNode)
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/Logging.kt b/language-server/src/main/kotlin/tools/samt/ls/Logging.kt
new file mode 100644
index 00000000..3c87b92f
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/Logging.kt
@@ -0,0 +1,40 @@
+package tools.samt.ls
+
+import org.eclipse.lsp4j.MessageParams
+import org.eclipse.lsp4j.MessageType
+import org.eclipse.lsp4j.services.LanguageClient
+import java.util.logging.Handler
+import java.util.logging.Level
+import java.util.logging.LogRecord
+import java.util.logging.Logger
+
+fun redirectLogs(client: LanguageClient) {
+ fun Level.toMessageType(): MessageType = when (this) {
+ Level.SEVERE -> MessageType.Error
+ Level.WARNING -> MessageType.Warning
+ Level.INFO -> MessageType.Info
+ Level.CONFIG -> MessageType.Log
+ Level.FINE -> MessageType.Log
+ Level.FINER -> MessageType.Log
+ Level.FINEST -> MessageType.Log
+ else -> MessageType.Log
+ }
+ fun LogRecord.toMessageParams(): MessageParams = MessageParams(
+ level.toMessageType(),
+ message
+ )
+
+
+ val rootLogger = Logger.getLogger("")
+ rootLogger.addHandler(object : Handler() {
+ override fun publish(record: LogRecord) {
+ client.logMessage(record.toMessageParams())
+ }
+
+ override fun flush() {
+ }
+
+ override fun close() {
+ }
+ })
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt b/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt
new file mode 100644
index 00000000..710a3507
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/Mapping.kt
@@ -0,0 +1,33 @@
+package tools.samt.ls
+
+import org.eclipse.lsp4j.*
+import tools.samt.common.DiagnosticMessage
+import tools.samt.common.DiagnosticSeverity
+import tools.samt.common.Location as SamtLocation
+
+fun DiagnosticMessage.toDiagnostic(): Diagnostic? {
+ val diagnostic = Diagnostic()
+ val primaryHighlight = this.highlights.firstOrNull() ?: return null
+ diagnostic.range = primaryHighlight.location.toRange()
+ diagnostic.severity = when (severity) {
+ DiagnosticSeverity.Error -> org.eclipse.lsp4j.DiagnosticSeverity.Error
+ DiagnosticSeverity.Warning -> org.eclipse.lsp4j.DiagnosticSeverity.Warning
+ DiagnosticSeverity.Info -> org.eclipse.lsp4j.DiagnosticSeverity.Information
+ }
+ diagnostic.source = "samt"
+ diagnostic.message = message
+ diagnostic.relatedInformation = highlights.filter { it.message != null }.map {
+ DiagnosticRelatedInformation(
+ Location(it.location.source.path.toString(), it.location.toRange()),
+ it.message
+ )
+ }
+ return diagnostic
+}
+
+fun SamtLocation.toRange(): Range {
+ return Range(
+ Position(start.row, start.col),
+ Position(start.row, end.col)
+ )
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt
new file mode 100644
index 00000000..ce0d194a
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/SamtLanguageServer.kt
@@ -0,0 +1,79 @@
+package tools.samt.ls
+
+import org.eclipse.lsp4j.*
+import org.eclipse.lsp4j.services.*
+import tools.samt.common.*
+import java.io.Closeable
+import java.net.URI
+import java.util.concurrent.CompletableFuture
+import java.util.logging.Logger
+import kotlin.system.exitProcess
+
+class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
+ private lateinit var client: LanguageClient
+ private val logger = Logger.getLogger("SamtLanguageServer")
+ private val workspaces = mutableMapOf()
+ private val textDocumentService = SamtTextDocumentService(workspaces)
+
+ override fun initialize(params: InitializeParams): CompletableFuture =
+ CompletableFuture.supplyAsync {
+ buildSamtModel(params)
+ val capabilities = ServerCapabilities().apply {
+ setTextDocumentSync(TextDocumentSyncKind.Full)
+ }
+ InitializeResult(capabilities)
+ }
+
+ override fun initialized(params: InitializedParams) {
+ pushDiagnostics()
+ }
+
+ override fun shutdown(): CompletableFuture = CompletableFuture.completedFuture(null)
+
+ override fun exit() {
+ exitProcess(0)
+ }
+
+ override fun getTextDocumentService(): TextDocumentService = textDocumentService
+
+ override fun getWorkspaceService(): WorkspaceService? = null
+
+ override fun connect(client: LanguageClient) {
+ this.client = client
+ textDocumentService.connect(client)
+ logger.info("Connected to client")
+ }
+
+ override fun close() {
+ shutdown().get()
+ }
+
+ private fun buildSamtModel(params: InitializeParams) {
+ val folders = params.workspaceFolders.map { it.uri.toPathUri() }
+ for (folder in folders) {
+ // if the folder is contained within another folder ignore it
+ if (folders.any { folder != it && folder.path.startsWith(it.path) }) continue
+ workspaces[folder] = buildWorkspace(folder)
+ }
+ }
+
+ private fun buildWorkspace(workspacePath: URI): SamtWorkspace {
+ val diagnosticController = DiagnosticController(workspacePath)
+ val sourceFiles = collectSamtFiles(workspacePath).readSamtSource(diagnosticController)
+ val workspace = SamtWorkspace(diagnosticController)
+ sourceFiles.asSequence().map(::parseFile).forEach(workspace::add)
+ workspace.buildSemanticModel()
+ return workspace
+ }
+
+ private fun pushDiagnostics() {
+ workspaces.values.flatMap { workspace ->
+ workspace.getAllMessages().map { (path, messages) ->
+ PublishDiagnosticsParams(
+ path.toString(),
+ messages.map { it.toDiagnostic() }
+ )
+ }
+ }.forEach(client::publishDiagnostics)
+ }
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt
new file mode 100644
index 00000000..ed11a423
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/SamtTextDocumentService.kt
@@ -0,0 +1,55 @@
+package tools.samt.ls
+
+import org.eclipse.lsp4j.*
+import org.eclipse.lsp4j.services.LanguageClient
+import org.eclipse.lsp4j.services.LanguageClientAware
+import org.eclipse.lsp4j.services.TextDocumentService
+import tools.samt.common.SourceFile
+import java.net.URI
+import java.util.logging.Logger
+
+class SamtTextDocumentService(private val workspaces: Map) : TextDocumentService,
+ LanguageClientAware {
+ private lateinit var client: LanguageClient
+ private val logger = Logger.getLogger("SamtTextDocumentService")
+
+ override fun didOpen(params: DidOpenTextDocumentParams) {
+ logger.info("Opened document ${params.textDocument.uri}")
+ }
+
+ override fun didChange(params: DidChangeTextDocumentParams) {
+ logger.info("Changed document ${params.textDocument.uri}")
+
+ val path = params.textDocument.uri.toPathUri()
+ val newText = params.contentChanges.single().text
+ val fileInfo = parseFile(SourceFile(path, newText))
+ val workspace = getWorkspace(path)
+
+ workspace.add(fileInfo)
+ workspace.buildSemanticModel()
+ workspace.getAllMessages().forEach { (path, messages) ->
+ client.publishDiagnostics(
+ PublishDiagnosticsParams(
+ path.toString(),
+ messages.map { it.toDiagnostic() },
+ params.textDocument.version
+ )
+ )
+ }
+ }
+
+ override fun didClose(params: DidCloseTextDocumentParams) {
+ logger.info("Closed document ${params.textDocument.uri}")
+ }
+
+ override fun didSave(params: DidSaveTextDocumentParams) {
+ logger.info("Saved document ${params.textDocument.uri}")
+ }
+
+ override fun connect(client: LanguageClient) {
+ this.client = client
+ }
+
+ private fun getWorkspace(filePath: URI): SamtWorkspace =
+ workspaces.values.first { filePath in it }
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt
new file mode 100644
index 00000000..8c03d857
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/SamtWorkspace.kt
@@ -0,0 +1,39 @@
+package tools.samt.ls
+
+import tools.samt.common.DiagnosticController
+import tools.samt.common.DiagnosticMessage
+import tools.samt.semantic.Package
+import tools.samt.semantic.SemanticModelBuilder
+import java.net.URI
+
+class SamtWorkspace(private val parserController: DiagnosticController) : Iterable {
+ private val files = mutableMapOf()
+ private var samtPackage: Package? = null
+ private var semanticController: DiagnosticController =
+ DiagnosticController(parserController.workingDirectory)
+
+ fun add(fileInfo: FileInfo) {
+ files[fileInfo.sourceFile.path] = fileInfo
+ }
+
+ operator fun get(path: URI): FileInfo? = files[path]
+
+ override fun iterator(): Iterator = files.values.iterator()
+
+ operator fun contains(path: URI) = path in files
+
+ fun buildSemanticModel() {
+ semanticController = DiagnosticController(parserController.workingDirectory)
+ samtPackage = SemanticModelBuilder.build(mapNotNull { it.fileNode }, semanticController)
+ }
+
+ private fun getMessages(path: URI): List {
+ val fileInfo = files[path] ?: return emptyList()
+ return fileInfo.diagnosticContext.messages +
+ semanticController.getOrCreateContext(fileInfo.sourceFile).messages
+ }
+
+ fun getAllMessages() = files.keys.associateWith {
+ getMessages(it)
+ }
+}
diff --git a/language-server/src/main/kotlin/tools/samt/ls/Uri.kt b/language-server/src/main/kotlin/tools/samt/ls/Uri.kt
new file mode 100644
index 00000000..e00398c3
--- /dev/null
+++ b/language-server/src/main/kotlin/tools/samt/ls/Uri.kt
@@ -0,0 +1,6 @@
+package tools.samt.ls
+
+import java.net.URI
+import kotlin.io.path.toPath
+
+fun String.toPathUri(): URI = URI(this).toPath().toUri()
diff --git a/language-server/src/test/kotlin/tools/samt/ls/UriTest.kt b/language-server/src/test/kotlin/tools/samt/ls/UriTest.kt
new file mode 100644
index 00000000..7f6929d7
--- /dev/null
+++ b/language-server/src/test/kotlin/tools/samt/ls/UriTest.kt
@@ -0,0 +1,11 @@
+package tools.samt.ls
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class UriTest {
+ @Test
+ fun `correctly transforms encoded URIs`() {
+ assertEquals("file:///c:/test/directory", "file:///c%3A/test/directory".toPathUri().toString())
+ }
+}
diff --git a/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt b/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt
index 0f5431df..8e935698 100644
--- a/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt
+++ b/lexer/src/main/kotlin/tools/samt/lexer/Lexer.kt
@@ -39,9 +39,7 @@ class Lexer private constructor(
skipBlanks()
resetStartPosition()
}
- while (true) {
- yield(EndOfFileToken(windowLocation()))
- }
+ yield(EndOfFileToken(windowLocation()))
}
private fun readToken(): Token? = when {
@@ -81,7 +79,7 @@ class Lexer private constructor(
}
}
- private inline fun readStructureToken(factory: (location: Location) -> T): StructureToken {
+ private inline fun readStructureToken(factory: (location: Location) -> T): StructureToken {
readNext()
return factory(windowLocation())
}
@@ -283,6 +281,7 @@ class Lexer private constructor(
skipLineComment()
return null
}
+
'*' -> {
skipCommentBlock()
return null
@@ -319,7 +318,13 @@ class Lexer private constructor(
if (current == '*') {
readNext()
nestedCommentDepth++
- commentOpenerPositionStack.add(Location(diagnostic.source, currentCharacterPosition, currentPosition))
+ commentOpenerPositionStack.add(
+ Location(
+ diagnostic.source,
+ currentCharacterPosition,
+ currentPosition
+ )
+ )
}
}
@@ -404,24 +409,24 @@ class Lexer private constructor(
companion object {
val KEYWORDS: Map StaticToken> = mapOf(
- "record" to { RecordToken(it) },
- "enum" to { EnumToken(it) },
- "service" to { ServiceToken(it) },
- "alias" to { AliasToken(it) },
- "package" to { PackageToken(it) },
- "import" to { ImportToken(it) },
- "provide" to { ProvideToken(it) },
- "consume" to { ConsumeToken(it) },
- "transport" to { TransportToken(it) },
- "implements" to { ImplementsToken(it) },
- "uses" to { UsesToken(it) },
- "extends" to { ExtendsToken(it) },
- "as" to { AsToken(it) },
- "async" to { AsyncToken(it) },
- "oneway" to { OnewayToken(it) },
- "raises" to { RaisesToken(it) },
- "true" to { TrueToken(it) },
- "false" to { FalseToken(it) },
+ "record" to { RecordToken(it) },
+ "enum" to { EnumToken(it) },
+ "service" to { ServiceToken(it) },
+ "alias" to { AliasToken(it) },
+ "package" to { PackageToken(it) },
+ "import" to { ImportToken(it) },
+ "provide" to { ProvideToken(it) },
+ "consume" to { ConsumeToken(it) },
+ "transport" to { TransportToken(it) },
+ "implements" to { ImplementsToken(it) },
+ "uses" to { UsesToken(it) },
+ "extends" to { ExtendsToken(it) },
+ "as" to { AsToken(it) },
+ "async" to { AsyncToken(it) },
+ "oneway" to { OnewayToken(it) },
+ "raises" to { RaisesToken(it) },
+ "true" to { TrueToken(it) },
+ "false" to { FalseToken(it) },
)
fun scan(reader: Reader, diagnostics: DiagnosticContext): Sequence {
diff --git a/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt b/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt
index 5684115b..6bca5998 100644
--- a/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt
+++ b/lexer/src/test/kotlin/tools/samt/lexer/LexerTest.kt
@@ -2,10 +2,11 @@ package tools.samt.lexer
import org.junit.jupiter.api.assertThrows
import tools.samt.common.*
+import java.net.URI
import kotlin.test.*
class LexerTest {
- private var diagnosticController = DiagnosticController("/tmp")
+ private var diagnosticController = DiagnosticController(URI("file:///tmp"))
@Test
fun `comment only file`() {
@@ -368,8 +369,8 @@ SAMT!""", stream.next()
}
private fun readTokenStream(source: String): Pair, DiagnosticContext> {
- val sourceFile = SourceFile("/tmp/test", source)
- val context = diagnosticController.createContext(sourceFile)
+ val sourceFile = SourceFile(URI("file:///tmp/test"), source)
+ val context = diagnosticController.getOrCreateContext(sourceFile)
return Pair(Lexer.scan(source.reader(), context).iterator(), context)
}
diff --git a/parser/src/main/kotlin/tools/samt/parser/Nodes.kt b/parser/src/main/kotlin/tools/samt/parser/Nodes.kt
index fb7447b4..ba8d9c76 100644
--- a/parser/src/main/kotlin/tools/samt/parser/Nodes.kt
+++ b/parser/src/main/kotlin/tools/samt/parser/Nodes.kt
@@ -24,18 +24,19 @@ sealed class NamedDeclarationNode(
sealed class ImportNode(
location: Location,
+ val name: BundleIdentifierNode,
) : StatementNode(location)
class TypeImportNode(
location: Location,
- val name: BundleIdentifierNode,
+ name: BundleIdentifierNode,
val alias: IdentifierNode?,
-) : ImportNode(location)
+) : ImportNode(location, name)
class WildcardImportNode(
location: Location,
- val name: BundleIdentifierNode,
-) : ImportNode(location)
+ name: BundleIdentifierNode,
+) : ImportNode(location, name)
class PackageDeclarationNode(
location: Location,
diff --git a/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt b/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt
index a5602abc..f19d2d5f 100644
--- a/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt
+++ b/parser/src/test/kotlin/tools/samt/parser/ParserTest.kt
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.assertThrows
import tools.samt.common.*
import tools.samt.lexer.Lexer
+import java.net.URI
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -819,10 +820,10 @@ class ParserTest {
}
private fun parse(source: String): FileNode {
- val filePath = "/tmp/ParserTest.samt"
+ val filePath = URI("file:///tmp/ParserTest.samt")
val sourceFile = SourceFile(filePath, source)
- val diagnosticController = DiagnosticController("/tmp")
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
val fileTree = Parser.parse(sourceFile, stream, diagnosticContext)
assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors")
@@ -830,10 +831,10 @@ class ParserTest {
}
private fun parseWithRecoverableError(source: String): Pair {
- val filePath = "/tmp/ParserTest.samt"
+ val filePath = URI("file:///tmp/ParserTest.samt")
val sourceFile = SourceFile(filePath, source)
- val diagnosticController = DiagnosticController("/tmp")
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
val fileTree = Parser.parse(sourceFile, stream, diagnosticContext)
assertTrue(diagnosticContext.hasErrors(), "Expected errors, but had no errors")
@@ -841,10 +842,10 @@ class ParserTest {
}
private fun parseWithFatalError(source: String): DiagnosticException {
- val filePath = "/tmp/ParserTest.samt"
+ val filePath = URI("file:///tmp/ParserTest.samt")
val sourceFile = SourceFile(filePath, source)
- val diagnosticController = DiagnosticController("/tmp")
- val diagnosticContext = diagnosticController.createContext(sourceFile)
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
val exception = assertThrows { Parser.parse(sourceFile, stream, diagnosticContext) }
assertTrue(diagnosticContext.hasErrors(), "Expected errors, but had no errors")
diff --git a/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt b/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt
index 66300a3e..87e3c366 100644
--- a/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt
+++ b/semantic/src/main/kotlin/tools/samt/semantic/ConstraintBuilder.kt
@@ -4,12 +4,15 @@ import tools.samt.common.DiagnosticController
import tools.samt.parser.*
internal class ConstraintBuilder(private val controller: DiagnosticController) {
- private fun createRange(expression: RangeExpressionNode): ResolvedTypeReference.Constraint.Range? {
+ private fun createRange(
+ expression: ExpressionNode,
+ argument: RangeExpressionNode,
+ ): ResolvedTypeReference.Constraint.Range? {
fun resolveSide(expressionNode: ExpressionNode): Number? = when (expressionNode) {
is NumberNode -> expressionNode.value
is WildcardNode -> null
else -> {
- controller.createContext(expressionNode.location.source).error {
+ controller.getOrCreateContext(expressionNode.location.source).error {
message("Range constraint argument must be a valid number range")
highlight("neither a number nor '*'", expressionNode.location)
help("A valid constraint would be range(1..10.5) or range(1..*)")
@@ -18,13 +21,13 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
}
}
- val lower = resolveSide(expression.left)
- val higher = resolveSide(expression.right)
+ val lower = resolveSide(argument.left)
+ val higher = resolveSide(argument.right)
if (lower == null && higher == null) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(argument.location.source).error {
message("Range constraint must have at least one valid number")
- highlight("invalid constraint", expression.location)
+ highlight("invalid constraint", argument.location)
help("A valid constraint would be range(1..10.5) or range(1..*)")
}
return null
@@ -33,28 +36,31 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
if (lower is Double && higher is Double && lower > higher ||
lower is Long && higher is Long && lower > higher
) {
- controller.createContext(expression.location.source)
+ controller.getOrCreateContext(argument.location.source)
.error {
message("Range constraint must have a lower bound lower than the upper bound")
- highlight("invalid constraint", expression.location)
+ highlight("invalid constraint", argument.location)
}
return null
}
return ResolvedTypeReference.Constraint.Range(
- definition = expression,
+ node = expression,
lowerBound = lower,
upperBound = higher,
)
}
- private fun createSize(expression: RangeExpressionNode): ResolvedTypeReference.Constraint.Size? {
+ private fun createSize(
+ expression: ExpressionNode,
+ argument: RangeExpressionNode,
+ ): ResolvedTypeReference.Constraint.Size? {
fun resolveSide(expressionNode: ExpressionNode): Long? = when (expressionNode) {
is IntegerNode -> expressionNode.value
is WildcardNode -> null
else -> {
- controller.createContext(expressionNode.location.source).error {
+ controller.getOrCreateContext(expressionNode.location.source).error {
message("Expected size constraint argument to be a whole number or wildcard")
highlight("expected whole number or wildcard '*'", expressionNode.location)
help("A valid constraint would be size(1..10), size(1..*) or size(*..10)")
@@ -63,47 +69,53 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
}
}
- val lower = resolveSide(expression.left)
- val higher = resolveSide(expression.right)
+ val lower = resolveSide(argument.left)
+ val higher = resolveSide(argument.right)
if (lower == null && higher == null) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(argument.location.source).error {
message("Constraint parameters cannot both be wildcards")
- highlight("invalid constraint", expression.location)
+ highlight("invalid constraint", argument.location)
help("A valid constraint would be range(1..10.5) or range(1..*)")
}
return null
}
if (lower != null && higher != null && lower > higher) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(argument.location.source).error {
message("Size constraint lower bound must be lower than or equal to the upper bound")
- highlight("invalid constraint", expression.location)
+ highlight("invalid constraint", argument.location)
}
return null
}
return ResolvedTypeReference.Constraint.Size(
- definition = expression,
+ node = expression,
lowerBound = lower,
upperBound = higher,
)
}
- private fun createPattern(expression: StringNode): ResolvedTypeReference.Constraint.Pattern {
+ private fun createPattern(
+ expression: ExpressionNode,
+ argument: StringNode,
+ ): ResolvedTypeReference.Constraint.Pattern {
// We will validate the pattern here in the future
- return ResolvedTypeReference.Constraint.Pattern(expression, expression.value)
+ return ResolvedTypeReference.Constraint.Pattern(expression, argument.value)
}
- private fun createValue(expression: ExpressionNode): ResolvedTypeReference.Constraint.Value? {
- return when (expression) {
- is StringNode -> ResolvedTypeReference.Constraint.Value(expression, expression.value)
- is NumberNode -> ResolvedTypeReference.Constraint.Value(expression, expression.value)
- is BooleanNode -> ResolvedTypeReference.Constraint.Value(expression, expression.value)
+ private fun createValue(
+ expression: ExpressionNode,
+ argument: ExpressionNode,
+ ): ResolvedTypeReference.Constraint.Value? {
+ return when (argument) {
+ is StringNode -> ResolvedTypeReference.Constraint.Value(expression, argument.value)
+ is NumberNode -> ResolvedTypeReference.Constraint.Value(expression, argument.value)
+ is BooleanNode -> ResolvedTypeReference.Constraint.Value(expression, argument.value)
else -> {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(argument.location.source).error {
message("Value constraint must be a string, integer, float or boolean")
- highlight("invalid constraint", expression.location)
+ highlight("invalid constraint", argument.location)
help("A valid constraint would be value(\"foo\"), value(42) or value(false)")
}
null
@@ -124,53 +136,56 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
when (name) {
"range" -> {
if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is RangeExpressionNode) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Range constraint must have exactly one range argument")
highlight("invalid constraint", expression.location)
help("A valid constraint would be range(1..10.5)")
}
return null
}
- return createRange(expression.arguments.first() as RangeExpressionNode)
+ return createRange(expression, expression.arguments.first() as RangeExpressionNode)
}
"size" -> {
if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is RangeExpressionNode) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Size constraint must have exactly one size argument")
highlight("invalid constraint", expression.location)
help("A valid constraint would be size(1..10)")
}
return null
}
- return createSize(expression.arguments.first() as RangeExpressionNode)
+ return createSize(
+ expression = expression,
+ argument = expression.arguments.first() as RangeExpressionNode,
+ )
}
"pattern" -> {
if (expression.arguments.size != 1 || expression.arguments.firstOrNull() !is StringNode) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Pattern constraint must have exactly one string argument")
highlight("invalid constraint", expression.location)
help("A valid constraint would be pattern(\"a-z\")")
}
return null
}
- return createPattern(expression.arguments.first() as StringNode)
+ return createPattern(expression, expression.arguments.first() as StringNode)
}
"value" -> {
if (expression.arguments.size != 1) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("value constraint must have exactly one argument")
highlight("invalid constraint", expression.location)
}
return null
}
- return createValue(expression.arguments.first())
+ return createValue(expression, expression.arguments.first())
}
is String -> {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Constraint with name '${name}' does not exist")
highlight("unknown constraint", expression.base.location)
help("A valid constraint would be range(1..10.5), size(1..10), pattern(\"a-z\") or value(\"foo\")")
@@ -180,30 +195,32 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
}
}
// It might make sense to limit shorthand constraints to only the first argument
- is RangeExpressionNode -> return if (baseType is StringType || baseType is ListType || baseType is MapType) createSize(
- expression
- ) else createRange(expression)
+ is RangeExpressionNode -> return if (baseType is StringType || baseType is ListType || baseType is MapType) {
+ createSize(expression = expression, argument = expression)
+ } else {
+ createRange(expression = expression, argument = expression)
+ }
is NumberNode -> {
return if (expression is IntegerNode && (baseType is StringType || baseType is ListType || baseType is MapType)) {
ResolvedTypeReference.Constraint.Size(
- definition = expression,
+ node = expression,
lowerBound = null,
upperBound = expression.value
)
} else {
ResolvedTypeReference.Constraint.Range(
- definition = expression,
+ node = expression,
lowerBound = null,
upperBound = expression.value
)
}
}
- is StringNode -> return createPattern(expression)
+ is StringNode -> return createPattern(expression = expression, argument = expression)
else -> Unit
}
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Invalid constraint")
highlight("invalid constraint", expression.location)
}
@@ -230,7 +247,7 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
return if (validateConstraintMatches(constraint, baseType)) {
constraint
} else {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Constraint '${constraint.humanReadableName}' is not allowed for type '${baseType.humanReadableName}'")
highlight("illegal constraint", expression.location)
@@ -256,4 +273,4 @@ internal class ConstraintBuilder(private val controller: DiagnosticController) {
}
}
-}
\ No newline at end of file
+}
diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt
index d1020a83..8258ef55 100644
--- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt
+++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModel.kt
@@ -12,7 +12,7 @@ import tools.samt.parser.*
* - Resolve all references to types
* - Resolve all references to their declarations in the AST
* */
-class SemanticModelBuilder(
+class SemanticModelBuilder private constructor(
private val files: List,
private val controller: DiagnosticController,
) {
@@ -29,7 +29,7 @@ class SemanticModelBuilder(
block()
} else {
val existingType = parentPackage.types.getValue(statement.name.name)
- controller.createContext(statement.location.source).error {
+ controller.getOrCreateContext(statement.location.source).error {
message("'${statement.name.name}' is already declared")
highlight("duplicate declaration", statement.name.location)
if (existingType is UserDefinedType) {
@@ -49,7 +49,7 @@ class SemanticModelBuilder(
val name = identifierGetter(item).name
val existingLocation = existingItems.putIfAbsent(name, item.location)
if (existingLocation != null) {
- controller.createContext(item.location.source).error {
+ controller.getOrCreateContext(item.location.source).error {
message("$what '$name' is defined more than once")
highlight("duplicate declaration", identifierGetter(item).location)
highlight("previous declaration", existingLocation)
@@ -88,7 +88,7 @@ class SemanticModelBuilder(
ensureNameIsAvailable(parentPackage, statement) {
reportDuplicates(statement.fields, "Record field") { it.name }
if (statement.extends.isNotEmpty()) {
- controller.createContext(statement.location.source).error {
+ controller.getOrCreateContext(statement.location.source).error {
message("Record extends are not yet supported")
highlight("cannot extend other records", statement.extends.first().location)
}
@@ -129,7 +129,7 @@ class SemanticModelBuilder(
is RequestResponseOperationNode -> {
if (operation.isAsync) {
- controller.createContext(operation.location.source).error {
+ controller.getOrCreateContext(operation.location.source).error {
message("Async operations are not yet supported")
highlight("unsupported async operation", operation.location)
}
@@ -179,7 +179,7 @@ class SemanticModelBuilder(
}
is TypeAliasNode -> {
- controller.createContext(statement.location.source).error {
+ controller.getOrCreateContext(statement.location.source).error {
message("Type aliases are not yet supported")
highlight("unsupported feature", statement.location)
}
@@ -246,7 +246,7 @@ class SemanticModelBuilder(
}
null -> {
- controller.createContext(component.location.source).error {
+ controller.getOrCreateContext(component.location.source).error {
message("Could not resolve reference '${component.name}'")
highlight("unresolved reference", component.location)
}
@@ -257,7 +257,7 @@ class SemanticModelBuilder(
if (iterator.hasNext()) {
// We resolved a non-package type but there are still components left
- controller.createContext(component.location.source).error {
+ controller.getOrCreateContext(component.location.source).error {
message("Type '${component.name}' is not a package, cannot access sub-types")
highlight("must be a package", component.location)
}
@@ -285,7 +285,7 @@ class SemanticModelBuilder(
file.imports.forEach { import ->
fun addImportedType(name: String, type: Type) {
putIfAbsent(name, type)?.let { existingType ->
- controller.createContext(file.sourceFile).error {
+ controller.getOrCreateContext(file.sourceFile).error {
message("Import '$name' conflicts with locally defined type with same name")
highlight("conflicting import", import.location)
if (existingType is UserDefinedType) {
@@ -318,7 +318,7 @@ class SemanticModelBuilder(
addImportedType(name, type)
}
} else {
- controller.createContext(file.sourceFile).error {
+ controller.getOrCreateContext(file.sourceFile).error {
message("Import '${import.name.name}.*' must point to a package and not a type")
highlight(
"illegal wildcard import", import.location, suggestChange = "import ${
@@ -338,7 +338,7 @@ class SemanticModelBuilder(
// Add built-in types
fun addBuiltIn(name: String, type: Type) {
putIfAbsent(name, type)?.let { existingType ->
- controller.createContext(file.sourceFile).error {
+ controller.getOrCreateContext(file.sourceFile).error {
message("Type '$name' shadows built-in type with same name")
if (existingType is UserDefinedType) {
val definition = existingType.definition
@@ -379,7 +379,7 @@ class SemanticModelBuilder(
return ResolvedTypeReference(expression, it)
}
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Type '${expression.name}' could not be resolved")
highlight("unresolved type", expression.location)
}
@@ -403,14 +403,14 @@ class SemanticModelBuilder(
}
null -> {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Type '${expression.name}' could not be resolved")
highlight("unresolved type", expression.location)
}
}
else -> {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Type '${expression.components.first().name}' is not a package, cannot access sub-types")
highlight("not a package", expression.components.first().location)
}
@@ -422,7 +422,7 @@ class SemanticModelBuilder(
val baseType = resolveExpression(expression.base)
val constraints = expression.arguments.mapNotNull { constraintBuilder.build(baseType.type, it) }
if (baseType.constraints.isNotEmpty()) {
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Cannot have nested constraints")
highlight("illegal nested constraint", expression.location)
}
@@ -460,7 +460,7 @@ class SemanticModelBuilder(
}
}
}
- controller.createContext(expression.location.source).error {
+ controller.getOrCreateContext(expression.location.source).error {
message("Unsupported generic type")
highlight(expression.location)
help("Valid generic types are List and Map")
@@ -470,7 +470,7 @@ class SemanticModelBuilder(
is OptionalDeclarationNode -> {
val baseType = resolveExpression(expression.base)
if (baseType.isOptional) {
- controller.createContext(expression.location.source).warn {
+ controller.getOrCreateContext(expression.location.source).warn {
message("Type is already optional, ignoring '?'")
highlight("already optional", expression.base.location)
}
@@ -481,7 +481,7 @@ class SemanticModelBuilder(
is BooleanNode,
is NumberNode,
is StringNode,
- -> controller.createContext(expression.location.source).error {
+ -> controller.getOrCreateContext(expression.location.source).error {
message("Cannot use literal value as type")
highlight("not a type expression", expression.location)
}
@@ -490,7 +490,7 @@ class SemanticModelBuilder(
is ArrayNode,
is RangeExpressionNode,
is WildcardNode,
- -> controller.createContext(expression.location.source).error {
+ -> controller.getOrCreateContext(expression.location.source).error {
message("Invalid type expression")
highlight("not a type expression", expression.location)
}
@@ -505,7 +505,7 @@ class SemanticModelBuilder(
companion object {
fun build(files: List, controller: DiagnosticController): Package {
// Sort by path to ensure deterministic order
- return SemanticModelBuilder(files.sortedBy { it.sourceFile.absolutePath }, controller).build()
+ return SemanticModelBuilder(files.sortedBy { it.sourceFile.path }, controller).build()
}
}
}
diff --git a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt
index 8a8178c2..14e056b3 100644
--- a/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt
+++ b/semantic/src/main/kotlin/tools/samt/semantic/SemanticModelPostProcessor.kt
@@ -27,7 +27,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
check(typeReference is ResolvedTypeReference)
when (val type = typeReference.type) {
is ServiceType -> {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
// error message applies to both record fields and return types
message("Cannot use service '${type.name}' as type")
highlight("service type not allowed here", typeReference.definition.location)
@@ -35,14 +35,14 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
}
is ProviderType -> {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot use provider '${type.name}' as type")
highlight("provider type not allowed here", typeReference.definition.location)
}
}
is PackageType -> {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot use package '${type.packageName}' as type")
highlight("package type not allowed here", typeReference.definition.location)
}
@@ -64,15 +64,15 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
private inline fun checkServiceType(typeReference: TypeReference, block: (serviceType: ServiceType) -> Unit) {
check(typeReference is ResolvedTypeReference)
if (typeReference.constraints.isNotEmpty()) {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot have constraints on service")
for (constraint in typeReference.constraints) {
- highlight("illegal constraint", constraint.definition.location)
+ highlight("illegal constraint", constraint.node.location)
}
}
}
if (typeReference.isOptional) {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot have optional service")
highlight("illegal optional", typeReference.definition.location)
}
@@ -84,7 +84,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
is UnknownType -> Unit
else -> {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Expected a service but got '${type.humanReadableName}'")
highlight("illegal type", typeReference.definition.location)
}
@@ -95,15 +95,15 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
private inline fun checkProviderType(typeReference: TypeReference, block: (providerType: ProviderType) -> Unit) {
check(typeReference is ResolvedTypeReference)
if (typeReference.constraints.isNotEmpty()) {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot have constraints on provider")
for (constraint in typeReference.constraints) {
- highlight("illegal constraint", constraint.definition.location)
+ highlight("illegal constraint", constraint.node.location)
}
}
}
if (typeReference.isOptional) {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Cannot have optional provider")
highlight("illegal optional", typeReference.definition.location)
}
@@ -115,7 +115,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
is UnknownType -> Unit
else -> {
- controller.createContext(typeReference.definition.location.source).error {
+ controller.getOrCreateContext(typeReference.definition.location.source).error {
message("Expected a provider but got '${type.humanReadableName}'")
highlight("illegal type", typeReference.definition.location)
}
@@ -144,7 +144,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
provider.implements.forEach { implements ->
checkServiceType(implements.service) { type ->
implementsTypes.putIfAbsent(type, implements.definition.location)?.let { existingLocation ->
- controller.createContext(implements.definition.location.source).error {
+ controller.getOrCreateContext(implements.definition.location.source).error {
message("Service '${type.name}' already implemented")
highlight("duplicate implements", implements.definition.location)
highlight("previous implements", existingLocation)
@@ -160,7 +160,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
if (matchingOperation != null) {
matchingOperation
} else {
- controller.createContext(provider.definition.location.source).error {
+ controller.getOrCreateContext(provider.definition.location.source).error {
message("Operation '${serviceOperationName.name}' not found in service '${type.name}'")
highlight("unknown operation", serviceOperationName.location)
}
@@ -178,7 +178,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
consumer.uses.forEach { uses ->
checkServiceType(uses.service) { type ->
usesTypes.putIfAbsent(type, uses.definition.location)?.let { existingLocation ->
- controller.createContext(uses.definition.location.source).error {
+ controller.getOrCreateContext(uses.definition.location.source).error {
message("Service '${type.name}' already used")
highlight("duplicate uses", uses.definition.location)
highlight("previous uses", existingLocation)
@@ -189,7 +189,7 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
val matchingImplements =
providerType.implements.find { (it.service as ResolvedTypeReference).type == type }
if (matchingImplements == null) {
- controller.createContext(uses.definition.location.source).error {
+ controller.getOrCreateContext(uses.definition.location.source).error {
message("Service '${type.name}' is not implemented by provider '${providerType.name}'")
highlight("unavailable service", uses.definition.serviceName.location)
}
@@ -205,12 +205,12 @@ internal class SemanticModelPostProcessor(private val controller: DiagnosticCont
matchingOperation
} else {
if (type.operations.any { it.name == serviceOperationName.name }) {
- controller.createContext(uses.definition.location.source).error {
+ controller.getOrCreateContext(uses.definition.location.source).error {
message("Operation '${serviceOperationName.name}' in service '${type.name}' is not implemented by provider '${providerType.name}'")
highlight("unavailable operation", serviceOperationName.location)
}
} else {
- controller.createContext(uses.definition.location.source).error {
+ controller.getOrCreateContext(uses.definition.location.source).error {
message("Operation '${serviceOperationName.name}' not found in service '${type.name}'")
highlight("unknown operation", serviceOperationName.location)
}
diff --git a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt
index f4f7d05d..99e82750 100644
--- a/semantic/src/main/kotlin/tools/samt/semantic/Types.kt
+++ b/semantic/src/main/kotlin/tools/samt/semantic/Types.kt
@@ -249,10 +249,10 @@ data class ResolvedTypeReference(
sealed interface Constraint {
val humanReadableName: String
- val definition: ExpressionNode
+ val node: ExpressionNode
data class Range(
- override val definition: ExpressionNode,
+ override val node: ExpressionNode,
val lowerBound: Number?,
val upperBound: Number?,
) : Constraint {
@@ -261,7 +261,7 @@ data class ResolvedTypeReference(
}
data class Size(
- override val definition: ExpressionNode,
+ override val node: ExpressionNode,
val lowerBound: Long?,
val upperBound: Long?,
) : Constraint {
@@ -270,7 +270,7 @@ data class ResolvedTypeReference(
}
data class Pattern(
- override val definition: ExpressionNode,
+ override val node: ExpressionNode,
val pattern: String,
) : Constraint {
override val humanReadableName: String
@@ -278,7 +278,7 @@ data class ResolvedTypeReference(
}
data class Value(
- override val definition: ExpressionNode,
+ override val node: ExpressionNode,
val value: Any,
) : Constraint {
override val humanReadableName: String
diff --git a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt
index 879375b4..b9bd330c 100644
--- a/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt
+++ b/semantic/src/test/kotlin/tools/samt/semantic/SemanticModelTest.kt
@@ -5,6 +5,7 @@ import tools.samt.common.DiagnosticController
import tools.samt.common.SourceFile
import tools.samt.lexer.Lexer
import tools.samt.parser.Parser
+import java.net.URI
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -692,11 +693,11 @@ class SemanticModelTest {
private fun parseAndCheck(
vararg sourceAndExpectedMessages: Pair>,
) {
- val diagnosticController = DiagnosticController("/tmp")
- val fileTrees = sourceAndExpectedMessages.mapIndexed { index, (source) ->
- val filePath = "/tmp/SemanticModelTest-${index}.samt"
+ val diagnosticController = DiagnosticController(URI("file:///tmp"))
+ val fileTree = sourceAndExpectedMessages.mapIndexed { index, (source) ->
+ val filePath = URI("file:///tmp/SemanticModelTest-${index}.samt")
val sourceFile = SourceFile(filePath, source)
- val parseContext = diagnosticController.createContext(sourceFile)
+ val parseContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), parseContext)
val fileTree = Parser.parse(sourceFile, stream, parseContext)
assertFalse(parseContext.hasErrors(), "Expected no parse errors, but had errors: ${parseContext.messages}}")
@@ -705,7 +706,7 @@ class SemanticModelTest {
val parseMessageCount = diagnosticController.contexts.associate { it.source.content to it.messages.size }
- SemanticModelBuilder.build(fileTrees, diagnosticController)
+ SemanticModelBuilder.build(fileTree, diagnosticController)
for ((source, expectedMessages) in sourceAndExpectedMessages) {
val messages = diagnosticController.contexts
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8858a6d0..48d9739a 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,9 +1,12 @@
rootProject.name = "samt-core"
-include(":common")
-include(":cli")
-include(":lexer")
-include(":parser")
-include(":semantic")
+include(
+ ":common",
+ ":cli",
+ ":lexer",
+ ":parser",
+ ":semantic",
+ ":language-server"
+)
dependencyResolutionManagement {
versionCatalogs {
@@ -13,12 +16,14 @@ dependencyResolutionManagement {
val mordant = "2.0.0-beta12"
val kotlinxSerialization = "1.5.0"
val kover = "0.6.1"
+ val lsp4j = "0.20.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)
+ library("lsp4j", "org.eclipse.lsp4j", "org.eclipse.lsp4j").version(lsp4j)
plugin("shadow", "com.github.johnrengelman.shadow").version(shadow)
plugin("kover", "org.jetbrains.kotlinx.kover").version(kover)