From acdbdc7039cd644e7ba453e309266f449fb77ea0 Mon Sep 17 00:00:00 2001 From: rochala Date: Mon, 21 Oct 2024 17:03:51 +0200 Subject: [PATCH 1/5] early work --- .../dotty/tools/pc/DiagnosticProvider.scala | 70 +++++++++++++++++++ .../tools/pc/ScalaPresentationCompiler.scala | 42 ++++++----- .../pc/tests/DiagnosticProviderSuite.scala | 69 ++++++++++++++++++ project/Build.scala | 1 + 4 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala create mode 100644 presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala diff --git a/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala new file mode 100644 index 000000000000..a11a16e43338 --- /dev/null +++ b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala @@ -0,0 +1,70 @@ +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.dotc.semanticdb.DiagnosticOps.toSemanticDiagnostic +import dotty.tools.pc.utils.InteractiveEnrichments.toLsp + +import scala.meta.pc.VirtualFileParams +import com.google.gson.Gson +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.Message +import dotty.tools.dotc.interfaces.DiagnosticRelatedInformation +import org.eclipse.lsp4j.Location +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 + // lspDiag.setRelatedInformation(???) Currently not emitted by the compiler + lspDiag.setData(actions) + 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 + diff --git a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala index 218d92c38ffa..b3bb78e80385 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/ScalaPresentationCompiler.scala @@ -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 @@ -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( @@ -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() @@ -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() @@ -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 @@ -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() @@ -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() @@ -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) @@ -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, @@ -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(), @@ -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)( diff --git a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala new file mode 100644 index 000000000000..99c45230ac36 --- /dev/null +++ b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala @@ -0,0 +1,69 @@ +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 org.eclipse.lsp4j.jsonrpc.messages.{Either => JEither} +import scala.meta.internal.jdk.CollectionConverters.* +import scala.meta.internal.metals.CompilerVirtualFileParams +import scala.meta.internal.metals.EmptyCancelToken +import scala.meta.internal.pc.PcReferencesRequest + +import org.junit.Test +import scala.collection.mutable.ListBuffer +import org.eclipse.lsp4j.DiagnosticSeverity +import scala.concurrent.duration.* +import dotty.tools.pc.utils.TestExtensions.getOffset + +class DiagnosticProviderSuite extends BasePCSuite with RangeReplace { + private val rangeRegex = "<<.*>>".r + 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 simple1 = + 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 `implicit-args-2` = + // check( + // """|package example + // | + // |class Bar(i: Int) + // |class Foo(implicit b: Bar) + // | + // |object Hello { + // | implicit val ba@@rr: Bar = new Bar(1) + // | val foo = new Foo<<>> + // |} + // |""".stripMargin + // ) + + // @Test def `case-class` = + // check( + // """|case class Ma@@in(i: Int) + // |""".stripMargin + // ) + + // @Test def `case-class-with-implicit` = + // check( + // """"|case class A()(implicit val fo@@o: Int) + // |""".stripMargin + // ) +} diff --git a/project/Build.scala b/project/Build.scala index 806412931696..8ad3d9ede7a5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -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, From 6498c18d1bc12212daf67d42ed6295eb2e0342f4 Mon Sep 17 00:00:00 2001 From: rochala Date: Wed, 11 Dec 2024 13:34:22 +0100 Subject: [PATCH 2/5] Clean tests --- .../pc/tests/DiagnosticProviderSuite.scala | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala index 99c45230ac36..9aee8f69d51a 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala @@ -36,34 +36,9 @@ class DiagnosticProviderSuite extends BasePCSuite with RangeReplace { @Test def simple1 = check( - """|class Bar(i: It) + """|class Bar(i: <>) |""".stripMargin, List(TestDiagnostic(13, 15, "Not found: type It - did you mean Int.type? or perhaps Int?", DiagnosticSeverity.Error)) ) - // @Test def `implicit-args-2` = - // check( - // """|package example - // | - // |class Bar(i: Int) - // |class Foo(implicit b: Bar) - // | - // |object Hello { - // | implicit val ba@@rr: Bar = new Bar(1) - // | val foo = new Foo<<>> - // |} - // |""".stripMargin - // ) - - // @Test def `case-class` = - // check( - // """|case class Ma@@in(i: Int) - // |""".stripMargin - // ) - - // @Test def `case-class-with-implicit` = - // check( - // """"|case class A()(implicit val fo@@o: Int) - // |""".stripMargin - // ) } From 3c48b0bd33a844f21bcd015574e946530300da03 Mon Sep 17 00:00:00 2001 From: rochala Date: Sun, 22 Dec 2024 12:58:52 +0100 Subject: [PATCH 3/5] Fix code actions + remove unused imports --- .../src/main/dotty/tools/pc/DiagnosticProvider.scala | 8 ++------ .../dotty/tools/pc/tests/DiagnosticProviderSuite.scala | 5 ----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala index a11a16e43338..d5f7a66e105b 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/DiagnosticProvider.scala @@ -5,19 +5,14 @@ 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.dotc.semanticdb.DiagnosticOps.toSemanticDiagnostic import dotty.tools.pc.utils.InteractiveEnrichments.toLsp import scala.meta.pc.VirtualFileParams -import com.google.gson.Gson 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.Message -import dotty.tools.dotc.interfaces.DiagnosticRelatedInformation -import org.eclipse.lsp4j.Location import dotty.tools.dotc.reporting.ErrorMessageID import org.eclipse.lsp4j.DiagnosticTag @@ -40,8 +35,9 @@ class DiagnosticProvider(driver: InteractiveDriver, params: VirtualFileParams): 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(actions) + lspDiag.setData(scalaDiagnostic) if diag.msg.errorId == ErrorMessageID.UnusedSymbolID then lspDiag.setTags(List(DiagnosticTag.Unnecessary).asJava) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala index 9aee8f69d51a..9c29980eaf5e 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala @@ -6,20 +6,15 @@ import dotty.tools.pc.base.BasePCSuite import dotty.tools.pc.utils.RangeReplace import java.net.URI -import org.eclipse.lsp4j.jsonrpc.messages.{Either => JEither} import scala.meta.internal.jdk.CollectionConverters.* import scala.meta.internal.metals.CompilerVirtualFileParams import scala.meta.internal.metals.EmptyCancelToken -import scala.meta.internal.pc.PcReferencesRequest import org.junit.Test -import scala.collection.mutable.ListBuffer import org.eclipse.lsp4j.DiagnosticSeverity -import scala.concurrent.duration.* import dotty.tools.pc.utils.TestExtensions.getOffset class DiagnosticProviderSuite extends BasePCSuite with RangeReplace { - private val rangeRegex = "<<.*>>".r case class TestDiagnostic(startIndex: Int, endIndex: Int, msg: String, severity: DiagnosticSeverity) def check( From 9c1c52717363bf08351a4520a628a00fa995ec6a Mon Sep 17 00:00:00 2001 From: rochala Date: Mon, 23 Dec 2024 08:55:51 +0100 Subject: [PATCH 4/5] Add and fix tests --- .../pc/tests/DiagnosticProviderSuite.scala | 24 +++++++++++++++++-- project/Build.scala | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala index 9c29980eaf5e..e15eed749800 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/DiagnosticProviderSuite.scala @@ -29,11 +29,31 @@ class DiagnosticProviderSuite extends BasePCSuite with RangeReplace { 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 simple1 = + @Test def error = check( - """|class Bar(i: <>) + """|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) + ) + ) + } diff --git a/project/Build.scala b/project/Build.scala index 8ad3d9ede7a5..2e3daf86dc1b 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -517,7 +517,7 @@ object Build { "scala2-library-tasty" ) - val enableBspAllProjects = false + val enableBspAllProjects = true // Settings used when compiling dotty with a non-bootstrapped dotty lazy val commonBootstrappedSettings = commonDottySettings ++ Seq( From 31c9470f1d44b7e242cd630d0a5c9d45536bd094 Mon Sep 17 00:00:00 2001 From: rochala Date: Mon, 23 Dec 2024 19:59:20 +0100 Subject: [PATCH 5/5] Remove illegal setting --- project/Build.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/Build.scala b/project/Build.scala index 2e3daf86dc1b..8ad3d9ede7a5 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -517,7 +517,7 @@ object Build { "scala2-library-tasty" ) - val enableBspAllProjects = true + val enableBspAllProjects = false // Settings used when compiling dotty with a non-bootstrapped dotty lazy val commonBootstrappedSettings = commonDottySettings ++ Seq(