Skip to content

Commit

Permalink
feat(lsp): report diagnostics and keep up with changes
Browse files Browse the repository at this point in the history
  • Loading branch information
mjossdev committed May 4, 2023
1 parent 763a29f commit 65fb45f
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 199 deletions.
2 changes: 1 addition & 1 deletion cli/src/main/kotlin/tools/samt/cli/CliCompiler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion cli/src/main/kotlin/tools/samt/cli/CliDumper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ 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
Expand Down
33 changes: 5 additions & 28 deletions cli/src/main/kotlin/tools/samt/cli/CliFileResolution.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>.readSamtSourceFiles(controller: DiagnosticController): List<SourceFile> {
val files = map { File(it) }.ifEmpty { gatherSamtFiles(controller.workingDirectoryAbsolutePath) }
internal fun List<String>.readSamtSourceFiles(controller: DiagnosticController): List<SourceFile> =
map { File(it) }.ifEmpty { collectSamtFiles(controller.workingDirectoryAbsolutePath) }
.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<File> {
val dir = File(directory)
return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList()
}
2 changes: 1 addition & 1 deletion cli/src/test/kotlin/tools/samt/cli/ASTPrinterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class ASTPrinterTest {
val filePath = "/tmp/ASTPrinterTest.samt"
val sourceFile = SourceFile(filePath, source)
val diagnosticController = DiagnosticController("/tmp")
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")
Expand Down
4 changes: 2 additions & 2 deletions cli/src/test/kotlin/tools/samt/cli/DiagnosticFormatterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class DiagnosticFormatterTest {
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")
Expand Down Expand Up @@ -484,7 +484,7 @@ class DiagnosticFormatterTest {
val filePath = Path("/tmp", "DiagnosticFormatterTest.samt").absolutePathString()
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")
Expand Down
2 changes: 1 addition & 1 deletion cli/src/test/kotlin/tools/samt/cli/TokenPrinterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class TokenPrinterTest {
val filePath = "/tmp/TokenPrinterTest.samt"
val sourceFile = SourceFile(filePath, source)
val diagnosticController = DiagnosticController("/tmp")
val diagnosticContext = diagnosticController.createContext(sourceFile)
val diagnosticContext = diagnosticController.getOrCreateContext(sourceFile)
val stream = Lexer.scan(source.reader(), diagnosticContext)
assertFalse(diagnosticContext.hasErrors(), "Expected no errors, but had errors")
return stream
Expand Down
2 changes: 1 addition & 1 deletion common/src/main/kotlin/tools/samt/common/Diagnostics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,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) }
Expand Down
31 changes: 31 additions & 0 deletions common/src/main/kotlin/tools/samt/common/Files.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package tools.samt.common

import java.io.File

fun List<File>.readSamtSource(controller: DiagnosticController): List<SourceFile> {
return buildList {
for (file in this@readSamtSource) {
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()))
}
}
}

fun collectSamtFiles(directory: String): List<File> {
val dir = File(directory)
return dir.walkTopDown().filter { it.isFile && it.extension == "samt" }.toList()
}
4 changes: 2 additions & 2 deletions common/src/test/kotlin/tools/samt/common/DiagnosticsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class DiagnosticsTest {
package debug
""".trimIndent()
val sourceFile = SourceFile(sourcePath, sourceCode)
val context = controller.createContext(sourceFile)
val context = controller.getOrCreateContext(sourceFile)

assertThrows<DiagnosticException>("some fatal error") {
context.fatal {
Expand Down Expand Up @@ -65,7 +65,7 @@ class DiagnosticsTest {
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)
Expand Down
35 changes: 35 additions & 0 deletions language-server/src/main/kotlin/tools/samt/ls/FileInfo.kt
Original file line number Diff line number Diff line change
@@ -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,
val tokens: List<Token>,
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)
}
34 changes: 34 additions & 0 deletions language-server/src/main/kotlin/tools/samt/ls/Mapping.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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
// TODO consider highlightBeginningOnly
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("file://${it.location.source.absolutePath}", it.location.toRange()),
it.message
)
}
return diagnostic
}

fun SamtLocation.toRange(): Range {
return Range(
Position(start.row, start.col),
Position(start.row, end.col)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,31 @@ package tools.samt.ls

import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.services.*
import tools.samt.common.*
import java.io.Closeable
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 textDocumentService = SamtTextDocumentService()
private val logger = Logger.getLogger("SamtLanguageServer")
private val workspaces = mutableMapOf<String, SamtWorkspace>()
private val textDocumentService = SamtTextDocumentService(workspaces)

override fun initialize(params: InitializeParams): CompletableFuture<InitializeResult> =
CompletableFuture.supplyAsync {
buildSamtModel(params)
val capabilities = ServerCapabilities().apply {
// TODO support pull-based diagnostics? diagnosticProvider = DiagnosticRegistrationOptions(true, false)
setTextDocumentSync(TextDocumentSyncKind.Full)
}
InitializeResult(capabilities)
}

override fun initialized(params: InitializedParams) {
pushDiagnostics()
}

override fun setTrace(params: SetTraceParams?) {
// TODO
}
Expand All @@ -44,4 +50,31 @@ class SamtLanguageServer : LanguageServer, LanguageClientAware, Closeable {
override fun close() {
shutdown().get()
}

private fun buildSamtModel(params: InitializeParams) {
params.workspaceFolders.forEach {
val path = it.uri.uriToPath()
workspaces[path] = buildWorkspace(path)
}
}

private fun buildWorkspace(workspacePath: String): 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.pathToUri(),
messages.map { it.toDiagnostic() }
)
}
}.forEach(client::publishDiagnostics)
}
}
Loading

0 comments on commit 65fb45f

Please sign in to comment.