Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Presentation compiler: diagnostic provider #22259

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dotty.tools.pc

import org.eclipse.lsp4j
import org.eclipse.lsp4j.DiagnosticSeverity
import dotty.tools.dotc.interactive.InteractiveDriver
import dotty.tools.dotc.interfaces.Diagnostic as DiagnosticInterfaces
import dotty.tools.dotc.reporting.Diagnostic
import dotty.tools.pc.utils.InteractiveEnrichments.toLsp

import scala.meta.pc.VirtualFileParams
import ch.epfl.scala.bsp4j
import dotty.tools.dotc.reporting.CodeAction
import dotty.tools.dotc.rewrites.Rewrites.ActionPatch
import scala.jdk.CollectionConverters.*
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.reporting.ErrorMessageID
import org.eclipse.lsp4j.DiagnosticTag

class DiagnosticProvider(driver: InteractiveDriver, params: VirtualFileParams):

def diagnostics(): List[lsp4j.Diagnostic] =
val diags = driver.run(params.uri().nn, params.text().nn)
given Context = driver.currentCtx
diags.flatMap(toLsp)

private def toLsp(diag: Diagnostic)(using Context): Option[lsp4j.Diagnostic] =
Option.when(diag.pos.exists):
val lspDiag = lsp4j.Diagnostic(
diag.pos.toLsp,
diag.msg.message,
toDiagnosticSeverity(diag.level),
"presentation compiler",
)
lspDiag.setCode(diag.msg.errorId.errorNumber)

val scalaDiagnostic = new bsp4j.ScalaDiagnostic()
val actions = diag.msg.actions.map(toBspScalaAction).asJava
scalaDiagnostic.setActions(actions)
// lspDiag.setRelatedInformation(???) Currently not emitted by the compiler
lspDiag.setData(scalaDiagnostic)
if diag.msg.errorId == ErrorMessageID.UnusedSymbolID then
lspDiag.setTags(List(DiagnosticTag.Unnecessary).asJava)

lspDiag

private def toBspScalaAction(action: CodeAction): bsp4j.ScalaAction =
val bspAction = bsp4j.ScalaAction(action.title)
action.description.foreach(bspAction.setDescription)
val workspaceEdit = bsp4j.ScalaWorkspaceEdit(action.patches.map(toBspTextEdit).asJava)
bspAction.setEdit(workspaceEdit)
bspAction

private def toBspTextEdit(actionPatch: ActionPatch): bsp4j.ScalaTextEdit =
val startPos = bsp4j.Position(actionPatch.srcPos.startLine, actionPatch.srcPos.startColumn)
val endPos = bsp4j.Position(actionPatch.srcPos.endLine, actionPatch.srcPos.endColumn)
val range = bsp4j.Range(startPos, endPos)
bsp4j.ScalaTextEdit(range, actionPatch.replacement)


private def toDiagnosticSeverity(severity: Int): DiagnosticSeverity =
severity match
case DiagnosticInterfaces.ERROR => DiagnosticSeverity.Error
case DiagnosticInterfaces.WARNING => DiagnosticSeverity.Warning
case DiagnosticInterfaces.INFO => DiagnosticSeverity.Information
case _ => DiagnosticSeverity.Information

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import java.util.Optional
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutorService
import java.util.concurrent.ScheduledExecutorService
import java.util.Collections
import java.util as ju

import scala.concurrent.ExecutionContext
Expand Down Expand Up @@ -108,7 +109,9 @@ case class ScalaPresentationCompiler(
params.token()
) { access =>
val driver = access.compiler()
new PcSemanticTokensProvider(driver, params).provide().asJava
new PcSemanticTokensProvider(driver, params)
.provide()
.asJava
}

override def inlayHints(
Expand Down Expand Up @@ -175,7 +178,7 @@ case class ScalaPresentationCompiler(
params: OffsetParams
): CompletableFuture[ju.List[DocumentHighlight]] =
compilerAccess.withInterruptableCompiler(Some(params))(
List.empty[DocumentHighlight].asJava,
Collections.emptyList(),
params.token()
) { access =>
val driver = access.compiler()
Expand All @@ -186,7 +189,7 @@ case class ScalaPresentationCompiler(
params: ReferencesRequest
): CompletableFuture[ju.List[ReferencesResult]] =
compilerAccess.withNonInterruptableCompiler(Some(params.file()))(
List.empty[ReferencesResult].asJava,
Collections.emptyList(),
params.file().token,
) { access =>
val driver = access.compiler()
Expand All @@ -204,14 +207,11 @@ case class ScalaPresentationCompiler(
new InferExpectedType(search, driver, params).infer().asJava
}

def shutdown(): Unit =
compilerAccess.shutdown()
def shutdown(): Unit = compilerAccess.shutdown()

def restart(): Unit =
compilerAccess.shutdownCurrentCompiler()
def restart(): Unit = compilerAccess.shutdownCurrentCompiler()

def diagnosticsForDebuggingPurposes(): ju.List[String] =
List[String]().asJava
def diagnosticsForDebuggingPurposes(): ju.List[String] = Collections.emptyList()

override def info(
symbol: String
Expand Down Expand Up @@ -264,7 +264,7 @@ case class ScalaPresentationCompiler(
ju.List[scala.meta.pc.AutoImportsResult]
] =
compilerAccess.withNonInterruptableCompiler(Some(params))(
List.empty[scala.meta.pc.AutoImportsResult].asJava,
Collections.emptyList(),
params.token()
) { access =>
val driver = access.compiler()
Expand All @@ -283,9 +283,8 @@ case class ScalaPresentationCompiler(
def implementAbstractMembers(
params: OffsetParams
): CompletableFuture[ju.List[l.TextEdit]] =
val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]()
compilerAccess.withNonInterruptableCompiler(Some(params))(
empty,
Collections.emptyList(),
params.token()
) { pc =>
val driver = pc.compiler()
Expand All @@ -301,9 +300,8 @@ case class ScalaPresentationCompiler(
override def insertInferredType(
params: OffsetParams
): CompletableFuture[ju.List[l.TextEdit]] =
val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]()
compilerAccess.withNonInterruptableCompiler(Some(params))(
empty,
Collections.emptyList(),
params.token()
) { pc =>
new InferredTypeProvider(params, pc.compiler(), config, search)
Expand All @@ -330,8 +328,10 @@ case class ScalaPresentationCompiler(
range: RangeParams,
extractionPos: OffsetParams
): CompletableFuture[ju.List[l.TextEdit]] =
val empty: ju.List[l.TextEdit] = new ju.ArrayList[l.TextEdit]()
compilerAccess.withInterruptableCompiler(Some(range))(empty, range.token()) {
compilerAccess.withInterruptableCompiler(Some(range))(
Collections.emptyList(),
range.token()
) {
pc =>
new ExtractMethodProvider(
range,
Expand Down Expand Up @@ -368,7 +368,7 @@ case class ScalaPresentationCompiler(
): CompletableFuture[ju.List[l.SelectionRange]] =
CompletableFuture.completedFuture {
compilerAccess.withSharedCompiler(params.asScala.headOption)(
List.empty[l.SelectionRange].asJava
Collections.emptyList()
) { pc =>
new SelectionRangeProvider(
pc.compiler(),
Expand Down Expand Up @@ -438,7 +438,13 @@ case class ScalaPresentationCompiler(
override def didChange(
params: VirtualFileParams
): CompletableFuture[ju.List[l.Diagnostic]] =
CompletableFuture.completedFuture(Nil.asJava)
compilerAccess.withNonInterruptableCompiler(Some(params))(
Collections.emptyList(),
params.token()
) { access =>
val driver = access.compiler()
new DiagnosticProvider(driver, params).diagnostics().asJava
}

override def didClose(uri: URI): Unit =
compilerAccess.withNonInterruptableCompiler(None)(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package dotty.tools.pc.tests

import scala.language.unsafeNulls

import dotty.tools.pc.base.BasePCSuite
import dotty.tools.pc.utils.RangeReplace

import java.net.URI
import scala.meta.internal.jdk.CollectionConverters.*
import scala.meta.internal.metals.CompilerVirtualFileParams
import scala.meta.internal.metals.EmptyCancelToken

import org.junit.Test
import org.eclipse.lsp4j.DiagnosticSeverity
import dotty.tools.pc.utils.TestExtensions.getOffset

class DiagnosticProviderSuite extends BasePCSuite with RangeReplace {
case class TestDiagnostic(startIndex: Int, endIndex: Int, msg: String, severity: DiagnosticSeverity)

def check(
text: String,
expected: List[TestDiagnostic]
): Unit =
val diagnostics = presentationCompiler
.didChange(CompilerVirtualFileParams(URI.create("file:/Diagnostic.scala"), text, EmptyCancelToken))
.get()
.asScala

val actual = diagnostics.map(d => TestDiagnostic(d.getRange().getStart().getOffset(text), d.getRange().getEnd().getOffset(text), d.getMessage(), d.getSeverity()))
assertEquals(expected, actual, s"Expected [${expected.mkString(", ")}] but got [${actual.mkString(", ")}]")

@Test def error =
check(
"""|class Bar(i: It)
|""".stripMargin,
List(TestDiagnostic(13, 15, "Not found: type It - did you mean Int.type? or perhaps Int?", DiagnosticSeverity.Error))
)

@Test def warning =
check(
"""|object M:
| 1 + 1
|""".stripMargin,
List(TestDiagnostic(12, 17, "A pure expression does nothing in statement position", DiagnosticSeverity.Warning))
)

@Test def mixed =
check(
"""|class Bar(i: It)
|object M:
| 1 + 1
|""".stripMargin,
List(
TestDiagnostic(13 ,15, "Not found: type It - did you mean Int.type? or perhaps Int?", DiagnosticSeverity.Error),
TestDiagnostic(29, 34, "A pure expression does nothing in statement position", DiagnosticSeverity.Warning)
)
)

}
1 change: 1 addition & 0 deletions project/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,7 @@ object Build {
.exclude("org.eclipse.lsp4j","org.eclipse.lsp4j")
.exclude("org.eclipse.lsp4j","org.eclipse.lsp4j.jsonrpc"),
"org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.20.1",
"ch.epfl.scala" % "bsp4j" % "2.1.1",
),
libraryDependencies += ("org.scalameta" % "mtags-shared_2.13.15" % mtagsVersion % SourceDeps),
ivyConfigurations += SourceDeps.hide,
Expand Down
Loading