diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 00000000000..bdcd8bd4001 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,3 @@ +rules = [ + RemoveUnused +] diff --git a/.scalafmt.conf b/.scalafmt.conf index a3bf33925b3..a215dc0f724 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -12,5 +12,6 @@ To fix this problem: project.excludeFilters = [ test-workspace + tests/unit/src/test/resources sbt-metals/src/sbt-test ] diff --git a/.travis.yml b/.travis.yml index d11e117a4af..61a92ad2a14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,8 +10,10 @@ stages: jobs: include: # default stage is test - - env: TEST="scalafmt" - script: ./bin/scalafmt --test + - env: TEST="scalafmt+scalafix" + script: + - ./bin/scalafmt --test + - sbt scalafixCheck - env: TEST="sbt test" script: - sbt startServer metalsSetup test diff --git a/build.sbt b/build.sbt index afdac0a6e44..79cdd60522f 100644 --- a/build.sbt +++ b/build.sbt @@ -1,9 +1,12 @@ +import java.io.File inThisBuild( List( version ~= { dynVer => if (sys.env.contains("CI")) dynVer else "SNAPSHOT" // only for local publishng }, + scalaVersion := V.scala212, + crossScalaVersions := List(V.scala212), scalacOptions ++= List( "-Yrangepos", "-deprecation", @@ -11,12 +14,11 @@ inThisBuild( // https://github.com/scala/bug/issues/10448 "-Ywarn-unused-import" ), + addCompilerPlugin(MetalsPlugin.semanticdbScalac), organization := "org.scalameta", licenses := Seq( "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") ), - testFrameworks := List(new TestFramework("utest.runner.Framework")), - libraryDependencies += "com.lihaoyi" %% "utest" % "0.6.0" % Test, homepage := Some(url("https://github.com/scalameta/metals")), developers := List( Developer( @@ -50,70 +52,49 @@ inThisBuild( url("http://delmore.io") ) ), + testFrameworks := List(), resolvers += Resolver.sonatypeRepo("releases"), // faster publishLocal: publishArtifact in packageDoc := sys.env.contains("CI"), - publishArtifact in packageSrc := sys.env.contains("CI"), - addCompilerPlugin( - "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full - ) + publishArtifact in packageSrc := sys.env.contains("CI") ) ) +addCommandAlias("scalafixAll", "all compile:scalafix test:scalafix") +addCommandAlias("scalafixCheck", "; scalafix --check ; test:scalafix --check") + +commands += Command.command("save-expect") { s => + "unit/test:runMain tests.SaveExpect" :: + s +} + lazy val V = new { val scala210 = "2.10.7" val scala212 = MetalsPlugin.scala212 val scalameta = MetalsPlugin.semanticdbVersion - val scalafix = "0.5.7" - val circe = "0.9.0" - val enumeratum = "1.5.12" } -lazy val legacyScala212 = List( - addCompilerPlugin(MetalsPlugin.semanticdbScalac), - scalaVersion := V.scala212, - crossScalaVersions := List(V.scala212), -) +skip.in(publish) := true -skip in publish := true -legacyScala212 +lazy val mtags = project + .settings( + libraryDependencies ++= List( + "com.thoughtworks.qdox" % "qdox" % "2.0-M9", // for java mtags + "org.scalameta" %% "scalameta" % V.scalameta + ) + ) lazy val metals = project - .enablePlugins(BuildInfoPlugin) .settings( - legacyScala212, - fork in Test := true, // required for jni interrop with leveldb. - buildInfoKeys := Seq[BuildInfoKey]( - "testWorkspaceBaseDirectory" -> - baseDirectory.in(testWorkspace).value, - version, - ), - buildInfoPackage := "scala.meta.metals.internal", libraryDependencies ++= List( + "com.thoughtworks.qdox" % "qdox" % "2.0-M9", // for java mtags "com.lihaoyi" %% "pprint" % "0.5.3", // for pretty formatting of log values - "org.scala-sbt.ipcsocket" % "ipcsocket" % "1.0.0", // for sbt server - "ch.epfl.scala" % "scalafix-reflect" % V.scalafix cross CrossVersion.full, - "com.googlecode.java-diff-utils" % "diffutils" % "1.3.0", // for edit-distance - "com.thoughtworks.qdox" % "qdox" % "2.0-M7", // for java mtags - "io.get-coursier" %% "coursier" % coursier.util.Properties.version, // for jars - "io.get-coursier" %% "coursier-cache" % coursier.util.Properties.version, - "io.github.soc" % "directories" % "5", // for cache location - "me.xdrop" % "fuzzywuzzy" % "1.1.9", // for workspace/symbol - "org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8", // for caching classpath index - "org.scalameta" %% "lsp4s" % "0.2.1", - "org.scalameta" %% "semanticdb-scalac" % V.scalameta cross CrossVersion.full, - "io.circe" %% "circe-core" % V.circe, - "io.circe" %% "circe-generic" % V.circe, - "io.circe" %% "circe-generic-extras" % V.circe, - "io.circe" %% "circe-parser" % V.circe, - "com.beachape" %% "enumeratum" % V.enumeratum, - "com.beachape" %% "enumeratum-circe" % "1.5.15", - "org.scalameta" %% "testkit" % V.scalameta % Test, + "org.scalameta" %% "scalameta" % V.scalameta, + "org.scalameta" %% "symtab" % V.scalameta, + "org.scalameta" %% "lsp4s" % "0.2.1" ) ) - .dependsOn( - testWorkspace % "test->test" - ) + .dependsOn(mtags) lazy val `sbt-metals` = project .settings( @@ -135,54 +116,73 @@ lazy val `sbt-metals` = project ), ) .enablePlugins(ScriptedPlugin) + .disablePlugins(ScalafixPlugin) -lazy val integration = project - .in(file("tests/integration")) +lazy val input = project + .in(file("tests/input")) .settings( - legacyScala212, - skip in publish := true + skip.in(publish) := true, + libraryDependencies ++= List( + // these projects have macro annotations + "org.scalameta" %% "scalameta" % V.scalameta, + "io.circe" %% "circe-derivation-annotations" % "0.9.0-M5" + ), + addCompilerPlugin( + "org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full + ) ) - .dependsOn(metals % "compile->compile;test->test") -lazy val testWorkspace = project - .in(file("test-workspace")) +lazy val unit = project + .in(file("tests/unit")) .settings( - legacyScala212, - skip in publish := true, - scalacOptions += { - // Need to fix source root so it matches the workspace folder. - s"-P:semanticdb:sourceroot:${baseDirectory.value}" - }, - scalacOptions += "-Ywarn-unused-import", - scalacOptions -= "-Xlint" + skip.in(publish) := true, + testFrameworks := List(new TestFramework("utest.runner.Framework")), + libraryDependencies ++= List( + "io.get-coursier" %% "coursier" % coursier.util.Properties.version, // for jars + "io.get-coursier" %% "coursier-cache" % coursier.util.Properties.version, + "org.scalameta" % "metac" % V.scalameta cross CrossVersion.full, + "org.scalameta" %% "testkit" % V.scalameta, + "com.lihaoyi" %% "utest" % "0.6.0", + ), + buildInfoPackage := "tests", + resourceGenerators.in(Compile) += InputProperties.resourceGenerator(input), + compile.in(Compile) := + compile.in(Compile).dependsOn(compile.in(input, Test)).value, + buildInfoKeys := Seq[BuildInfoKey]( + "testResourceDirectory" -> resourceDirectory.in(Test).value + ) ) + .dependsOn(metals) + .enablePlugins(BuildInfoPlugin) + +lazy val bench = project + .in(file("metals-bench")) + .settings( + fork.in(run) := true, + skip.in(publish) := true, + moduleName := "metals-bench", + libraryDependencies ++= List( + // for measuring memory usage + "org.spire-math" %% "clouseau" % "0.2.2", + "org.openjdk.jol" % "jol-core" % "0.9" + ), + ) + .dependsOn(unit) + .enablePlugins(JmhPlugin) lazy val docs = project .in(file("metals-docs")) .settings( - skip in publish := true, // disabled until Scalameta v4 upgrade + skip.in(publish) := true, moduleName := "metals-docs", - sources.in(Compile) += { - sourceDirectory.in(metals, Compile).value / - "scala/scala/meta/metals/Configuration.scala" - }, - scalaVersion := "2.12.7", - crossScalaVersions := List("2.12.6"), mainClass.in(Compile) := Some("docs.Docs"), - SettingKey[Boolean]("metalsEnabled") := false, libraryDependencies ++= List( - "com.geirsson" % "mdoc" % "0.5.0" cross CrossVersion.full, - // Dependencies below can be removed after the upgrade to Scalameta v4.0 - "io.circe" %% "circe-core" % V.circe, - "io.circe" %% "circe-generic" % V.circe, - "io.circe" %% "circe-generic-extras" % V.circe, - "io.circe" %% "circe-parser" % V.circe, - "com.beachape" %% "enumeratum" % V.enumeratum, - "com.beachape" %% "enumeratum-circe" % "1.5.15" + "com.geirsson" % "mdoc" % "0.5.1" cross CrossVersion.full ), buildInfoKeys := Seq[BuildInfoKey]( version, ), buildInfoPackage := "docs" ) + .dependsOn(metals) .enablePlugins(DocusaurusPlugin, BuildInfoPlugin) diff --git a/docs/installation-contributors.md b/docs/installation-contributors.md index 857f7d831f1..e8d9ab546f3 100644 --- a/docs/installation-contributors.md +++ b/docs/installation-contributors.md @@ -3,17 +3,18 @@ id: installation-contributors title: Installation --- -> ⚠ ️ This project is very alpha stage. Expect bugs and incomplete documentation. +> ⚠ ️ This project is very alpha stage. Expect bugs and incomplete +> documentation. The following instructions are intended for contributors who want to try Metals and provide feedback. We do not provide support for day-to-day usage of Metals. ## Requirements -* Scala 2.12.4, down to the exact PATCH version. Note that Scala versions 2.11.x and - 2.12.6 are not supported. -* Java 8. Note that Java 9 or higher has not been tested. -* macOS or Linux. Note that there have been reported issues on Windows. +- Scala @SCALA_VERSION@, down to the exact PATCH version. Note that Scala + versions 2.11.x and 2.12.6 are not supported. +- Java 8. Note that Java 9 or higher has not been tested. +- macOS or Linux. Note that there have been reported issues on Windows. ## Global setup @@ -30,50 +31,58 @@ You can install the plugin with: addSbtPlugin("org.scalameta" % "sbt-metals" % "@VERSION@") ``` -You can add the plugin to a specific project (adding it to `project/plugins.sbt`) or globally adding it to: +You can add the plugin to a specific project (adding it to +`project/plugins.sbt`) or globally adding it to: - (sbt 1) `~/.sbt/1.0/plugins/plugins.sbt` - (sbt 0.13) `~/.sbt/0.13/plugins/plugins.sbt` ### VSCode extension -> ***N.B.*** This project is still in development - right now you will need to run the VSCode plugin -> as described [here](getting-started-contributors.html#running-a-local-version-of-the-vscode-extension) +> **_N.B._** This project is still in development - right now you will need to +> run the VSCode plugin as described +> [here](getting-started-contributors.html#running-a-local-version-of-the-vscode-extension) ## Per-project setup These steps are required on each project. ### Quick-start -The quickest way to get started with Metals is to use the `metalsSetup` command in sbt. + +The quickest way to get started with Metals is to use the `metalsSetup` command +in sbt. ``` sbt > metalsSetup ``` -The command will create the necessary metadata in the `.metals` directory -(which you should not checkout into version control) and setup the `semanticdb-scalac` compiler -plugin for the current sbt session. +The command will create the necessary metadata in the `.metals` directory (which +you should not checkout into version control) and setup the `semanticdb-scalac` +compiler plugin for the current sbt session. -You should not checkout the `.metals` directory into version control. We recommend to add it to your -project's `.gitignore` or/and to your global `.gitignore`: +You should not checkout the `.metals` directory into version control. We +recommend to add it to your project's `.gitignore` or/and to your global +`.gitignore`: ``` echo ".metals/" >> .gitignore ``` -Note that in sbt 0.13 you will need to invoke `metalsSetup` (or `semanticdbEnable`) whenever you close and -re-open sbt. For a more persistent setup, keep reading. In sbt 1 you don't need to do it because Metals will -automatically invoke `semanticdbEnable` every time it connects to the sbt server. +Note that in sbt 0.13 you will need to invoke `metalsSetup` (or +`semanticdbEnable`) whenever you close and re-open sbt. For a more persistent +setup, keep reading. In sbt 1 you don't need to do it because Metals will +automatically invoke `semanticdbEnable` every time it connects to the sbt +server. ### Persisting the semanticdb-scalac compiler plugin -Some features like definition/references/hover rely on artifacts produced by a compiler plugin -called `semanticdb-scalac`. -`metalsSetup` enables the plugin on the current session (by invoking `semanticdbEnable`), but you -can choose to enable it permanently on your project by adding these two settings in your sbt build -definition: +Some features like definition/references/hover rely on artifacts produced by a +compiler plugin called `semanticdb-scalac`. + +`metalsSetup` enables the plugin on the current session (by invoking +`semanticdbEnable`), but you can choose to enable it permanently on your project +by adding these two settings in your sbt build definition: ```scala addCompilerPlugin(MetalsPlugin.semanticdbScalac) @@ -81,6 +90,7 @@ scalacOptions += "-Yrangepos" ``` ### Start editing + Open your project in VSCode (`code .` from your terminal) and open a Scala file; the server will now start. diff --git a/docs/new-editor.md b/docs/new-editor.md index dacea34b210..4dfa1e037d4 100644 --- a/docs/new-editor.md +++ b/docs/new-editor.md @@ -6,13 +6,13 @@ title: Integrating a new editor Before writing a new editor client, first check if someone else has managed to integrate metals with your favorite text editor. -* [Visual Studio Code](https://github.com/scalameta/metals/blob/master/vscode-extension/src/extension.ts), +- [Visual Studio Code](https://github.com/scalameta/metals/blob/master/vscode-extension/src/extension.ts), maintained in this repo -* [Atom](https://github.com/laughedelic/atom-ide-scala), maintained by +- [Atom](https://github.com/laughedelic/atom-ide-scala), maintained by [@laughedelic](https://github.com/laughedelic) -* [Emacs](https://github.com/rossabaker/lsp-scala), maintained by +- [Emacs](https://github.com/rossabaker/lsp-scala), maintained by [@rossabaker](https://github.com/rossabaker) -* Others, see [#217](https://github.com/scalameta/metals/issues/217). Please +- Others, see [#217](https://github.com/scalameta/metals/issues/217). Please open an issue or ask on [gitter](https://gitter.im/scalameta/metals) if you want to create a new editor client. @@ -40,12 +40,12 @@ The following script will launch the latest published version of the server: coursier launch -r bintray:scalameta/maven org.scalameta:metals_2.12:@VERSION@ -M scala.meta.metals.Main ``` -Our CI publishes a new version of the server at every merge on master; there are currently no stable -or officially supported releases of Metals. +Our CI publishes a new version of the server at every merge on master; there are +currently no stable or officially supported releases of Metals. -You can use a local version of metals by publishing it locally with `sbt publishLocal`, and changing -the artifact version to `SNAPSHOT`. The following script will launch the locally published version -of the server: +You can use a local version of metals by publishing it locally with +`sbt publishLocal`, and changing the artifact version to `SNAPSHOT`. The +following script will launch the locally published version of the server: ``` coursier launch -r bintray:scalameta/maven org.scalameta:metals_2.12:SNAPSHOT -M scala.meta.metals.Main @@ -53,7 +53,7 @@ coursier launch -r bintray:scalameta/maven org.scalameta:metals_2.12:SNAPSHOT -M The following Java options are recommended: -* `-XX:+UseG1GC -XX:+UseStringDeduplication`: to reduce memory consumption from +- `-XX:+UseG1GC -XX:+UseStringDeduplication`: to reduce memory consumption from navigation indexes. May not be necessary in the future. Refer to the coursier documentation for how to build a fat jar or configure java @@ -76,17 +76,17 @@ Metals delegates file watching to the editor client by listening for client to send notifications for changes to files matching the following patterns -* `.metals/buildinfo/**/*.properties`: build metadata to enable goto definition +- `.metals/buildinfo/**/*.properties`: build metadata to enable goto definition for the classpath, completions with the presentation compiler and refactoring with Scalafix. -* `**/*.semanticdb`: artifacts produced by the semanticdb-scalac compiler plugin +- `**/*.semanticdb`: artifacts produced by the semanticdb-scalac compiler plugin during batch compilation in the build. See [here](https://github.com/scalameta/scalameta/blob/master/semanticdb/semanticdb3/semanticdb3.md) to learn more about SemanticDB. These files are required for goto definition, find references, hover and Scalafix to work. -* `project/target/active.json`: an indicator of the running sbt server. - Watching this file allows metals to (re)connect to the sbt server whenever it - is (re)started. +- `project/target/active.json`: an indicator of the running sbt server. Watching + this file allows metals to (re)connect to the sbt server whenever it is + (re)started. See the VS Code plugin [clientOptions](https://github.com/scalameta/metals/blob/fb166f1d81eb77ebd9c6b3ee95e65fb58a907eec/vscode-extension/src/extension.ts#L44-L54) @@ -110,21 +110,20 @@ client is expected to send a `workspace/didChangeConfiguration` notification containing user configuration right after the `initialized` notification. A full list of server configuration options can be found in -[Configuration.scala][]. An -example of how the configuration options are used from the VS Code plugin can be -seen in the +[Configuration.scala][]. An example of how the configuration options are used +from the VS Code plugin can be seen in the [package.json](https://github.com/scalameta/metals/blob/master/vscode-extension/package.json) manifest. Here are the default values for all the options: -```scala mdoc:passthrough +````scala mdoc:passthrough { println("```json") println(scala.meta.metals.Configuration.defaultAsJson) println("```") } -``` +```` Server side configuration options include settings to enable experimental/unstable features such as completions with the presentation @@ -137,7 +136,7 @@ an empty list of completion suggestions. Clients are also encouraged to implement this setting: -* `serverVersion: String`: while metals is still under active development, it is +- `serverVersion: String`: while metals is still under active development, it is recommended to allow end-users to easily configure the version of the metals server. @@ -181,12 +180,19 @@ shortcut (on macOS). ![](assets/code-actions.gif) -[`textdocument/willsavewaituntil`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_willSaveWaitUntil -[`textdocument/codeaction`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction -[`workspace/executecommand`]: https://microsoft.github.io/language-server-protocol/specification#workspace_executeCommand -[workspacecommand.scala]: https://github.com/scalameta/metals/blob/master/metals/src/main/scala/scala/meta/metals/WorkspaceCommand.scala -[configuration.scala]: https://github.com/scalameta/metals/blob/master/metals/src/main/scala/scala/meta/metals/Configuration.scala -[package.json]: https://github.com/scalameta/metals/blob/master/vscode-extension/package.json -[`workspace/didchangewatchedfiles`]: https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles +[`textdocument/willsavewaituntil`]: + https://microsoft.github.io/language-server-protocol/specification#textDocument_willSaveWaitUntil +[`textdocument/codeaction`]: + https://microsoft.github.io/language-server-protocol/specification#textDocument_codeAction +[`workspace/executecommand`]: + https://microsoft.github.io/language-server-protocol/specification#workspace_executeCommand +[workspacecommand.scala]: + https://github.com/scalameta/metals/blob/master/metals/src/main/scala/scala/meta/metals/WorkspaceCommand.scala +[configuration.scala]: + https://github.com/scalameta/metals/blob/master/metals/src/main/scala/scala/meta/metals/Configuration.scala +[package.json]: + https://github.com/scalameta/metals/blob/master/vscode-extension/package.json +[`workspace/didchangewatchedfiles`]: + https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles [#216]: https://github.com/scalameta/metals/issues/216 [#255]: https://github.com/scalameta/metals/issues/255 diff --git a/metals-bench/readme.md b/metals-bench/readme.md new file mode 100644 index 00000000000..1c70c61b34d --- /dev/null +++ b/metals-bench/readme.md @@ -0,0 +1,19 @@ +# Benchmarks + +Date: 2018 October 8th, commit 59bda2ac81a497fa168677499bd1a9df60fec5ab +``` +> bench/jmh:run -i 10 -wi 10 -f1 -t1 +[info] Benchmark Mode Cnt Score Error Units +[info] MetalsBench.indexSources ss 10 0.620 ± 0.058 s/op +[info] MetalsBench.javaMtags ss 10 7.233 ± 0.017 s/op +[info] MetalsBench.scalaMtags ss 10 4.617 ± 0.034 s/op +[info] MetalsBench.scalaToplevels ss 10 0.361 ± 0.005 s/op +> bench/run +[info] info elapsed: 3265ms +[info] info java lines: 0 +[info] info scala lines: 1263569 +[info] bench.Memory.printFootprint:11 iterable.source: "index" +[info] bench.Memory.printFootprint:12 Units.approx(size): "16.9M" +[info] bench.Memory.printFootprint:24 count: 12L +[info] bench.Memory.printFootprint:25 Units.approx(elementSize): "1.41M" +``` diff --git a/metals-bench/src/main/scala/bench/Inflated.scala b/metals-bench/src/main/scala/bench/Inflated.scala new file mode 100644 index 00000000000..da401c96540 --- /dev/null +++ b/metals-bench/src/main/scala/bench/Inflated.scala @@ -0,0 +1,49 @@ +package bench + +import java.nio.charset.StandardCharsets +import scala.meta.inputs.Input +import scala.meta.internal.io.FileIO +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath +import scala.meta.metals.MetalsLogger + +case class Inflated(inputs: List[Input.VirtualFile], linesOfCode: Long) { + def filter(f: Input.VirtualFile => Boolean): Inflated = { + val newInputs = inputs.filter(input => f(input)) + val newLinesOfCode = newInputs.foldLeft(0) { + case (accum, input) => + accum + input.text.lines.length + } + Inflated(newInputs, newLinesOfCode) + } + def +(other: Inflated): Inflated = + Inflated(other.inputs ++ inputs, other.linesOfCode + linesOfCode) +} +object Inflated { + + def jars(classpath: Classpath): Inflated = { + classpath.entries.foldLeft(Inflated(Nil, 0L)) { + case (accum, next) => + accum + jar(next) + } + } + def jar(file: AbsolutePath): Inflated = { + FileIO.withJarFileSystem( + file, + create = false, + close = true + ) { root => + MetalsLogger.updateFormat() + var lines = 0L + val buf = List.newBuilder[Input.VirtualFile] + FileIO.listAllFilesRecursively(root).foreach { file => + val path = file.toString() + val text = FileIO.slurp(file, StandardCharsets.UTF_8) + lines += text.lines.length + buf += Input.VirtualFile(path, text) + } + val inputs = buf.result() + Inflated(inputs, lines) + } + } +} diff --git a/metals-bench/src/main/scala/bench/MainBench.scala b/metals-bench/src/main/scala/bench/MainBench.scala new file mode 100644 index 00000000000..4a0f580e56c --- /dev/null +++ b/metals-bench/src/main/scala/bench/MainBench.scala @@ -0,0 +1,21 @@ +package bench + +import java.util.concurrent.TimeUnit +import scala.meta.metals.MetalsLogger +import scala.meta.internal.mtags.OnDemandSymbolIndex +import tests.Libraries + +object MainBench { + def main(args: Array[String]): Unit = { + MetalsLogger.updateFormat() + val classpath = Libraries.suite.map(_.sources()).reduce(_ ++ _) + val start = System.nanoTime() + val index = OnDemandSymbolIndex() + classpath.entries.foreach(entry => index.addSourceJar(entry)) + val end = System.nanoTime() + scribe.info(s"elapsed: ${TimeUnit.NANOSECONDS.toMillis(end - start)}ms") + scribe.info(s"java lines: ${index.mtags.totalLinesOfJava}") + scribe.info(s"scala lines: ${index.mtags.totalLinesOfScala}") + Memory.printFootprint(index) + } +} diff --git a/metals-bench/src/main/scala/bench/Memory.scala b/metals-bench/src/main/scala/bench/Memory.scala new file mode 100644 index 00000000000..9db8085d7b2 --- /dev/null +++ b/metals-bench/src/main/scala/bench/Memory.scala @@ -0,0 +1,28 @@ +package bench + +import clouseau.Units +import org.openjdk.jol.info.GraphLayout +import scala.meta.internal.mtags.OnDemandSymbolIndex + +object Memory { + def printFootprint(iterable: sourcecode.Text[Object]): Unit = { + val layout = GraphLayout.parseInstance(iterable.value) + val size = layout.totalSize() + pprint.log(iterable.source) + pprint.log(Units.approx(size)) + val count: Long = iterable.value match { + case it: Iterable[_] => + it.size + case index: OnDemandSymbolIndex => + // 100k loc + index.mtags.totalLinesOfCode / 100000 + case _ => + 1 + } + if (count > 1) { + val elementSize = size / count + pprint.log(count) + pprint.log(Units.approx(elementSize)) + } + } +} diff --git a/metals-bench/src/main/scala/bench/MetalsBench.scala b/metals-bench/src/main/scala/bench/MetalsBench.scala new file mode 100644 index 00000000000..b556c0f0d61 --- /dev/null +++ b/metals-bench/src/main/scala/bench/MetalsBench.scala @@ -0,0 +1,92 @@ +package bench + +import org.openjdk.jmh.annotations.Benchmark +import org.openjdk.jmh.annotations.BenchmarkMode +import org.openjdk.jmh.annotations.Mode +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State +import scala.meta.internal.semanticdb.TextDocument +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath +import scala.meta.metals.MetalsLogger +import tests.InputProperties +import tests.Libraries +import scala.meta.internal.mtags.Mtags +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.mtags.SemanticdbClasspath +import tests.Library + +@State(Scope.Benchmark) +class MetalsBench { + + MetalsLogger.updateFormat() + val inputs = InputProperties.default() + val classpath = new SemanticdbClasspath(inputs.sourceroot, inputs.classpath) + val documents: List[(AbsolutePath, TextDocument)] = + inputs.scalaFiles.map { input => + (input.file, classpath.textDocument(input.file).get) + } + + val scalaLibrarySourcesJar: AbsolutePath = inputs.dependencySources.entries + .find(_.toNIO.getFileName.toString.contains("scala-library")) + .getOrElse { + sys.error( + s"missing: scala-library-sources.jar, obtained ${inputs.dependencySources}" + ) + } + + val jdk = Classpath(Library.jdkSources.toList) + val fullClasspath = jdk ++ inputs.dependencySources + + val inflated = Inflated.jars(fullClasspath) + + val scalaDependencySources: Inflated = { + val result = inflated.filter(_.path.endsWith(".scala")) + scribe.info(s"Scala lines: ${result.linesOfCode}") + result + } + + val javaDependencySources: Inflated = { + val result = inflated.filter(_.path.endsWith(".java")) + scribe.info(s"Java lines: ${result.linesOfCode}") + result + } + + val megaSources = Classpath( + Libraries.suite + .flatMap(_.sources().entries) + .filter(_.toNIO.getFileName.toString.endsWith(".jar")) + ) + + @Benchmark + @BenchmarkMode(Array(Mode.SingleShotTime)) + def scalaMtags(): Unit = { + scalaDependencySources.inputs.foreach { input => + Mtags.index(input) + } + } + + @Benchmark + @BenchmarkMode(Array(Mode.SingleShotTime)) + def scalaToplevels(): Unit = { + scalaDependencySources.inputs.foreach { input => + Mtags.toplevels(input) + } + } + + @Benchmark + @BenchmarkMode(Array(Mode.SingleShotTime)) + def javaMtags(): Unit = { + javaDependencySources.inputs.foreach { input => + Mtags.index(input) + } + } + + @Benchmark + @BenchmarkMode(Array(Mode.SingleShotTime)) + def indexSources(): Unit = { + val index = OnDemandSymbolIndex() + fullClasspath.entries.foreach(entry => index.addSourceJar(entry)) + } + +} diff --git a/metals-docs/src/main/scala/docs/Docs.scala b/metals-docs/src/main/scala/docs/Docs.scala index 0147ac98a22..dd42016f9f2 100644 --- a/metals-docs/src/main/scala/docs/Docs.scala +++ b/metals-docs/src/main/scala/docs/Docs.scala @@ -8,7 +8,12 @@ object Docs { // build arguments for mdoc val settings = mdoc .MainSettings() - .withSiteVariables(Map("VERSION" -> BuildInfo.version)) + .withSiteVariables( + Map( + "VERSION" -> BuildInfo.version, + "SCALA_VERSION" -> scala.util.Properties.versionNumberString + ) + ) .withOut(out) .withArgs(args.toList) // generate out/readme.md from working directory diff --git a/metals/src/main/protobuf/metaserver.proto b/metals/src/main/protobuf/metaserver.proto deleted file mode 100644 index 0c998785fea..00000000000 --- a/metals/src/main/protobuf/metaserver.proto +++ /dev/null @@ -1,41 +0,0 @@ -syntax = "proto3"; - -package scala.meta.metals.index; - -// All of the metadata associated with a single symbol definition -message SymbolData { - string symbol = 1; - // The Position where this symbol is defined. - Position definition = 2; - map references = 3; - int64 flags = 4; - string name = 5; - string signature = 6; - // Planned for Scalameta v2.2, see https://github.com/scalameta/scalameta/milestone/9 - // string docstring = 7; // javadoc/scaladoc - // string overrides = 8; // parent symbol that this symbol overrides -} - -// A standalone position used by "Go to definition" when jumping to a symbol -// definition. -message Position { - // can be a local file, entry inside local zip file, http url, or anything - // else. - string uri = 1; - Range range = 2; -} - -// A hack to workaround the fact that protobuf can't -// encode `map` -message Ranges { - repeated Range ranges = 1; -} - -// A slim range position, the filename is embedded in the -// key of `references: map` -message Range { - int32 startLine = 1; - int32 startColumn = 2; - int32 endLine = 3; - int32 endColumn = 4; -} diff --git a/metals/src/main/scala/org/langmeta/languageserver/InputEnrichments.scala b/metals/src/main/scala/org/langmeta/languageserver/InputEnrichments.scala deleted file mode 100644 index 8a76cba43fc..00000000000 --- a/metals/src/main/scala/org/langmeta/languageserver/InputEnrichments.scala +++ /dev/null @@ -1,85 +0,0 @@ -package org.langmeta.languageserver - -import scala.meta.lsp -import org.langmeta.inputs.Input -import org.langmeta.inputs.Position -import scala.meta.metals.{index => i} - -object InputEnrichments { - implicit class XtensionPositionOffset(val pos: Position) extends AnyVal { - def caret: String = pos match { - case Position.None => "" - case _ => " " * pos.startColumn + "^" - } - def path: String = pos match { - case Position.None => "" - case _ => s"${pos.input.syntax}:${pos.startLine + 1}:${pos.startColumn}" - } - def lineContent: String = pos match { - case r: Position.Range => - val start = pos.start - pos.startColumn - val end = pos.input.lineToOffset(pos.startLine + 1) - 1 - r.copy(start = start, end = end).text - case _ => "" - } - } - implicit class XtensionInputOffset(val input: Input) extends AnyVal { - def toIndexRange(start: Int, end: Int): i.Range = { - val pos = Position.Range(input, start, end) - i.Range( - startLine = pos.startLine, - startColumn = pos.startColumn, - endLine = pos.endLine, - endColumn = pos.endColumn - ) - } - - /** Returns offset position with end == start == offset */ - def toOffsetPosition(offset: Int): Position = - Position.Range(input, offset, offset) - - /** Returns a scala.meta.Position from an index range. */ - def toPosition(range: lsp.Range): Position = { - toPosition( - range.start.line, - range.start.character, - range.end.line, - range.end.character - ) - } - - /** Returns a scala.meta.Position from an index range. */ - def toPosition(range: i.Range): Position = { - toPosition( - range.startLine, - range.startColumn, - range.endLine, - range.endColumn - ) - } - - def toOffset(pos: lsp.Position): Int = - toOffset(pos.line, pos.character) - - /** Returns an offset for this input */ - def toOffset(line: Int, column: Int): Int = - input.lineToOffset(line) + column - - /** Returns an offset position for this input */ - def toPosition(startLine: Int, startColumn: Int): Position.Range = - toPosition(startLine, startColumn, startLine, startColumn) - - /** Returns a range position for this input */ - def toPosition( - startLine: Int, - startColumn: Int, - endLine: Int, - endColumn: Int - ): Position.Range = - Position.Range( - input, - toOffset(startLine, startColumn), - toOffset(endLine, endColumn) - ) - } -} diff --git a/metals/src/main/scala/org/langmeta/semanticdb/SemanticdbEnrichments.scala b/metals/src/main/scala/org/langmeta/semanticdb/SemanticdbEnrichments.scala deleted file mode 100644 index 423ba8e8152..00000000000 --- a/metals/src/main/scala/org/langmeta/semanticdb/SemanticdbEnrichments.scala +++ /dev/null @@ -1,16 +0,0 @@ -package org.langmeta.semanticdb - -import scala.meta.lsp.SymbolKind - -object SemanticdbEnrichments { - implicit class XtensionLongAsFlags(val flags: Long) extends HasFlags { - def hasOneOfFlags(flags: Long): Boolean = - (this.flags & flags) != 0L - def toSymbolKind: SymbolKind = - if (isClass) SymbolKind.Class - else if (isTrait) SymbolKind.Interface - else if (isTypeParam) SymbolKind.TypeParameter - else if (isObject) SymbolKind.Object - else SymbolKind.Module - } -} diff --git a/metals/src/main/scala/scala/meta/metals/Buffers.scala b/metals/src/main/scala/scala/meta/metals/Buffers.scala deleted file mode 100644 index 50cea281c13..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Buffers.scala +++ /dev/null @@ -1,66 +0,0 @@ -package scala.meta.metals - -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.util.concurrent.ConcurrentHashMap -import java.util.{Map => JMap} -import scala.meta.lsp.VersionedTextDocumentIdentifier -import org.langmeta.io.AbsolutePath -import scala.meta.Source -import scala.meta.lsp.TextDocumentIdentifier -import scala.meta.lsp.VersionedTextDocumentIdentifier -import org.langmeta.inputs.Input - -/** - * Utility to keep local state of file contents. - * - * If we use nio.Files.read directly then we don't pick up unsaved changes in - * the editor buffer. Instead, on every file open/changed we update Buffers with - * the file contents. - * - * It should be possible to avoid the need for this class, see - * https://github.com/sourcegraph/language-server-protocol/blob/master/extension-files.md - */ -class Buffers private ( - contents: JMap[Uri, String], - cwd: AbsolutePath -) { - private def readFromDisk(path: AbsolutePath): String = { - scribe.info(s"Reading $path from disk") - new String(Files.readAllBytes(path.toNIO), StandardCharsets.UTF_8) - } - def changed(input: Input.VirtualFile): Effects.UpdateBuffers = { - contents.put(Uri(input.path), input.value) - Effects.UpdateBuffers - } - def closed(uri: Uri): Unit = { - contents.remove(uri) - sources.remove(uri) - } - - def read(td: TextDocumentIdentifier): String = - read(Uri(td.uri)) - def read(td: VersionedTextDocumentIdentifier): String = - read(Uri(td.uri)) - def read(path: AbsolutePath): String = - read(Uri(path)) - def read(uri: Uri): String = - Option(contents.get(uri)) - .getOrElse(readFromDisk(uri.toAbsolutePath)) - - private val sources: JMap[Uri, Source] = new ConcurrentHashMap() - // Tries to parse and record it or fallback to an old source if it existed - def source(uri: Uri): Option[Source] = - Parser - .parse(read(uri)) - .toOption - .map { tree => - sources.put(uri, tree) - tree - } - .orElse(Option(sources.get(uri))) -} -object Buffers { - def apply()(implicit cwd: AbsolutePath): Buffers = - new Buffers(new ConcurrentHashMap(), cwd) -} diff --git a/metals/src/main/scala/scala/meta/metals/Configuration.scala b/metals/src/main/scala/scala/meta/metals/Configuration.scala deleted file mode 100644 index 1c918b8b787..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Configuration.scala +++ /dev/null @@ -1,69 +0,0 @@ -package scala.meta.metals - -import scala.util.Try -import io.circe.Encoder -import io.circe.Decoder -import io.circe.generic.extras.{ConfiguredJsonCodec => JsonCodec} -import io.circe.generic.extras.{Configuration => CirceConfiguration} -import io.circe.syntax._ -import Configuration._ -import java.nio.file.Path -import java.nio.file.Paths - -@JsonCodec case class Configuration( - sbt: Sbt = Sbt(), - scalac: Scalac = Scalac(), - scalafmt: Scalafmt = Scalafmt(), - scalafix: Scalafix = Scalafix(), - search: Search = Search(), - hover: Enabled = Enabled(true), - highlight: Enabled = Enabled(false), - rename: Enabled = Enabled(false), -) - -object Configuration { - - /** pretty-printed default configuration */ - lazy val defaultAsJson: String = Configuration().asJson.spaces2 - - @JsonCodec case class Enabled(enabled: Boolean) - - @JsonCodec case class Sbt( - diagnostics: Enabled = Enabled(true), - command: String = "", - ) - @JsonCodec case class Scalac( - completions: Enabled = Enabled(false), - diagnostics: Enabled = Enabled(false), - ) { - def enabled: Boolean = completions.enabled || diagnostics.enabled - } - - @JsonCodec case class Scalafmt( - enabled: Boolean = true, - onSave: Boolean = false, - version: String = "1.4.0", - confPath: Option[Path] = Some(Scalafmt.defaultConfPath) - ) - object Scalafmt { - lazy val defaultConfPath = Paths.get(".scalafmt.conf") - } - @JsonCodec case class Scalafix( - enabled: Boolean = true, - confPath: Option[Path] = Some(Scalafix.defaultConfPath) - ) - object Scalafix { - lazy val defaultConfPath = Paths.get(".scalafix.conf") - } - @JsonCodec case class Search( - indexJDK: Boolean = true, - indexClasspath: Boolean = true - ) - - implicit val pathReads: Decoder[Path] = - Decoder.decodeString.emapTry(s => Try(Paths.get(s))) - implicit val pathWrites: Encoder[Path] = - Encoder.encodeString.contramap(_.toString) - implicit val circeConfiguration: CirceConfiguration = - CirceConfiguration.default.withDefaults -} diff --git a/metals/src/main/scala/scala/meta/metals/Effects.scala b/metals/src/main/scala/scala/meta/metals/Effects.scala deleted file mode 100644 index ad35eed9fd0..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Effects.scala +++ /dev/null @@ -1,18 +0,0 @@ -package scala.meta.metals - -/** - * The Metals effects. - * - * Observable[Unit] is not descriptive of what the observable represents. - * Instead, we create Unit-like types to better document what effects are - * flowing through our application. - */ -sealed abstract class Effects -object Effects { - final class IndexSemanticdb extends Effects - final val IndexSemanticdb = new IndexSemanticdb - final class IndexSourcesClasspath extends Effects - final val IndexSourcesClasspath = new IndexSourcesClasspath - final class UpdateBuffers extends Effects - final val UpdateBuffers = new UpdateBuffers -} diff --git a/metals/src/main/scala/scala/meta/metals/Formatter.scala b/metals/src/main/scala/scala/meta/metals/Formatter.scala deleted file mode 100644 index c58785a6459..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Formatter.scala +++ /dev/null @@ -1,63 +0,0 @@ -package scala.meta.metals - -import scala.language.reflectiveCalls - -import scala.reflect.internal.util.ScalaClassLoader.URLClassLoader -import org.langmeta.io.AbsolutePath - -abstract class Formatter { - - /** Format using configuration from configFile */ - def format(code: String, filename: String, configFile: AbsolutePath): String - - /** Format using default settings */ - def format(code: String, filename: String): String - -} - -object Formatter { - - /** Returns formatter that does nothing */ - lazy val noop: Formatter = new Formatter { - override def format( - code: String, - filename: String, - configFile: AbsolutePath - ): String = code - override def format( - code: String, - filename: String - ): String = code - } - - /** Returns instance of scalafmt from isoloated classloader */ - def classloadScalafmt(version: String): Formatter = { - val urls = Jars - .fetch("com.geirsson", "scalafmt-cli_2.12", version, System.out) - .iterator - .map(_.toURI.toURL) - .toArray - scribe.info(s"Classloading scalafmt with ${urls.length} downloaded jars") - type Scalafmt210 = { - def format(code: String, configFile: String, filename: String): String - def format(code: String, filename: String): String - } - val classloader = new URLClassLoader(urls, null) - val scalafmt210 = classloader - .loadClass("org.scalafmt.cli.Scalafmt210") - .newInstance() - .asInstanceOf[Scalafmt210] - new Formatter { - override def format( - code: String, - filename: String, - configFile: AbsolutePath - ): String = - scalafmt210.format(code, configFile.toString(), filename) - - override def format(code: String, filename: String): String = - scalafmt210.format(code, filename) - } - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/MSchedulers.scala b/metals/src/main/scala/scala/meta/metals/MSchedulers.scala deleted file mode 100644 index ecec30461ac..00000000000 --- a/metals/src/main/scala/scala/meta/metals/MSchedulers.scala +++ /dev/null @@ -1,23 +0,0 @@ -package scala.meta.metals - -import java.util.concurrent.Executors -import monix.execution.Scheduler -import monix.execution.schedulers.SchedulerService - -/** - * Utility to manage monix schedulers. - * - * @param global The default scheduler when you are unsure which one to use. - * @param lsp to communicate with LSP editor client. - * @param sbt to communicate with sbt server. - */ -case class MSchedulers(global: Scheduler, lsp: Scheduler, sbt: Scheduler) -object MSchedulers { - def apply(): MSchedulers = new MSchedulers(main, lsp, sbt) - lazy val main: SchedulerService = - Scheduler(Executors.newFixedThreadPool(4)) - lazy val lsp: SchedulerService = - Scheduler(Executors.newFixedThreadPool(1)) - lazy val sbt: SchedulerService = - Scheduler(Executors.newFixedThreadPool(3)) -} diff --git a/metals/src/main/scala/scala/meta/metals/Main.scala b/metals/src/main/scala/scala/meta/metals/Main.scala deleted file mode 100644 index 513eca6bb26..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Main.scala +++ /dev/null @@ -1,56 +0,0 @@ -package scala.meta.metals - -import java.io.PrintStream -import java.nio.file.Files -import scala.meta.jsonrpc.BaseProtocolMessage -import scala.util.Properties -import scala.util.control.NonFatal -import org.langmeta.internal.io.PathIO -import scala.meta.jsonrpc.LanguageClient -import scala.meta.jsonrpc.LanguageServer - -object Main { - def main(args: Array[String]): Unit = { - val cwd = PathIO.workingDirectory - val configDir = cwd.resolve(".metals").toNIO - val logPath = configDir.resolve("metals.log") - Files.createDirectories(configDir) - val out = new PrintStream(Files.newOutputStream(logPath)) - val err = new PrintStream(Files.newOutputStream(logPath)) - val stdin = System.in - val stdout = System.out - val stderr = System.err - try { - // route System.out somewhere else. Any output not from the server (e.g. logging) - // messes up with the client, since stdout is used for the language server protocol - System.setOut(out) - System.setErr(err) - MetalsLogger.setup(logPath) - - scribe.info(s"Starting server in $cwd") - scribe.info(s"Classpath: ${Properties.javaClassPath}") - val s = MSchedulers() - val client = new LanguageClient(stdout, scribe.Logger.root) - val services = new MetalsServices(cwd, client, s) - val messages = BaseProtocolMessage - .fromInputStream(stdin, scribe.Logger.root) - .executeOn(s.lsp) - val languageServer = new LanguageServer( - messages, - client, - services.services, - s.global, - scribe.Logger.root - ) - languageServer.listen() - } catch { - case NonFatal(e) => - scribe.error("Uncaught top-level exception", e) - } finally { - System.setOut(stdout) - System.setErr(stderr) - } - - System.exit(0) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/MetalsLogger.scala b/metals/src/main/scala/scala/meta/metals/MetalsLogger.scala index 747d8284c48..cbd88fc8d3f 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsLogger.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsLogger.scala @@ -6,6 +6,12 @@ import scribe.format._ import scribe.writer.FileWriter object MetalsLogger { + def updateFormat(): Unit = { + Logger.root + .clearHandlers() + .withHandler(formatter = defaultFormat) + .replace() + } def setup(logfile: Path): Unit = { val filewriter = FileWriter().path(_ => logfile).autoFlush // Example format: "MyProgram.scala:14 trace foo" diff --git a/metals/src/main/scala/scala/meta/metals/MetalsServices.scala b/metals/src/main/scala/scala/meta/metals/MetalsServices.scala deleted file mode 100644 index b63aff63966..00000000000 --- a/metals/src/main/scala/scala/meta/metals/MetalsServices.scala +++ /dev/null @@ -1,469 +0,0 @@ -package scala.meta.metals - -import io.circe.Json -import io.github.soc.directories.ProjectDirectories -import java.io.File -import java.io.IOException -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes -import java.util.concurrent.Executors -import monix.eval.Coeval -import monix.eval.Task -import monix.execution.Cancelable -import monix.execution.Scheduler -import monix.execution.atomic.Atomic -import monix.execution.schedulers.SchedulerService -import monix.reactive.MulticastStrategy -import monix.reactive.Observable -import monix.reactive.Observer -import monix.reactive.OverflowStrategy -import org.langmeta.inputs.Input -import org.langmeta.internal.semanticdb.schema -import org.langmeta.io.AbsolutePath -import org.langmeta.languageserver.InputEnrichments._ -import scala.meta.jsonrpc.LanguageClient -import scala.meta.jsonrpc.MonixEnrichments._ -import scala.meta.jsonrpc.Response -import scala.meta.jsonrpc.Services -import scala.meta.lsp.Window.showMessage -import scala.meta.lsp.{TextDocument => td} -import scala.meta.lsp.{Workspace => ws} -import scala.meta.lsp.{Lifecycle => lc} -import scala.meta.lsp._ -import scala.meta.metals.compiler.CompilerConfig -import scala.meta.metals.compiler.Cursor -import scala.meta.metals.providers._ -import scala.meta.metals.sbtserver.Sbt -import scala.meta.metals.sbtserver.SbtServer -import scala.meta.metals.search.SymbolIndex -import scala.util.Failure -import scala.util.Success -import scala.util.control.NonFatal - -class MetalsServices( - cwd: AbsolutePath, - client: LanguageClient, - s: MSchedulers -) { - implicit val scheduler: Scheduler = s.global - implicit val languageClient: LanguageClient = client - private var sbtServer: Option[SbtServer] = None - private val tempSourcesDir: AbsolutePath = - cwd.resolve("target").resolve("sources") - // Always run the presentation compiler on the same thread - private val presentationCompilerScheduler: SchedulerService = - Scheduler(Executors.newFixedThreadPool(1)) - def withPC[A](f: => A): Task[Either[Response.Error, A]] = - Task(Right(f)).executeOn(presentationCompilerScheduler) - val (fileSystemSemanticdbSubscriber, fileSystemSemanticdbsPublisher) = - MetalsServices.fileSystemSemanticdbStream(cwd) - val (compilerConfigSubscriber, compilerConfigPublisher) = - MetalsServices.compilerConfigStream(cwd) - val (sourceChangeSubscriber, sourceChangePublisher) = - Observable.multicast[Input.VirtualFile]( - MulticastStrategy.Publish, - OverflowStrategy.DropOld(2) - ) - val (configurationSubscriber, configurationPublisher) = - MetalsServices.configurationStream - val latestConfig: () => Configuration = - configurationPublisher.toFunction0() - val buffers: Buffers = Buffers() - val symbolIndex: SymbolIndex = - SymbolIndex(cwd, buffers, configurationPublisher) - val documentFormattingProvider = - new DocumentFormattingProvider(configurationPublisher, cwd) - - // Effects - val indexedSemanticdbs: Observable[Effects.IndexSemanticdb] = - fileSystemSemanticdbsPublisher.map(symbolIndex.indexDatabase) - val indexedDependencyClasspath: Observable[Effects.IndexSourcesClasspath] = - compilerConfigPublisher.mapTask { config => - symbolIndex.indexDependencyClasspath(config.sourceJars) - } - private var cancelEffects = List.empty[Cancelable] - val effects: List[Observable[Effects]] = List( - configurationPublisher.map(_ => Effects.UpdateBuffers), - indexedDependencyClasspath, - indexedSemanticdbs, - ).map(_.doOnError(MetalsServices.onError)) - - // TODO(olafur): make it easier to invoke fluid services from tests - def initialize( - params: InitializeParams - ): Task[Either[Response.Error, InitializeResult]] = { - scribe.info(s"Initialized with $cwd, $params") - LSPLogger.client = Some(client) - cancelEffects = effects.map(_.subscribe()) - Workspace.initialize(cwd) { onChangedFile(_)(()) } - val commands = WorkspaceCommand.values.map(_.entryName) - val capabilities = ServerCapabilities( - textDocumentSync = Some( - TextDocumentSyncOptions( - openClose = Some(true), - change = Some(TextDocumentSyncKind.Full), - willSave = Some(false), - willSaveWaitUntil = Some(true), - save = Some( - SaveOptions( - includeText = Some(true) - ) - ) - ) - ), - definitionProvider = true, - referencesProvider = true, - documentHighlightProvider = true, - documentSymbolProvider = true, - documentFormattingProvider = true, - hoverProvider = true, - executeCommandProvider = ExecuteCommandOptions(commands), - workspaceSymbolProvider = true, - renameProvider = true, - codeActionProvider = false - ) - Task(Right(InitializeResult(capabilities))) - } - - // TODO(olafur): make it easier to invoke fluid services from tests - def shutdown(): Unit = { - scribe.info("Shutting down...") - cancelEffects.foreach(_.cancel()) - } - - private val shutdownReceived = Atomic(false) - val services: Services = Services - .empty(scribe.Logger.root) - .requestAsync(lc.initialize)(initialize) - .notification(lc.initialized) { _ => - scribe.info("Client is initialized") - } - .request(lc.shutdown) { _ => - shutdown() - shutdownReceived.set(true) - Json.Null - } - .notification(lc.exit) { _ => - // The server should exit with success code 0 if the shutdown request has - // been received before; otherwise with error code 1 - // -- https://microsoft.github.io/language-server-protocol/specification#exit - val code = if (shutdownReceived.get) 0 else 1 - scribe.info(s"exit($code)") - sys.exit(code) - } - .notification(td.didClose) { params => - buffers.closed(Uri(params.textDocument)) - () - } - .notification(td.didOpen) { params => - val input = - Input.VirtualFile(params.textDocument.uri, params.textDocument.text) - buffers.changed(input) - sourceChangeSubscriber.onNext(input) - () - } - .notification(td.didChange) { params => - val changes = params.contentChanges - require(changes.length == 1, s"Expected one change, got $changes") - val input = Input.VirtualFile(params.textDocument.uri, changes.head.text) - buffers.changed(input) - sourceChangeSubscriber.onNext(input) - () - } - .notification(td.willSave) { _ => - () - } - .requestAsync(td.willSaveWaitUntil) { params => - params.reason match { - case TextDocumentSaveReason.Manual if latestConfig().scalafmt.onSave => - scribe.info(s"Formatting on manual save: $params.textDocument") - val uri = Uri(params.textDocument) - documentFormattingProvider.format(uri.toInput(buffers)) - case _ => - Task.now { Right(List()) } - } - } - .notification(td.didSave) { _ => - // if sbt is not connected or the command is empty it won't do anything - sbtExec() - } - .notification(ws.didChangeConfiguration) { params => - params.settings.hcursor.downField("metals").as[Configuration] match { - case Left(err) => - showMessage.notify( - ShowMessageParams(MessageType.Error, err.toString) - ) - case Right(conf) => - scribe.info(s"Configuration updated $conf") - configurationSubscriber.onNext(conf) - } - } - .notification(ws.didChangeWatchedFiles) { params => - params.changes.foreach { - case FileEvent( - Uri(path), - FileChangeType.Created | FileChangeType.Changed - ) => - onChangedFile(path.toAbsolutePath) { - scribe.warn(s"Unknown file extension for path $path") - } - - case event => - scribe.warn(s"Unhandled file event: $event") - () - } - () - } - .request(td.definition) { params => - DefinitionProvider.definition( - symbolIndex, - Uri(params.textDocument.uri), - params.position, - tempSourcesDir - ) - } - .request(td.references) { params => - ReferencesProvider.references( - symbolIndex, - Uri(params.textDocument.uri), - params.position, - params.context - ) - } - .request(ws.symbol) { params => - symbolIndex.workspaceSymbols(params.query) - } - .request(td.documentHighlight) { params => - if (latestConfig().highlight.enabled) { - DocumentHighlightProvider.highlight( - symbolIndex, - Uri(params.textDocument.uri), - params.position - ) - } else DocumentHighlightProvider.empty - } - .request(td.documentSymbol) { params => - val uri = Uri(params.textDocument.uri) - buffers.source(uri) match { - case Some(source) => DocumentSymbolProvider.documentSymbols(uri, source) - case None => DocumentSymbolProvider.empty - } - } - .requestAsync(td.formatting) { params => - val uri = Uri(params.textDocument) - documentFormattingProvider.format(uri.toInput(buffers)) - } - .request(td.rename) { params => - RenameProvider.rename(params, symbolIndex) - } - .requestAsync(ws.executeCommand) { params => - scribe.info(s"executeCommand $params") - WorkspaceCommand.withNameOption(params.command) match { - case None => - Task { - val msg = s"Unknown command ${params.command}" - scribe.error(msg) - Left(Response.invalidParams(msg)) - } - case Some(command) => - executeCommand(command, params) - } - } - - import WorkspaceCommand._ - val ok = Right(Json.Null) - private def executeCommand( - command: WorkspaceCommand, - params: ExecuteCommandParams - ): Task[Either[Response.Error, Json]] = command match { - case ClearIndexCache => - Task { - scribe.info("Clearing the index cache") - MetalsServices.clearCacheDirectory() - symbolIndex.clearIndex() - Right(Json.Null) - } - case SbtConnect => - Task { - SbtServer.readVersion(cwd) match { - case Some(ver) if ver.startsWith("0.") || ver.startsWith("1.0") => - showMessage.warn( - s"sbt v${ver} used in this project doesn't have server functionality. " + - "Upgrade to sbt v1.1+ to enjoy Metals integration with the sbt server." - ) - case _ => - connectToSbtServer() - } - Right(Json.Null) - } - } - - private def sbtExec(commands: String*): Task[Unit] = { - val cmd = commands.mkString("; ", "; ", "") - sbtServer match { - case None => - scribe.warn( - s"Trying to execute commands when there is no connected sbt server: ${cmd}" - ) - Task.unit - case Some(sbt) => - scribe.debug(s"sbt/exec: ${cmd}") - Sbt - .exec(cmd)(sbt.client) - .onErrorRecover { - case NonFatal(err) => - // TODO(olafur) figure out why this "broken pipe" is not getting - // caught here. - scribe.error("Failed to send sbt compile", err) - showMessage.warn( - "Lost connection to sbt server. " + - "Restart the sbt session and run the 'Re-connect to sbt server' command" - ) - } - } - } - private def sbtExec(): Task[Unit] = sbtExec(latestConfig().sbt.command) - - private val loadPluginJars: Coeval[List[AbsolutePath]] = Coeval.evalOnce { - Jars.fetch("ch.epfl.scala", "load-plugin_2.12", "0.1.0+2-496ac670") - } - - private def sbtExecWithMetalsPlugin(commands: String*): Task[Unit] = { - val metalsPluginModule = ModuleID( - "org.scalameta", - "sbt-metals", - scala.meta.metals.internal.BuildInfo.version - ) - val metalsPluginRef = "scala.meta.sbt.MetalsPlugin" - val loadPluginClasspath = loadPluginJars.value.mkString(File.pathSeparator) - val loadCommands = Seq( - s"apply -cp ${loadPluginClasspath} ch.epfl.scala.loadplugin.LoadPlugin", - s"""if-absent ${metalsPluginRef} "load-plugin ${metalsPluginModule} ${metalsPluginRef}"""", - ) - sbtExec(loadCommands ++ commands: _*) - } - - private def connectToSbtServer(): Unit = { - sbtServer.foreach(_.disconnect()) - val services = SbtServer.forwardingServices(client, latestConfig) - SbtServer.connect(cwd, services)(s.sbt).foreach { - case Left(err) => showMessage.error(err) - case Right(server) => - val msg = "Established connection with sbt server 😎" - scribe.info(msg) - showMessage.info(msg) - sbtServer = Some(server) - cancelEffects ::= server.runningServer - server.runningServer.onComplete { - case Failure(err) => - scribe.error(s"Unexpected failure from sbt server connection", err) - showMessage.error(err.getMessage) - case Success(()) => - sbtServer = None - showMessage.warn("Disconnected from sbt server") - } - sbtExecWithMetalsPlugin("semanticdbEnable").runAsync.foreach { _ => - scribe.info("semanticdb-scalac is enabled") - } - sbtExec().runAsync // run configured command right away - } - } - - private def toCursor( - td: TextDocumentIdentifier, - pos: Position - ): Cursor = { - val contents = buffers.read(td) - val input = Input.VirtualFile(td.uri, contents) - val offset = input.toOffset(pos) - Cursor(Uri(td.uri), contents, offset) - } - - private def onChangedFile( - path: AbsolutePath - )(fallback: => Unit): Unit = { - scribe.info(s"File $path changed") - path.toRelative(cwd) match { - case Semanticdbs.File() => - fileSystemSemanticdbSubscriber.onNext(path) - case CompilerConfig.File() => - compilerConfigSubscriber.onNext(path) - case SbtServer.ActiveJson() => - connectToSbtServer() - case _ => - fallback - } - } -} - -object MetalsServices { - lazy val cacheDirectory: AbsolutePath = { - val path = AbsolutePath( - ProjectDirectories.fromProjectName("metals").projectCacheDir - ) - Files.createDirectories(path.toNIO) - path - } - - def clearCacheDirectory(): Unit = - Files.walkFileTree( - cacheDirectory.toNIO, - new SimpleFileVisitor[Path] { - override def visitFile( - file: Path, - attr: BasicFileAttributes - ): FileVisitResult = { - Files.delete(file) - FileVisitResult.CONTINUE - } - override def postVisitDirectory( - dir: Path, - exc: IOException - ): FileVisitResult = { - Files.delete(dir) - FileVisitResult.CONTINUE - } - } - ) - - def compilerConfigStream(cwd: AbsolutePath)( - implicit scheduler: Scheduler - ): (Observer.Sync[AbsolutePath], Observable[CompilerConfig]) = { - val (subscriber, publisher) = multicast[AbsolutePath]() - val compilerConfigPublished = publisher - .map(path => CompilerConfig.fromPath(path)) - subscriber -> compilerConfigPublished - } - - def fileSystemSemanticdbStream(cwd: AbsolutePath)( - implicit scheduler: Scheduler - ): (Observer.Sync[AbsolutePath], Observable[schema.Database]) = { - val (subscriber, publisher) = multicast[AbsolutePath]() - val semanticdbPublisher = publisher - .map(path => Semanticdbs.loadFromFile(semanticdbPath = path, cwd)) - subscriber -> semanticdbPublisher - } - - def configurationStream( - implicit scheduler: Scheduler - ): (Observer.Sync[Configuration], Observable[Configuration]) = { - val (subscriber, publisher) = - multicast[Configuration](MulticastStrategy.behavior(Configuration())) - val configurationPublisher = publisher - subscriber -> configurationPublisher - } - - def multicast[A]( - strategy: MulticastStrategy[A] = MulticastStrategy.publish - )(implicit s: Scheduler): (Observer.Sync[A], Observable[A]) = { - val (sub, pub) = Observable.multicast[A](strategy) - (sub, pub.doOnError(onError)) - } - - def onError(e: Throwable): Unit = { - scribe.error(e.getMessage, e) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/Models.scala b/metals/src/main/scala/scala/meta/metals/Models.scala deleted file mode 100644 index 1c6e8a5f5e9..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Models.scala +++ /dev/null @@ -1,21 +0,0 @@ -package scala.meta.metals - -import io.circe.Json -import io.circe.generic.JsonCodec -import org.langmeta.io.AbsolutePath - -@JsonCodec case class ActiveJson(uri: String) - -@JsonCodec case class SettingParams(setting: String) -@JsonCodec case class SettingResult(value: Json, contentType: Json) - -@JsonCodec case class SbtInitializeParams( - initializationOptions: Json = Json.obj() -) -@JsonCodec case class SbtInitializeResult(json: Json) - -@JsonCodec case class SbtExecParams(commandLine: String) - -case class MissingActiveJson(path: AbsolutePath) - extends Exception(s"sbt-server 1.1+ is not running, $path does not exist") -case class SbtServerConnectionError(msg: String) extends Exception(msg) diff --git a/metals/src/main/scala/scala/meta/metals/Parser.scala b/metals/src/main/scala/scala/meta/metals/Parser.scala deleted file mode 100644 index 5906d38cb0b..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Parser.scala +++ /dev/null @@ -1,27 +0,0 @@ -package scala.meta.metals - -import scala.meta.Dialect -import scala.meta.Source -import scala.meta.parsers.Parsed -import org.langmeta.inputs.Position -import org.langmeta.semanticdb.Document -import scalafix.internal.config.ScalafixConfig -import org.langmeta.inputs.Input - -// Small utility to parse inputs into scala.meta.Tree, -// this is missing in the API after semanticdb went language agnostics with langmeta. -object Parser { - def parse(document: Document): Parsed[Source] = - Dialect.standards.get(document.language) match { - case Some(dialect) => - dialect(document.input).parse[Source] - case None => - val err = s"Unknown dialect ${document.language}" - Parsed.Error(Position.None, err, new IllegalArgumentException(err)) - } - - def parse(input: Input): Parsed[Source] = - ScalafixConfig.DefaultDialect(input).parse[Source] - def parse(content: String): Parsed[Source] = - parse(Input.String(content)) -} diff --git a/metals/src/main/scala/scala/meta/metals/ScalametaEnrichments.scala b/metals/src/main/scala/scala/meta/metals/ScalametaEnrichments.scala deleted file mode 100644 index 32e8cbf2ce5..00000000000 --- a/metals/src/main/scala/scala/meta/metals/ScalametaEnrichments.scala +++ /dev/null @@ -1,322 +0,0 @@ -package scala.meta.metals - -import java.net.URI -import java.nio.charset.StandardCharsets -import java.nio.file.Files -import java.nio.file.Paths -import scala.meta.lsp.Diagnostic -import scala.meta.lsp.Location -import scala.meta.lsp.Position -import scala.meta.metals.{index => i} -import org.langmeta.internal.semanticdb.{schema => s} -import scala.{meta => m} -import scala.meta.lsp.SymbolKind -import scala.meta.{lsp => l} -import org.langmeta.internal.io.FileIO -import org.langmeta.io.AbsolutePath - -// Extension methods for convenient reuse of data conversions between -// scala.meta._ and language.types._ -object ScalametaEnrichments { - - implicit class XtensionMessageLSP(val msg: m.Message) extends AnyVal { - def toLSP(source: String): Diagnostic = - l.Diagnostic( - range = msg.position.toRange, - severity = Some(msg.severity.toLSP), - code = None, - source = Some(source), - message = msg.text - ) - } - - implicit class XtensionSeverityLSP(val severity: m.Severity) extends AnyVal { - def toLSP: l.DiagnosticSeverity = severity match { - case m.Severity.Info => l.DiagnosticSeverity.Information - case m.Severity.Warning => l.DiagnosticSeverity.Warning - case m.Severity.Error => l.DiagnosticSeverity.Error - } - } - - implicit class XtensionTreeLSP(val tree: m.Tree) extends AnyVal { - import scala.meta._ - - // TODO(alexey) function inside a block/if/for/etc.? - def isFunction: Boolean = { - val tpeOpt: Option[Type] = tree match { - case d: Decl.Val => Some(d.decltpe) - case d: Decl.Var => Some(d.decltpe) - case d: Defn.Val => d.decltpe - case d: Defn.Var => d.decltpe - case _ => None - } - tpeOpt.filter(_.is[Type.Function]).nonEmpty - } - - // NOTE: we care only about descendants of Decl, Defn and Pkg[.Object] (see documentSymbols implementation) - def symbolKind: SymbolKind = tree match { - case f if f.isFunction => SymbolKind.Function - case _: Decl.Var | _: Defn.Var => SymbolKind.Variable - case _: Decl.Val | _: Defn.Val => SymbolKind.Constant - case _: Decl.Def | _: Defn.Def => SymbolKind.Method - case _: Decl.Type | _: Defn.Type => SymbolKind.Field - case _: Defn.Macro => SymbolKind.Constructor - case _: Defn.Class => SymbolKind.Class - case _: Defn.Trait => SymbolKind.Interface - case _: Defn.Object => SymbolKind.Module - case _: Pkg.Object => SymbolKind.Namespace - case _: Pkg => SymbolKind.Package - case _: Type.Param => SymbolKind.TypeParameter - case _: Lit.Null => SymbolKind.Null - // TODO(alexey) are these kinds useful? - // case ??? => SymbolKind.Enum - // case ??? => SymbolKind.String - // case ??? => SymbolKind.Number - // case ??? => SymbolKind.Boolean - // case ??? => SymbolKind.Array - case _ => SymbolKind.Field - } - - /** Fully qualified name for packages, normal name for everything else */ - def qualifiedName: Option[String] = tree match { - case Term.Name(name) => Some(name) - case Term.Select(qual, name) => - qual.qualifiedName.map { prefix => - s"${prefix}.${name}" - } - case Pkg(sel: Term.Select, _) => sel.qualifiedName - case m: Member => Some(m.name.value) - case _ => None - } - - /** All names within the node. - * - if it's a package, it will have its qualified name: `package foo.bar.buh` - * - if it's a val/var, it may contain several names in the pattern: `val (x, y, z) = ...` - * - for everything else it's just its normal name (if it has one) - */ - private def patternNames(pats: List[Pat]): Seq[String] = - pats.flatMap { _.collect { case Pat.Var(name) => name.value } } - def names: Seq[String] = tree match { - case t: Pkg => t.qualifiedName.toSeq - case t: Defn.Val => patternNames(t.pats) - case t: Decl.Val => patternNames(t.pats) - case t: Defn.Var => patternNames(t.pats) - case t: Decl.Var => patternNames(t.pats) - case t: Member => Seq(t.name.value) - case _ => Seq() - } - } - implicit class XtensionInputLSP(val input: m.Input) extends AnyVal { - def contents: String = input match { - case m.Input.VirtualFile(_, value) => value - case _ => new String(input.chars) - } - } - implicit class XtensionIndexPosition(val pos: i.Position) extends AnyVal { - def pretty: String = - s"${pos.uri.replaceFirst(".*/", "")} [${pos.range.map(_.pretty).getOrElse("")}]" - - def toLocation: Location = { - l.Location( - pos.uri, - pos.range.get.toRange - ) - } - } - implicit class XtensionIndexRange(val range: i.Range) extends AnyVal { - def pretty: String = - f"${range.startLine}%3d:${range.startColumn}%3d|${range.endLine}%3d:${range.endColumn}%3d" - def toRange: l.Range = l.Range( - Position(line = range.startLine, character = range.startColumn), - l.Position(line = range.endLine, character = range.endColumn) - ) - def contains(pos: m.Position): Boolean = { - range.startLine <= pos.startLine && - range.startColumn <= pos.startColumn && - range.endLine >= pos.endLine && - range.endColumn >= pos.endColumn - } - def contains(line: Int, column: Int): Boolean = { - range.startLine <= line && - range.startColumn <= column && - range.endLine >= line && - range.endColumn >= column - } - } - implicit class XtensionAbsolutePathLSP(val path: m.AbsolutePath) - extends AnyVal { - def toLocation(pos: m.Position): Location = - l.Location(path.toLanguageServerUri, pos.toRange) - def toLanguageServerUri: String = "file:" + path.toString() - } - implicit class XtensionPositionRangeLSP(val pos: m.Position) extends AnyVal { - def toIndexRange: i.Range = i.Range( - startLine = pos.startLine, - startColumn = pos.startColumn, - endLine = pos.endLine, - endColumn = pos.endColumn - ) - def contains(offset: Int): Boolean = - if (pos.start == pos.end) pos.end == offset - else { - pos.start <= offset && - pos.end > offset - } - def location: String = - s"${pos.input.syntax}:${pos.startLine}:${pos.startColumn}" - def toRange: l.Range = l.Range( - l.Position(line = pos.startLine, character = pos.startColumn), - l.Position(line = pos.endLine, character = pos.endColumn) - ) - } - implicit class XtensionSymbolGlobalTerm(val sym: m.Symbol.Global) - extends AnyVal { - def toType: m.Symbol.Global = sym match { - case m.Symbol.Global(owner, m.Signature.Term(name)) => - m.Symbol.Global(owner, m.Signature.Type(name)) - case _ => sym - } - def toTerm: m.Symbol.Global = sym match { - case m.Symbol.Global(owner, m.Signature.Type(name)) => - m.Symbol.Global(owner, m.Signature.Term(name)) - case _ => sym - } - } - implicit class XtensionSymbol(val sym: m.Symbol) extends AnyVal { - import scala.meta._ - - /** Returns a list of fallback symbols that can act instead of given symbol. */ - // TODO(alexey) review/refine this list - def referenceAlternatives: List[Symbol] = { - List( - caseClassCompanionToType, - caseClassApplyOrCopyParams - ).flatten - } - - /** Returns a list of fallback symbols that can act instead of given symbol. */ - // TODO(alexey) review/refine this list - def definitionAlternative: List[Symbol] = { - List( - caseClassCompanionToType, - caseClassApplyOrCopy, - caseClassApplyOrCopyParams, - methodToVal - ).flatten - } - - /** If `case class A(a: Int)` and there is no companion object, resolve - * `A` in `A(1)` to the class definition. - */ - def caseClassCompanionToType: Option[Symbol] = Option(sym).collect { - case Symbol.Global(owner, Signature.Term(name)) => - Symbol.Global(owner, Signature.Type(name)) - } - - /** If `case class Foo(a: Int)`, then resolve - * `a` in `Foo.apply(a = 1)`, and - * `a` in `Foo(1).copy(a = 2)` - * to the `Foo.a` primary constructor definition. - */ - def caseClassApplyOrCopyParams: Option[Symbol] = Option(sym).collect { - case Symbol.Global( - Symbol.Global( - Symbol.Global(owner, signature), - Signature.Method("copy" | "apply", _) - ), - param: Signature.TermParameter - ) => - Symbol.Global( - Symbol.Global(owner, Signature.Type(signature.name)), - param - ) - } - - /** If `case class Foo(a: Int)`, then resolve - * `apply` in `Foo.apply(1)`, and - * `copy` in `Foo(1).copy(a = 2)` - * to the `Foo` class definition. - */ - def caseClassApplyOrCopy: Option[Symbol] = Option(sym).collect { - case Symbol.Global( - Symbol.Global(owner, signature), - Signature.Method("apply" | "copy", _) - ) => - Symbol.Global(owner, Signature.Type(signature.name)) - } - - /** Fallback to the val term for a def with multiple params */ - def methodToVal: Option[Symbol] = Option(sym).collect { - case Symbol.Global(owner, Signature.Method(name, _)) => - Symbol.Global(owner, Signature.Term(name)) - } - } - - implicit class XtensionLocation(val loc: Location) extends AnyVal { - - /** A workaround for locations referring to jars */ - def toNonJar(destination: AbsolutePath): Location = { - if (loc.uri.startsWith("jar:file")) { - val newURI = - createFileInWorkspaceTarget(URI.create(loc.uri), destination) - loc.copy(uri = newURI.toString) - } else loc - } - - // Writes the contents from in-memory source file to a file in the target/source/* - // directory of the workspace. vscode has support for TextDocumentContentProvider - // which can provide hooks to open readonly views for custom uri schemes: - // https://code.visualstudio.com/docs/extensionAPI/vscode-api#TextDocumentContentProvider - // However, that is a vscode only solution and we'd like this work for all - // text editors. Therefore, we write instead the file contents to disk in order to - // return a file: uri. - // TODO: Fix this with https://github.com/scalameta/metals/issues/36 - private def createFileInWorkspaceTarget( - uri: URI, - destination: AbsolutePath - ): URI = { - // scribe.info(s"Jumping into uri $uri, writing contents to file in target file") - val contents = - new String(FileIO.readAllBytes(uri), StandardCharsets.UTF_8) - // HACK(olafur) URIs are not typesafe, jar:file://blah.scala will return - // null for `.getPath`. We should come up with nicer APIs to deal with this - // kinda stuff. - val path: String = - if (uri.getPath == null) - uri.getSchemeSpecificPart - else uri.getPath - val filename = Paths.get(path).getFileName - - Files.createDirectories(destination.toNIO) - val out = destination.toNIO.resolve(filename) - Files.write(out, contents.getBytes(StandardCharsets.UTF_8)) - out.toUri - } - } - - implicit class XtensionSymbolData(val data: i.SymbolData) extends AnyVal { - - /** Returns reference positions for the given symbol index data - * @param withDefinition if set to `true` will include symbol definition location - */ - def referencePositions(withDefinition: Boolean): Set[i.Position] = { - val defPosition = if (withDefinition) data.definition else None - - val refPositions = for { - (uri, rangeSet) <- data.references - range <- rangeSet.ranges - } yield i.Position(uri, Some(range)) - - (defPosition.toSet ++ refPositions.toSet) - .filterNot { _.uri.startsWith("jar:file") } // definition may refer to a jar - } - - } - implicit class XtensionSchemaDocument(val document: s.Document) - extends AnyVal { - - /** Returns scala.meta.Document from protobuf schema.Document */ - def toMetaDocument: m.Document = - s.Database(document :: Nil).toDb(None).documents.head - } -} diff --git a/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala b/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala deleted file mode 100644 index 30d01e96fe8..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala +++ /dev/null @@ -1,39 +0,0 @@ -package scala.meta.metals - -import java.nio.file.Files -import org.langmeta.internal.io.PathIO -import org.langmeta.internal.semanticdb.schema.Database -import org.langmeta.io.AbsolutePath -import org.langmeta.io.RelativePath - -object Semanticdbs { - - object File { - def unapply(path: RelativePath): Boolean = - PathIO.extension(path.toNIO) == "semanticdb" - } - - def loadFromFile( - semanticdbPath: AbsolutePath, - cwd: AbsolutePath - ): Database = { - val bytes = Files.readAllBytes(semanticdbPath.toNIO) - val sdb = Database.parseFrom(bytes) - Database( - sdb.documents.map { d => - val filename = cwd.resolve(d.filename).toURI.toString() - scribe.info(s"Loading file $filename") - d.withFilename(filename) - .withNames { - // This should be done inside semanticdb-scalac. - val names = d.names.toArray - util.Sorting.quickSort(names)( - Ordering.by(_.position.fold(-1)(_.start)) - ) - names - } - } - ) - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/Uri.scala b/metals/src/main/scala/scala/meta/metals/Uri.scala deleted file mode 100644 index 09ccee08344..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Uri.scala +++ /dev/null @@ -1,53 +0,0 @@ -package scala.meta.metals - -import java.net.URI -import java.nio.file.Path -import java.nio.file.Paths -import scala.meta.lsp.TextDocumentIdentifier -import scala.meta.lsp.VersionedTextDocumentIdentifier -import scala.meta.lsp.VersionedTextDocumentIdentifier -import org.langmeta.inputs.Input -import org.langmeta.io.AbsolutePath - -/** - * Wrapper for a string representing a URI. - * - * The value is a String and not java.net.URI because - * - URI has a lot of methods that return null and we don't use anyways - * - URI supports any scheme while we are only interested in a couple schemes - * - Both file:///path and file:/path are valid URIs while we only use - * file:///path in this project in order to support storing them as strings. For context, see https://github.com/scalameta/metals/pull/127#issuecomment-351880150 - */ -sealed abstract case class Uri(value: String) { - // Runtime check because wrapping constructor in Option[Uri] is too cumbersome - require(isJar || isFile, s"$value must start with file:///path or jar:") - def isJar: Boolean = - value.startsWith("jar:") - def isFile: Boolean = - value.startsWith("file:///") && - !value.startsWith("file:////") - def toInput(buffers: Buffers): Input.VirtualFile = - Input.VirtualFile(value, buffers.read(this)) - def toURI: URI = URI.create(value) - def toPath: Path = Paths.get(toURI) - def toAbsolutePath: AbsolutePath = AbsolutePath(toPath) -} - -object Uri { - def apply(uri: String): Uri = Uri(URI.create(uri)) - def file(path: String): Uri = { - val slash = if (path.startsWith("/")) "" else "/" - Uri(s"file:$slash${path.replace(' ', '-')}") - } - def apply(td: TextDocumentIdentifier): Uri = Uri(td.uri) - def apply(td: VersionedTextDocumentIdentifier): Uri = Uri(td.uri) - def apply(path: AbsolutePath): Uri = Uri(path.toURI) - def apply(uri: URI): Uri = - if (uri.getScheme == "file") { - // nio.Path.toUri.toString produces file:/// while LSP expected file:/ - new Uri(s"file://${uri.getPath}") {} - } else { - new Uri(uri.toString) {} - } - def unapply(arg: String): Option[Uri] = Some(Uri(URI.create(arg))) -} diff --git a/metals/src/main/scala/scala/meta/metals/Workspace.scala b/metals/src/main/scala/scala/meta/metals/Workspace.scala deleted file mode 100644 index 4044744f5d1..00000000000 --- a/metals/src/main/scala/scala/meta/metals/Workspace.scala +++ /dev/null @@ -1,48 +0,0 @@ -package scala.meta.metals - -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes -import org.langmeta.internal.io.FileIO -import org.langmeta.io.AbsolutePath -import scala.meta.metals.compiler.CompilerConfig -import scala.meta.metals.sbtserver.SbtServer - -object Workspace { - - def compilerConfigFiles(cwd: AbsolutePath): Iterable[AbsolutePath] = { - val configDir = CompilerConfig.dir(cwd) - if (configDir.isDirectory) { - FileIO.listAllFilesRecursively(configDir) - } else { - Nil - } - } - - def initialize(cwd: AbsolutePath)( - action: AbsolutePath => Unit - ): Unit = { - compilerConfigFiles(cwd).foreach(action) - - Files.walkFileTree( - cwd.toNIO, - new SimpleFileVisitor[Path] { - override def visitFile( - file: Path, - attrs: BasicFileAttributes - ): FileVisitResult = { - val absPath = AbsolutePath(file) - absPath.toRelative(cwd) match { - case Semanticdbs.File() => action(absPath) - case _ => // ignore, to avoid spamming console. - } - FileVisitResult.CONTINUE - } - } - ) - - action(SbtServer.ActiveJson(cwd)) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/WorkspaceCommand.scala b/metals/src/main/scala/scala/meta/metals/WorkspaceCommand.scala deleted file mode 100644 index 4844f09e315..00000000000 --- a/metals/src/main/scala/scala/meta/metals/WorkspaceCommand.scala +++ /dev/null @@ -1,16 +0,0 @@ -package scala.meta.metals - -import enumeratum.Enum -import enumeratum.EnumEntry -import enumeratum.EnumEntry.Uncapitalised - -sealed trait WorkspaceCommand extends EnumEntry with Uncapitalised - -case object WorkspaceCommand extends Enum[WorkspaceCommand] { - - case object ClearIndexCache extends WorkspaceCommand - case object SbtConnect extends WorkspaceCommand - - val values = findValues - -} diff --git a/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala b/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala deleted file mode 100644 index 43696aedc4c..00000000000 --- a/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala +++ /dev/null @@ -1,136 +0,0 @@ -package scala.meta.metals.compiler - -import java.nio.file.Files -import java.nio.file.Paths -import java.util.Properties -import scala.tools.nsc.settings.ScalaVersion -import scala.tools.nsc.settings.SpecificScalaVersion -import org.langmeta.internal.io.PathIO -import org.langmeta.io.AbsolutePath -import org.langmeta.io.RelativePath -import scala.util.control.NonFatal - -/** - * Configuration to load up a presentation compiler. - * - * In sbt, one compiler config typically corresponds to one project+config. - * For example one sbt project with test/main/it configurations has three - * CompilerConfig. - * - * @param sources list of source files for this project - * @param unmanagedSourceDirectories list of directories that are manually edited, not auto-generated - * @param managedSourceDirectories list of directories that contain auto-generated code - * @param scalacOptions space separated list of flags to pass to the Scala compiler - * @param dependencyClasspath File.pathSeparated list of *.jar and classDirectories. - * Includes both dependencyClasspath and classDirectory. - * @param classDirectory The output directory where *.class files are emitted - * for this project. - * @param sourceJars File.pathSeparated list of *-sources.jar from the - * dependencyClasspath. - * @param origin Path to this .compilerconfig file. - */ -case class CompilerConfig( - sources: List[AbsolutePath], - unmanagedSourceDirectories: List[AbsolutePath], - managedSourceDirectories: List[AbsolutePath], - scalacOptions: List[String], - classDirectory: AbsolutePath, - dependencyClasspath: List[AbsolutePath], - sourceJars: List[AbsolutePath], - origin: AbsolutePath, - scalaVersion: SpecificScalaVersion -) { - lazy val sourceDirectories: List[AbsolutePath] = - unmanagedSourceDirectories ++ managedSourceDirectories - override def toString: String = - s"CompilerConfig(" + - s"sources={+${sources.length}}, " + - s"scalacOptions=${scalacOptions.mkString(" ")}, " + - s"dependencyClasspath={+${dependencyClasspath.length}}, " + - s"classDirectory=$classDirectory, " + - s"sourceJars={+${sourceJars.length}}, " + - s"origin=$origin, " + - s"scalaVersion=${scalaVersion.unparse})" - - def classpath: String = - (classDirectory :: dependencyClasspath).mkString(java.io.File.pathSeparator) -} - -object CompilerConfig { - private val relativeDir: RelativePath = - RelativePath(".metals").resolve("buildinfo") - - def dir(cwd: AbsolutePath): AbsolutePath = - cwd.resolve(relativeDir) - - object File { - def unapply(path: RelativePath): Boolean = { - path.toNIO.startsWith(relativeDir.toNIO) && - PathIO.extension(path.toNIO) == "properties" - } - } - - def jdkSources: Option[AbsolutePath] = - for { - javaHome <- sys.props.get("java.home") - jdkSources = Paths.get(javaHome).getParent.resolve("src.zip") - if Files.isRegularFile(jdkSources) - } yield AbsolutePath(jdkSources) - - def fromPath( - path: AbsolutePath - )(implicit cwd: AbsolutePath): CompilerConfig = { - val input = Files.newInputStream(path.toNIO) - try { - val props = new Properties() - props.load(input) - fromProperties(props, path) - } catch { - case NonFatal(e) => - scribe.error(s"Failed to parse $path", e) - throw new IllegalArgumentException(path.toString(), e) - } finally { - input.close() - } - } - - def fromProperties( - props: Properties, - origin: AbsolutePath - )(implicit cwd: AbsolutePath): CompilerConfig = { - - def getPaths(implicit name: sourcecode.Name): List[AbsolutePath] = { - Option(props.getProperty(name.value)) match { - case None => - scribe.warn(s"$origin: Missing key '${name.value}'") - Nil - case Some(paths) => - paths - .split(java.io.File.pathSeparator) - .iterator - .map(AbsolutePath(_)) - .toList - } - } - val sources = getPaths - val unmanagedSourceDirectories = getPaths - val managedSourceDirectories = getPaths - val scalacOptions = props.getProperty("scalacOptions").split(" ").toList - val dependencyClasspath = getPaths - val sourceJars = getPaths - val classDirectory = AbsolutePath(props.getProperty("classDirectory")) - val scalaVersion = ScalaVersion(props.getProperty("scalaVersion")) - .asInstanceOf[SpecificScalaVersion] - CompilerConfig( - sources, - unmanagedSourceDirectories, - managedSourceDirectories, - scalacOptions, - classDirectory, - dependencyClasspath, - sourceJars, - origin, - scalaVersion - ) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/compiler/Cursor.scala b/metals/src/main/scala/scala/meta/metals/compiler/Cursor.scala deleted file mode 100644 index b064e986eda..00000000000 --- a/metals/src/main/scala/scala/meta/metals/compiler/Cursor.scala +++ /dev/null @@ -1,5 +0,0 @@ -package scala.meta.metals.compiler - -import scala.meta.metals.Uri - -case class Cursor(uri: Uri, contents: String, offset: Int) diff --git a/metals/src/main/scala/scala/meta/metals/index/SymbolData.scala b/metals/src/main/scala/scala/meta/metals/index/SymbolData.scala deleted file mode 100644 index b200ce9e6dc..00000000000 --- a/metals/src/main/scala/scala/meta/metals/index/SymbolData.scala +++ /dev/null @@ -1,27 +0,0 @@ -package scala.meta.metals.index - -case class SymbolData( - symbol: String, - definition: Option[Position] = None, - references: Map[String, Ranges] = Map.empty, - flags: Long = 0L, - name: String = "", - signature: String = "" -) - -case class Position( - uri: String, - range: Option[Range] -) - -case class Ranges(ranges: Seq[Range] = Nil) { - def addRanges(range: Range*): Ranges = - copy(ranges = range ++ ranges) -} - -case class Range( - startLine: Int, - startColumn: Int, - endLine: Int, - endColumn: Int -) diff --git a/metals/src/main/scala/scala/meta/metals/mtags/JavaMtags.scala b/metals/src/main/scala/scala/meta/metals/mtags/JavaMtags.scala deleted file mode 100644 index b02c26c734c..00000000000 --- a/metals/src/main/scala/scala/meta/metals/mtags/JavaMtags.scala +++ /dev/null @@ -1,116 +0,0 @@ -package scala.meta.metals.mtags - -import java.io.StringReader - -import com.thoughtworks.qdox._ -import com.thoughtworks.qdox.model._ -import org.langmeta.inputs.{Input, Position} -import org.langmeta.languageserver.InputEnrichments._ - -import scala.meta.{CLASS, DEF, OBJECT, PACKAGE, TRAIT, VAL, VAR} - -object JavaMtags { - private implicit class XtensionJavaModel(val m: JavaModel) extends AnyVal { - def lineNumber: Int = m.getLineNumber - 1 - } - - def index(input: Input.VirtualFile): MtagsIndexer = { - val builder = new JavaProjectBuilder() - new MtagsIndexer { self => - override def indexRoot(): Unit = { - try { - val source = builder.addSource(new StringReader(input.value)) - if (source.getPackage != null) { - source.getPackageName.split("\\.").foreach { p => - term(p, toRangePosition(source.getPackage.lineNumber, p), PACKAGE) - } - } - source.getClasses.forEach(visitClass) - } catch { - case _: NullPointerException => () - // Hitting on this fellow here indexing the JDK - // Error indexing file:///Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/src.zip/java/time/temporal/IsoFields.java - // java.lang.NullPointerException: null - // at com.thoughtworks.qdox.builder.impl.ModelBuilder.createTypeVariable(ModelBuilder.java:503) - // at com.thoughtworks.qdox.builder.impl.ModelBuilder.endMethod(ModelBuilder.java:470) - // TODO(olafur) report bug to qdox. - } - } - - /** Computes the start/end offsets from a name in a line number. - * - * Applies a simple heuristic to find the name: the first occurence of - * name in that line. If the name does not appear in the line then - * 0 is returned. If the name appears for example in the return type - * of a method then we get the position of the return type, not the - * end of the world. - */ - def toRangePosition(line: Int, name: String): Position = { - val offset = input.toOffset(line, 0) - val columnAndLength = { - val fromIndex = { - // HACK(olafur) avoid hitting on substrings of "package". - if (input.value.startsWith("package", offset)) "package".length - else offset - } - val idx = input.value.indexOf(name, fromIndex) - if (idx == -1) (0, 0) - else (idx - offset, name.length) - } - input.toPosition( - line, - columnAndLength._1, - line, - columnAndLength._1 + columnAndLength._2 - ) - } - - def visitFields[T <: JavaMember](fields: java.util.List[T]): Unit = - if (fields == null) () - else fields.forEach(visitMember) - - def visitClasses(classes: java.util.List[JavaClass]): Unit = - if (classes == null) () - else classes.forEach(visitClass) - - def visitClass(cls: JavaClass): Unit = - withOwner(owner(cls.isStatic)) { - val flags = if (cls.isInterface) TRAIT else CLASS - val pos = toRangePosition(cls.lineNumber, cls.getName) - if (cls.isEnum) { - term(cls.getName, pos, OBJECT) - } else { - withOwner() { term(cls.getName, pos, OBJECT) } // object - tpe(cls.getName, pos, flags) - } - visitClasses(cls.getNestedClasses) - visitFields(cls.getMethods) - visitFields(cls.getFields) - visitFields(cls.getEnumConstants) - } - - def visitMember[T <: JavaMember](m: T): Unit = - withOwner(owner(m.isStatic)) { - val name = m.getName - val line = m match { - case c: JavaMethod => c.lineNumber - case c: JavaField => c.lineNumber - // TODO(olafur) handle constructos - case _ => 0 - } - val pos = toRangePosition(line, name) - val flags: Long = m match { - case c: JavaMethod => DEF - case c: JavaField => - if (c.isFinal || c.isEnumConstant) VAL - else VAR - // TODO(olafur) handle constructos - case _ => 0L - } - term(name, pos, flags) - } - override def language: String = "Java" - } - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/mtags/Mtags.scala b/metals/src/main/scala/scala/meta/metals/mtags/Mtags.scala deleted file mode 100644 index 0c9a070f697..00000000000 --- a/metals/src/main/scala/scala/meta/metals/mtags/Mtags.scala +++ /dev/null @@ -1,263 +0,0 @@ -package scala.meta.metals.mtags - -import java.io.IOException -import java.net.URI -import java.nio.charset.StandardCharsets -import java.nio.file.FileVisitResult -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.SimpleFileVisitor -import java.nio.file.attribute.BasicFileAttributes -import java.text.DecimalFormat -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger -import java.util.zip.ZipInputStream -import scala.collection.parallel.mutable.ParArray -import scala.meta.parsers.ParseException -import com.thoughtworks.qdox.parser.{ParseException => QParseException} -import scala.reflect.ClassTag -import scala.util.Sorting -import scala.util.control.NonFatal -import org.langmeta.inputs.Input -import org.langmeta.internal.io.FileIO -import org.langmeta.internal.io.PathIO -import org.langmeta.io.AbsolutePath -import org.langmeta.io.Fragment -import org.langmeta.io.RelativePath -import org.langmeta.internal.semanticdb.schema.Database -import org.langmeta.internal.semanticdb.schema.Document - -/** - * Syntactically build a semanticdb index containing only global symbol definition. - * - * The purpose of this module is to provide "Go to definition" from - * project sources to dependency sources without indexing classfiles or - * requiring dependencies to publish semanticdbs alongside their artifacts. - * - * One other use-case for this module is to implement "Workspace symbol provider" - * without any build-tool or compiler integration. "Mtags" name comes from - * mixing "meta" and "ctags". - */ -object Mtags { - - /** - * Build an index from a classpath of -sources.jar - * - * @param shouldIndex An optional filter to skip particular relative filenames. - * @param callback A callback that is called as soon as a document has been - * indexed. - */ - def index( - classpath: List[AbsolutePath], - shouldIndex: RelativePath => Boolean = _ => true - )(callback: Document => Unit): Unit = { - val fragments = allClasspathFragments(classpath, shouldIndex) - val totalIndexedFiles = new AtomicInteger() - val totalIndexedLines = new AtomicInteger() - val start = System.nanoTime() - def elapsed: Long = { - val result = TimeUnit.SECONDS.convert( - System.nanoTime() - start, - TimeUnit.NANOSECONDS - ) - if (result == 0) 1 // prevent divide by zero - else result - } - val decimal = new DecimalFormat("###,###") - val N = fragments.length - def updateTotalLines(doc: Document): Unit = { - // NOTE(olafur) it would be interesting to have separate statistics for - // Java/Scala lines/s but that would require a more sophisticated setup. - totalIndexedLines.addAndGet(countLines(doc.contents)) - } - def reportProgress(indexedFiles: Int): Unit = { - if (indexedFiles < 100) return - val percentage = ((indexedFiles / N.toDouble) * 100).toInt - val loc = decimal.format(totalIndexedLines.get() / elapsed) - scribe.info( - s"Progress $percentage%, ${decimal.format(indexedFiles)} files indexed " + - s"out of total ${decimal.format(fragments.length)} ($loc loc/s)" - ) - } - scribe.info(s"Indexing $N source files") - fragments.foreach { fragment => - try { - val indexedFiles = totalIndexedFiles.incrementAndGet() - if (indexedFiles % 200 == 0) { - reportProgress(indexedFiles) - } - if (shouldIndex(fragment.name)) { - val doc = index(fragment) - updateTotalLines(doc) - callback(doc) - } - } catch { - case _: ParseException | _: QParseException => // nothing - case NonFatal(e) => - scribe.error(s"Error indexing ${fragment.name}", e) - } - } - reportProgress(totalIndexedFiles.get) - scribe.info( - s"Completed indexing ${decimal.format(totalIndexedFiles.get)} files with " + - s"total ${decimal.format(totalIndexedLines.get())} lines of code" - ) - } - - /** Index all documents into a single scala.meta.Database. */ - def indexDatabase( - classpath: List[AbsolutePath], - shouldIndex: RelativePath => Boolean = _ => true - ): Database = { - val buffer = List.newBuilder[Document] - index(classpath, shouldIndex) { doc => - buffer += doc - } - Database(buffer.result()) - } - - /** Index single Scala or Java source file from memory */ - def index(filename: String, contents: String): Document = - index(Input.VirtualFile(filename, contents)) - - /** Index single Scala or Java from disk or zip file. */ - def index(fragment: Fragment): Document = { - val uri = { - val base = fragment.base.toNIO.getFileName.toString - // Need special handling because https://github.com/scalameta/scalameta/issues/1163 - if (isZip(base) || isJar(base)) - new URI(s"jar:${fragment.base.toURI - .normalize()}!/${fragment.name.toString().replace('\\', '/')}") - else fragment.uri - } - val contents = new String(FileIO.readAllBytes(uri), StandardCharsets.UTF_8) - index(Input.VirtualFile(uri.toString, contents)) - } - - /** Index single Scala or Java source file from memory */ - def index(input: Input.VirtualFile): Document = { - scribe.trace(s"Indexing ${input.path} with length ${input.value.length}") - val indexer: MtagsIndexer = - if (isScala(input.path)) ScalaMtags.index(input) - else if (isJava(input.path)) JavaMtags.index(input) - else { - throw new IllegalArgumentException( - s"Unknown file extension ${input.path}" - ) - } - val (names, symbols) = indexer.index() - Document( - filename = input.path, - contents = input.value, - language = indexer.language, - names, - Nil, - symbols, - Nil - ) - } - - private def canIndex(path: String): Boolean = - isScala(path) || isJava(path) - private def canUnzip(path: String): Boolean = - isJar(path) || isZip(path) - private def isJar(path: String): Boolean = path.endsWith(".jar") - private def isZip(path: String): Boolean = path.endsWith(".zip") - private def isJava(path: String): Boolean = path.endsWith(".java") - private def isScala(path: String): Boolean = path.endsWith(".scala") - private def isScala(path: Path): Boolean = PathIO.extension(path) == "scala" - - /** Returns all *.scala fragments to index from this classpath - * - * This implementation is copy-pasted from scala.meta.Classpath.deep with - * the following differences: - * - * - We build a parallel array - * - We log errors instead of silently ignoring them - * - We filter out non-scala sources - */ - private def allClasspathFragments( - classpath: List[AbsolutePath], - shouldIndex: RelativePath => Boolean - ): ParArray[Fragment] = { - val buf = ParArray.newBuilder[Fragment] - def add(fragment: Fragment): Unit = { - if (shouldIndex(fragment.name)) buf += fragment - else () - } - classpath.foreach[Any] { base => - def exploreJar(base: AbsolutePath): Unit = { - val stream = Files.newInputStream(base.toNIO) - try { - val zip = new ZipInputStream(stream) - try { - var entry = zip.getNextEntry - while (entry != null) { - if (!entry.getName.endsWith("/") && - canIndex(entry.getName)) { - add( - Fragment(base, RelativePath(entry.getName.stripPrefix("/"))) - ) - } - entry = zip.getNextEntry - } - } finally zip.close() - } catch { - case ex: IOException => - scribe.error(ex.getMessage, ex) - } finally { - stream.close() - } - } - if (base.isDirectory) { - Files.walkFileTree( - base.toNIO, - new SimpleFileVisitor[Path] { - override def visitFile( - file: Path, - attrs: BasicFileAttributes - ): FileVisitResult = { - if (isScala(file)) { - add(Fragment(base, RelativePath(base.toNIO.relativize(file)))) - } - FileVisitResult.CONTINUE - } - } - ) - } else if (base.isFile) { - if (canUnzip(base.toString())) { - exploreJar(base) - } else { - sys.error( - s"Obtained non-jar file $base. Expected directory or *.jar file." - ) - } - } else { - scribe.info(s"Skipping $base") - // Skip - } - } - val result = buf.result() - Sorting.stableSort(result.arrayseq)( - implicitly[ClassTag[Fragment]], - Ordering.by { fragment => - PathIO.extension(fragment.name.toNIO) match { - case "scala" => 1 - case "java" => 2 - case _ => 3 - } - } - ) - result - } - - private def countLines(string: String): Int = { - var i = 0 - var lines = 0 - while (i < string.length) { - if (string.charAt(i) == '\n') lines += 1 - i += 1 - } - lines - } -} diff --git a/metals/src/main/scala/scala/meta/metals/mtags/MtagsIndexer.scala b/metals/src/main/scala/scala/meta/metals/mtags/MtagsIndexer.scala deleted file mode 100644 index 0fd6a36740c..00000000000 --- a/metals/src/main/scala/scala/meta/metals/mtags/MtagsIndexer.scala +++ /dev/null @@ -1,73 +0,0 @@ -package scala.meta.metals.mtags - -import scala.meta.Name -import scala.meta.Term -import scala.meta.PACKAGE -import scala.meta.metals.ScalametaEnrichments._ -import org.langmeta.internal.semanticdb.schema.Denotation -import org.langmeta.internal.semanticdb.schema.ResolvedName -import org.langmeta.internal.semanticdb.schema.Position -import org.langmeta.internal.semanticdb.schema.ResolvedSymbol -import org.{langmeta => m} -import org.langmeta.semanticdb.Signature -import org.langmeta.semanticdb.Symbol - -trait MtagsIndexer { - def language: String - def indexRoot(): Unit - def index(): (List[ResolvedName], List[ResolvedSymbol]) = { - indexRoot() - names.result() -> symbols.result() - } - private val root: Symbol.Global = - Symbol.Global(Symbol.None, Signature.Term("_root_")) - var currentOwner: Symbol.Global = root - def owner(isStatic: Boolean): Symbol.Global = - if (isStatic) currentOwner.toTerm - else currentOwner - def withOwner[A](owner: Symbol.Global = currentOwner)(thunk: => A): A = { - val old = currentOwner - currentOwner = owner - val result = thunk - currentOwner = old - result - } - def term(name: String, pos: m.Position, flags: Long): Unit = - addSignature(Signature.Term(name), pos, flags) - def term(name: Term.Name, flags: Long): Unit = - addSignature(Signature.Term(name.value), name.pos, flags) - def param(name: Name, flags: Long): Unit = - addSignature(Signature.TermParameter(name.value), name.pos, flags) - def tpe(name: String, pos: m.Position, flags: Long): Unit = - addSignature(Signature.Type(name), pos, flags) - def tpe(name: Name, flags: Long): Unit = - addSignature(Signature.Type(name.value), name.pos, flags) - def pkg(ref: Term): Unit = ref match { - case Name(name) => - currentOwner = symbol(Signature.Term(name)) - case Term.Select(qual, Name(name)) => - pkg(qual) - currentOwner = symbol(Signature.Term(name)) - } - private val names = List.newBuilder[ResolvedName] - private val symbols = List.newBuilder[ResolvedSymbol] - private def addSignature( - signature: Signature, - definition: m.Position, - flags: Long - ): Unit = { - currentOwner = symbol(signature) - val syntax = currentOwner.syntax - names += ResolvedName( - Some(Position(definition.start, definition.end)), - syntax, - isDefinition = (flags & PACKAGE) == 0 - ) - symbols += ResolvedSymbol( - syntax, - Some(Denotation(flags, signature.name, "", Nil)) - ) - } - private def symbol(signature: Signature): Symbol.Global = - Symbol.Global(currentOwner, signature) -} diff --git a/metals/src/main/scala/scala/meta/metals/mtags/ScalaMtags.scala b/metals/src/main/scala/scala/meta/metals/mtags/ScalaMtags.scala deleted file mode 100644 index c3fa837c7c7..00000000000 --- a/metals/src/main/scala/scala/meta/metals/mtags/ScalaMtags.scala +++ /dev/null @@ -1,56 +0,0 @@ -package scala.meta.metals.mtags - -import scala.meta._ -import org.langmeta.inputs.Input - -object ScalaMtags { - def index(input: Input.VirtualFile): MtagsIndexer = { - val root: Source = input.parse[Source].get - new Traverser with MtagsIndexer { - override def language: String = - "Scala212" // TODO(olafur) more accurate dialect - override def indexRoot(): Unit = apply(root) - override def apply(tree: Tree): Unit = withOwner() { - def continue(): Unit = super.apply(tree) - def stop(): Unit = () - def pats(ps: List[Pat], flag: Long): Unit = { - ps.foreach { - case Pat.Var(name) => withOwner() { term(name, flag) } - case _ => - } - } - tree match { - case t: Source => continue() - case t: Template => continue() - case t: Pkg => pkg(t.ref); continue() - case t: Pkg.Object => - term(t.name, PACKAGEOBJECT); - term("package", t.name.pos, OBJECT); - continue() - case t: Defn.Class => - tpe(t.name, CLASS) - for { - params <- t.ctor.paramss - param <- params - } withOwner() { - // TODO(olafur) More precise flags, we add VAL here blindly even if - // it's not a val, it might even be a var! - super.param(param.name, VAL | PARAM) - } - continue() - case t: Defn.Trait => tpe(t.name, TRAIT); continue() - case t: Defn.Object => term(t.name, OBJECT); continue() - case t: Defn.Type => tpe(t.name, TYPE); stop() - case t: Decl.Type => tpe(t.name, TYPE); stop() - case t: Defn.Def => term(t.name, DEF); stop() - case t: Decl.Def => term(t.name, DEF); stop() - case t: Defn.Val => pats(t.pats, VAL); stop() - case t: Decl.Val => pats(t.pats, VAL); stop() - case t: Defn.Var => pats(t.pats, VAR); stop() - case t: Decl.Var => pats(t.pats, VAR); stop() - case _ => stop() - } - } - } - } -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/DefinitionProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/DefinitionProvider.scala deleted file mode 100644 index 95a7ee1cc3d..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/DefinitionProvider.scala +++ /dev/null @@ -1,26 +0,0 @@ -package scala.meta.metals.providers - -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.Uri -import scala.meta.lsp.Location -import scala.meta.lsp.Position -import scala.meta.metals.search.SymbolIndex -import org.langmeta.io.AbsolutePath - -object DefinitionProvider { - - def definition( - symbolIndex: SymbolIndex, - uri: Uri, - position: Position, - tempSourcesDir: AbsolutePath - ): List[Location] = { - val locations = for { - data <- symbolIndex.findDefinition(uri, position.line, position.character) - pos <- data.definition - _ = scribe.info(s"Found definition ${pos.pretty} ${data.symbol}") - } yield pos.toLocation.toNonJar(tempSourcesDir) - locations.toList - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/DocumentFormattingProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/DocumentFormattingProvider.scala deleted file mode 100644 index a69b5d44c6f..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/DocumentFormattingProvider.scala +++ /dev/null @@ -1,95 +0,0 @@ -package scala.meta.metals.providers - -import java.nio.file.Files -import scala.meta.metals.Configuration -import scala.meta.metals.Configuration.Scalafmt -import scala.meta.metals.Formatter -import scala.meta.jsonrpc.MonixEnrichments._ -import scala.meta.lsp.Position -import scala.meta.jsonrpc.JsonRpcClient -import scala.meta.jsonrpc.Response -import scala.meta.lsp.Window.showMessage -import scala.meta.RelativePath -import scala.util.control.NonFatal -import cats.syntax.bifunctor._ -import cats.instances.either._ -import scala.meta.lsp.Range -import scala.meta.lsp.TextEdit -import monix.eval.Task -import monix.execution.Scheduler -import monix.reactive.Observable -import org.langmeta.inputs.Input -import org.langmeta.io.AbsolutePath - -class DocumentFormattingProvider( - configuration: Observable[Configuration], - cwd: AbsolutePath -)(implicit client: JsonRpcClient, s: Scheduler) { - - private val formatter: () => Either[String, Formatter] = - configuration - .focus(_.scalafmt.version) - .map[Either[String, Formatter]] { version => - try { - // TODO(olafur) convert Jars.fetch to use monix.Task to avoid blocking - Right(Formatter.classloadScalafmt(version)) - } catch { - case NonFatal(e) => - val msg = - s"Unable to install scalafmt version $version, cause: ${e.getMessage}" - Left(msg) - } - } - .toFunction0() - - private val config: () => Either[String, Option[AbsolutePath]] = - configuration - .focus(_.scalafmt.confPath) - .map[Either[String, Option[AbsolutePath]]] { - case None => - val default = cwd.resolve(RelativePath(Scalafmt.defaultConfPath)) - if (Files.isRegularFile(default.toNIO)) Right(Some(default)) - else Right(None) - case Some(relpath) => - val custom = cwd.resolve(RelativePath(relpath)) - if (Files.isRegularFile(custom.toNIO)) - Right(Some(custom)) - else if (relpath == Configuration.Scalafmt.defaultConfPath) - Right(None) - else - Left(s"metals.scalafmt.confPath=$relpath is not a file") - } - .toFunction0() - - def format( - input: Input.VirtualFile - ): Task[Either[Response.Error, List[TextEdit]]] = Task { - val pos = scala.meta.Position.Range(input, 0, input.chars.length) - val fullDocumentRange = Range( - Position(pos.startLine, pos.startColumn), - Position(pos.endLine, pos.endColumn) - ) - val formatResult = for { - scalafmt <- formatter() - scalafmtConf <- config() - } yield { - scalafmtConf match { - case None => // default config - scalafmt.format(input.value, input.path) - case Some(path) => - scalafmt.format(input.value, input.path, path) - } - } - formatResult.bimap( - message => { - // We show a message here to be sure the message is - // reported in the UI. invalidParams responses don't - // get reported in vscode at least. - showMessage.error(message) - Response.invalidParams(message) - }, - formatted => List(TextEdit(fullDocumentRange, formatted)) - ) - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/DocumentHighlightProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/DocumentHighlightProvider.scala deleted file mode 100644 index 22ca36a195c..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/DocumentHighlightProvider.scala +++ /dev/null @@ -1,28 +0,0 @@ -package scala.meta.metals.providers - -import scala.meta.metals.Uri -import scala.meta.metals.search.SymbolIndex -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.lsp.DocumentHighlight -import scala.meta.lsp.Position - -object DocumentHighlightProvider { - def empty: List[DocumentHighlight] = Nil - - def highlight( - symbolIndex: SymbolIndex, - uri: Uri, - position: Position - ): List[DocumentHighlight] = { - scribe.info(s"Document highlight in $uri") - for { - data <- symbolIndex.findReferences(uri, position.line, position.character) - _ = scribe.info(s"Highlighting symbol `${data.name}: ${data.signature}`") - pos <- data.referencePositions(withDefinition = true) - if pos.uri == uri.value - _ = scribe.debug(s"Found highlight at [${pos.range.get.pretty}]") - // TODO(alexey) add DocumentHighlightKind: Text (default), Read, Write - } yield DocumentHighlight(pos.range.get.toRange) - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/DocumentSymbolProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/DocumentSymbolProvider.scala deleted file mode 100644 index 45a1f2ed4db..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/DocumentSymbolProvider.scala +++ /dev/null @@ -1,64 +0,0 @@ -package scala.meta.metals.providers - -import scala.meta._ -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.Uri -import scala.meta.lsp -import scala.meta.lsp.Location -import scala.meta.lsp.SymbolInformation - -object DocumentSymbolProvider { - - private class SymbolTraverser(uri: Uri) { - private val builder = List.newBuilder[SymbolInformation] - - val traverser = new Traverser { - private var currentRoot: Option[Tree] = None - override def apply(currentNode: Tree): Unit = { - def continue(withNewRoot: Boolean = false): Unit = { - val oldRoot = currentRoot - if (withNewRoot) currentRoot = Some(currentNode) - super.apply(currentNode) - currentRoot = oldRoot - } - - def addName(name: String): Unit = { - builder += lsp.SymbolInformation( - name = name, - kind = currentNode.symbolKind, - location = Location(uri.value, currentNode.pos.toRange), - containerName = currentRoot.flatMap(_.qualifiedName) - ) - } - - def addNode(): Unit = currentNode.names.foreach(addName) - - currentNode match { - // we need to go deeper - case _: Source | _: Template => continue() - // add package, but don't set it as a new root - case _: Pkg => addNode(); continue() - // terminal nodes: add them, but don't go inside - case _: Defn.Def | _: Defn.Val | _: Defn.Var => addNode() - case _: Decl.Def | _: Decl.Val | _: Decl.Var => addNode() - // all other (named) types and terms can contain more nodes - case t if t.is[Member.Type] || t.is[Member.Term] => - addNode(); continue(withNewRoot = true) - case _ => () - } - } - } - - def apply(tree: Tree): List[SymbolInformation] = { - traverser.apply(tree) - builder.result() - } - } - - def empty: List[SymbolInformation] = Nil - def documentSymbols( - uri: Uri, - source: Source - ): List[SymbolInformation] = - new SymbolTraverser(uri).apply(source) -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/ReferencesProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/ReferencesProvider.scala deleted file mode 100644 index 72922dc05eb..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/ReferencesProvider.scala +++ /dev/null @@ -1,25 +0,0 @@ -package scala.meta.metals.providers - -import scala.meta.metals.search.SymbolIndex -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.Uri -import scala.meta.lsp.Location -import scala.meta.lsp.Position -import scala.meta.lsp.ReferenceContext - -object ReferencesProvider { - - def references( - symbolIndex: SymbolIndex, - uri: Uri, - position: Position, - context: ReferenceContext - ): List[Location] = { - for { - data <- symbolIndex.findReferences(uri, position.line, position.character) - pos <- data.referencePositions(context.includeDeclaration) - _ = scribe.info(s"Found reference ${pos.pretty} ${data.symbol}") - } yield pos.toLocation - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/providers/RenameProvider.scala b/metals/src/main/scala/scala/meta/metals/providers/RenameProvider.scala deleted file mode 100644 index 01486a9bc75..00000000000 --- a/metals/src/main/scala/scala/meta/metals/providers/RenameProvider.scala +++ /dev/null @@ -1,56 +0,0 @@ -package scala.meta.metals.providers - -import scala.meta._ -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.Uri -import scala.meta.lsp.RenameParams -import scala.meta.lsp.TextEdit -import scala.meta.lsp.Window.showMessage -import scala.meta.lsp.WorkspaceEdit -import scala.meta.jsonrpc.JsonRpcClient -import scala.meta.metals.refactoring.Backtick -import scala.meta.metals.search.SymbolIndex -import scala.meta.lsp.WorkspaceEdit - -object RenameProvider { - def rename(params: RenameParams, symbolIndex: SymbolIndex)( - implicit client: JsonRpcClient - ): WorkspaceEdit = Backtick.backtickWrap(params.newName) match { - case Left(err) => - // LSP specifies that a ResponseError should be returned in this case - // but it seems when we do that at least vscode doesn't display the error - // message, only "No result" is displayed to the user, which is not helpful. - // I prefer to use showMessage to explain what went wrong and perform - // no text edit. - showMessage.warn(err) - WorkspaceEdit(Map.empty) - case Right(newName) => - val uri = Uri(params.textDocument.uri) - val edits = for { - reference <- symbolIndex.findReferences( - uri, - params.position.line, - params.position.character - ) - symbol = Symbol(reference.symbol) - if { - symbol match { - case _: Symbol.Local => true - case _ => - showMessage.warn( - s"Rename for global symbol $symbol is not supported yet. " + - s"Only local symbols can be renamed." - ) - false - } - } - position <- reference.referencePositions(true) - } yield { - TextEdit(position.toLocation.range, newName) - } - // NOTE(olafur) uri.value is hardcoded here because local symbols - // cannot be references across multiple files. When we add support for - // renaming global symbols then this needs to change. - WorkspaceEdit(Map(uri.value -> edits)) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/refactoring/Backtick.scala b/metals/src/main/scala/scala/meta/metals/refactoring/Backtick.scala deleted file mode 100644 index 1e8d0b8cc4e..00000000000 --- a/metals/src/main/scala/scala/meta/metals/refactoring/Backtick.scala +++ /dev/null @@ -1,72 +0,0 @@ -// Sources copy-pasted from Ammonite: -// https://github.com/lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala#L71-L121 -// Original licence: MIT -// Original author: Li Haoyi -package scala.meta.metals.refactoring - -object Backtick { - private val alphaKeywords = Set( - "abstract", "case", "catch", "class", "def", "do", "else", "extends", - "false", "finally", "final", "finally", "forSome", "for", "if", "implicit", - "import", "lazy", "match", "new", "null", "object", "override", "package", - "private", "protected", "return", "sealed", "super", "this", "throw", - "trait", "try", "true", "type", "val", "var", "while", "with", "yield", "_", - "macro" - ) - private val symbolKeywords = Set( - ":", ";", "=>", "=", "<-", "<:", "<%", ">:", "#", "@", "\u21d2", "\u2190" - ) - // scalafmt: { style = default } - private val blockCommentStart = "/*" - private val lineCommentStart = "//" - - def needsBacktick(s: String): Boolean = { - val chunks = s.split("_", -1) - def validOperator(c: Char) = { - c.getType == Character.MATH_SYMBOL || - c.getType == Character.OTHER_SYMBOL || - "!#%&*+-/:<=>?@\\^|~".contains(c) - } - val validChunks = chunks.zipWithIndex.forall { - case (chunk, index) => - chunk.forall(c => c.isLetter || c.isDigit || c == '$') || - (chunk.forall(validOperator) && - // operators can only come last - index == chunks.length - 1 && - // but cannot be preceded by only a _ - !(chunks.lift(index - 1).contains("") && index - 1 == 0)) - } - - val firstLetterValid = s(0).isLetter || s(0) == '_' || s(0) == '$' || validOperator( - s(0) - ) - - val valid = - validChunks && - firstLetterValid && - !alphaKeywords.contains(s) && - !symbolKeywords.contains(s) && - !s.contains(blockCommentStart) && - !s.contains(lineCommentStart) - - !valid - } - - def backtickWrap(s: String): Either[String, String] = { - val ident = - if (s.isEmpty) "``" - else if (s(0) == '`' && s.last == '`') s - else if (s.contains('`')) s - else if (needsBacktick(s)) '`' + s + '`' - else s - import scala.meta._ - ident.tokenize match { - case Tokenized.Success( - Tokens(_: Token.BOF, _: Token.Ident, _: Token.EOF)) => - Right(ident) - case _ => - Left(s"$s is not a valid identifier") - } - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/refactoring/TextEdits.scala b/metals/src/main/scala/scala/meta/metals/refactoring/TextEdits.scala deleted file mode 100644 index 4d1d1017816..00000000000 --- a/metals/src/main/scala/scala/meta/metals/refactoring/TextEdits.scala +++ /dev/null @@ -1,41 +0,0 @@ -package scala.meta.metals.refactoring - -import scala.meta.Input -import org.langmeta.languageserver.InputEnrichments._ -import scala.annotation.tailrec -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.lsp.TextEdit - -/** Re-implementation of how TextEdits should be handled by the editor client - * - * Useful for testing/logging purposes. Instead of reading massive JSON blobs like - * [{"range":{"start":{"line":1,"character":0},"end":{"line":2,"character":0}},"newText":""}, - * ...] - * you can look at how the code looks like applied instead. - */ -object TextEdits { - def applyToInput( - input: Input, - edits: List[TextEdit] - ): String = { - val original = input.contents - val sb = new java.lang.StringBuilder - @tailrec def loop(i: Int, es: List[TextEdit]): Unit = { - val isDone = i >= original.length - if (isDone) () - else { - es match { - case Nil => - sb.append(original.substring(i)) - case edit :: tail => - val pos = input.toPosition(edit.range) - sb.append(original.substring(i, pos.start)) - .append(edit.newText) - loop(pos.end, tail) - } - } - } - loop(0, edits) - sb.toString - } -} diff --git a/metals/src/main/scala/scala/meta/metals/sbtserver/Sbt.scala b/metals/src/main/scala/scala/meta/metals/sbtserver/Sbt.scala deleted file mode 100644 index 1265711d672..00000000000 --- a/metals/src/main/scala/scala/meta/metals/sbtserver/Sbt.scala +++ /dev/null @@ -1,34 +0,0 @@ -package scala.meta.metals.sbtserver - -import scala.meta.metals.SbtExecParams -import scala.meta.metals.SbtInitializeParams -import scala.meta.metals.SbtInitializeResult -import scala.meta.metals.SettingParams -import scala.meta.metals.SettingResult -import monix.eval.Task -import scala.meta.jsonrpc.Endpoint -import scala.meta.jsonrpc.JsonRpcClient -import scala.meta.jsonrpc.Response - -trait Sbt { - object initialize - extends Endpoint[SbtInitializeParams, SbtInitializeResult]("initialize") - object setting extends Endpoint[SettingParams, SettingResult]("sbt/setting") { - def query(setting: String)( - implicit client: JsonRpcClient - ): Task[Either[Response.Error, SettingResult]] = - super.request(SettingParams(setting)) - - } - object exec extends Endpoint[SbtExecParams, Unit]("sbt/exec") { - def apply( - commandLine: String - )(implicit client: JsonRpcClient): Task[Unit] = { - if (commandLine.trim.isEmpty) Task.now(()) - // NOTE(olafur) sbt/exec is a request that never responds - else super.request(SbtExecParams(commandLine)).map(_ => Unit) - } - } -} - -object Sbt extends Sbt diff --git a/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala b/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala deleted file mode 100644 index 06e50d02d4b..00000000000 --- a/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala +++ /dev/null @@ -1,186 +0,0 @@ -package scala.meta.metals.sbtserver - -import java.io.IOException -import java.net.URI -import java.nio.ByteBuffer -import java.nio.file.Files -import java.util.Properties -import scala.meta.metals.ActiveJson -import scala.meta.metals.MissingActiveJson -import scala.meta.metals.SbtInitializeParams -import scala.meta.metals.Configuration -import scala.util.Try -import io.circe.jawn.parseByteBuffer -import monix.eval.Task -import monix.execution.CancelableFuture -import monix.execution.Scheduler -import org.langmeta.io.AbsolutePath -import org.langmeta.io.RelativePath -import scala.meta.jsonrpc.BaseProtocolMessage -import scala.meta.jsonrpc.JsonRpcClient -import scala.meta.jsonrpc.Services -import scala.meta.jsonrpc.LanguageClient -import scala.meta.jsonrpc.LanguageServer -import scala.meta.lsp.TextDocument -import scala.meta.lsp.Window -import org.scalasbt.ipcsocket.UnixDomainSocket - -/** - * A wrapper around a connection to an sbt server. - * - * @param client client that can send requests and notifications - * to the sbt server. - * @param runningServer The running client listening for requests from the server. - * Use runningServer.onComplete to attach callbacks on - * disconnect. - * - */ -case class SbtServer( - client: JsonRpcClient, - runningServer: CancelableFuture[Unit] -) { - def disconnect(): Unit = runningServer.cancel() -} - -object SbtServer { - private def fail(message: String) = Task.now(Left(message)) - - /** - * Tries to read sbt version from the `project/build.properties` file. - * - * @param cwd sbt project root directory. - * @return version string value or `None` if anything goes wrong. - */ - def readVersion(cwd: AbsolutePath): Option[String] = { - val props = new Properties() - val path = cwd.resolve("project").resolve("build.properties") - if (path.isFile) { - val input = Files.newInputStream(path.toNIO) - try { - props.load(input) - } finally { - input.close() - } - } - Option(props.getProperty("sbt.version")) - } - - /** - * Establish connection with sbt server. - * - * Requires sbt 1.1.0 and above. - * - * @see http://www.scala-sbt.org/1.x-beta/docs/sbt-server.html - * - * @param cwd The workspace directory, baseDirectory.in(ThisBuild). - * @param services the handler for requests/notifications/responses from - * the sbt server. - * @param scheduler the scheduler on which to run the services handling - * sbt responses and notifications. - * @return A client to communicate with sbt server in case of success or a - * user-friendly error message if something went wrong in case of - * failure. - */ - def connect( - cwd: AbsolutePath, - services: Services - )( - implicit scheduler: Scheduler - ): Task[Either[String, SbtServer]] = { - Task(SbtServer.openSocketConnection(cwd)).flatMap { - case Left(err: MissingActiveJson) => - fail(err.getMessage) - case Left(_: IOException) => - fail( - s"Unable to establish connection with sbt server. " + - s"Do you have an active sbt 1.1+ session?" - ) - case Left(err) => - val msg = s"Unexpected error opening connection to sbt server" - scribe.error(msg, err) - fail(msg + ". Check .metals/metals.log") - case Right(socket) => - val client: LanguageClient = - new LanguageClient(socket.getOutputStream, scribe.Logger.root) - val messages = - BaseProtocolMessage.fromInputStream( - socket.getInputStream, - scribe.Logger.root - ) - val server = - new LanguageServer( - messages, - client, - services, - scheduler, - scribe.Logger.root - ) - val runningServer = - server.startTask.doOnCancel(Task.eval(socket.close())).runAsync - val initialize = client.request(Sbt.initialize, SbtInitializeParams()) - initialize.map { _ => - Right(SbtServer(client, runningServer)) - } - } - } - - /** - * Handler that forwards logMessage and publishNotifications to the sbt server. - * - * @param editorClient the LSP editor client to forward the notifications - * from the sbt server. - */ - def forwardingServices( - editorClient: JsonRpcClient, - config: () => Configuration - ): Services = - Services - .empty(scribe.Logger.root) - .notification(Window.logMessage) { msg => - editorClient.notify(Window.logMessage, msg) - } - .notification(TextDocument.publishDiagnostics) { msg => - if (config().sbt.diagnostics.enabled) { - editorClient.notify(TextDocument.publishDiagnostics, msg) - } - } - - /** - * Returns path to project/target/active.json from the base directory of an sbt build. - */ - object ActiveJson { - private val relativePath: RelativePath = - RelativePath("project").resolve("target").resolve("active.json") - - def apply(cwd: AbsolutePath): AbsolutePath = - cwd.resolve(relativePath) - - def unapply(path: RelativePath): Boolean = - path == relativePath - } - - /** - * Establishes a unix domain socket connection with sbt server. - */ - def openSocketConnection( - cwd: AbsolutePath - ): Either[Throwable, UnixDomainSocket] = { - val path = ActiveJson(cwd) - for { - bytes <- { - if (path.isFile) Right(Files.readAllBytes(path.toNIO)) - else Left(MissingActiveJson(path)) - } - parsed <- parseByteBuffer(ByteBuffer.wrap(bytes)) - activeJson <- parsed.as[ActiveJson] - uri <- Try(URI.create(activeJson.uri)).toEither - socket <- uri.getScheme match { - case "local" => - scribe.info(s"Connecting to sbt server socket ${uri.getPath}") - Try(new UnixDomainSocket(uri.getPath)).toEither - case invalid => - Left(new IllegalArgumentException(s"Unsupported scheme $invalid")) - } - } yield socket - } -} diff --git a/metals/src/main/scala/scala/meta/metals/search/BinarySearch.scala b/metals/src/main/scala/scala/meta/metals/search/BinarySearch.scala deleted file mode 100644 index 0dddb3de500..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/BinarySearch.scala +++ /dev/null @@ -1,43 +0,0 @@ -package scala.meta.metals.search - -import scala.annotation.tailrec - -object BinarySearch { - sealed trait ComparisonResult - case object Greater extends ComparisonResult - case object Equal extends ComparisonResult - case object Smaller extends ComparisonResult - - /** - * Binary search using a custom compare function. - * - * scala.util.Searching does not support the ability to search an IndexedSeq - * by a custom mapping function, you must search by an element of the same - * type as the elements of the Seq. - * - * @param array Must be sorted according to compare function so that for all - * i > j, compare(array(i), array(i)) == Greater. - * @param compare Callback used at every guess index to determine whether - * the search element has been found, or whether to search - * above or below the guess index. - * @return The first element where compare(element) == Equal. There is no guarantee - * which element is chosen if many elements return Equal. - */ - def array[T]( - array: Array[T], - compare: T => ComparisonResult - ): Option[T] = { - @tailrec def loop(lo: Int, hi: Int): Option[T] = - if (lo > hi) None - else { - val mid = lo + (hi - lo) / 2 - val curr = array(mid) - compare(curr) match { - case Greater => loop(lo, mid - 1) - case Equal => Some(curr) - case Smaller => loop(mid + 1, hi) - } - } - loop(0, array.length - 1) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/search/DocumentIndex.scala b/metals/src/main/scala/scala/meta/metals/search/DocumentIndex.scala deleted file mode 100644 index 783d58130e8..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/DocumentIndex.scala +++ /dev/null @@ -1,9 +0,0 @@ -package scala.meta.metals.search - -import scala.meta.metals.Uri -import org.langmeta.internal.semanticdb.schema.Document - -trait DocumentIndex { - def getDocument(uri: Uri): Option[Document] // should this be future? - def putDocument(uri: Uri, document: Document): Unit -} diff --git a/metals/src/main/scala/scala/meta/metals/search/InMemoryDocumentIndex.scala b/metals/src/main/scala/scala/meta/metals/search/InMemoryDocumentIndex.scala deleted file mode 100644 index 64605f91d46..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/InMemoryDocumentIndex.scala +++ /dev/null @@ -1,19 +0,0 @@ -package scala.meta.metals.search - -import java.util -import java.util.concurrent.ConcurrentHashMap -import scala.meta.metals.Uri -import org.langmeta.internal.semanticdb.schema.Document - -class InMemoryDocumentIndex( - documents: util.Map[Uri, Document] = new ConcurrentHashMap() -) extends DocumentIndex { - override def getDocument(uri: Uri): Option[Document] = - Option(documents.get(uri)) - override def putDocument(uri: Uri, document: Document): Unit = { - if (!uri.isJar) { - scribe.info(s"Storing in-memory document for uri $uri") - } - documents.put(uri, document) - } -} diff --git a/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndex.scala b/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndex.scala deleted file mode 100644 index f81fe8a4bee..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndex.scala +++ /dev/null @@ -1,267 +0,0 @@ -package scala.meta.metals.search - -import java.util.concurrent.ConcurrentHashMap -import scala.meta.metals.Buffers -import scala.meta.metals.Effects -import scala.meta.metals.Configuration -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.MetalsServices.cacheDirectory -import scala.meta.metals.Uri -import scala.meta.metals.compiler.CompilerConfig -import scala.meta.metals.index.SymbolData -import scala.meta.metals.mtags.Mtags -import scala.meta.metals.storage.LevelDBMap -import scala.meta.jsonrpc.MonixEnrichments._ -import scala.meta.lsp.SymbolInformation -import scala.meta.jsonrpc.JsonRpcClient -import scala.meta.metals.{index => i} -import me.xdrop.fuzzywuzzy.FuzzySearch -import org.langmeta.inputs.Input -import org.langmeta.inputs.Position -import org.langmeta.internal.semanticdb.schema.Database -import org.langmeta.internal.semanticdb.schema.ResolvedName -import org.langmeta.internal.semanticdb.{schema => s} -import org.langmeta.io.AbsolutePath -import org.langmeta.languageserver.InputEnrichments._ -import org.langmeta.semanticdb.SemanticdbEnrichments._ -import org.langmeta.semanticdb.Symbol -import monix.eval.Task -import monix.execution.Scheduler -import monix.reactive.Observable -import scala.util.control.NonFatal - -class InMemorySymbolIndex( - val symbolIndexer: SymbolIndexer, - val documentIndex: DocumentIndex, - cwd: AbsolutePath, - buffers: Buffers, - configuration: Observable[Configuration], -)(implicit scheduler: Scheduler, client: JsonRpcClient) - extends SymbolIndex { - private val config = configuration.map(_.search).toFunction0() - private val indexedJars: ConcurrentHashMap[AbsolutePath, Unit] = - new ConcurrentHashMap[AbsolutePath, Unit]() - - /** Returns a ResolvedName at the given location */ - def resolveName( - uri: Uri, - line: Int, - column: Int - ): Option[(ResolvedName, TokenEditDistance)] = { - scribe.info(s"resolveName at $uri:$line:$column") - for { - document <- documentIndex.getDocument(uri) - _ = scribe.info(s"Found document for $uri") - original = Input.VirtualFile(document.filename, document.contents) - revised = uri.toInput(buffers) - (originalPosition, edit) <- { - findOriginalPosition(original, revised, line, column) - } - name <- document.names.collectFirst { - case name @ ResolvedName(Some(position), symbol, _) if { - val range = original.toIndexRange(position.start, position.end) - scribe.trace( - s"${document.filename.replaceFirst(".*/", "")} [${range.pretty}] ${symbol}" - ) - range.contains(originalPosition) - } => - name - } - } yield name -> edit - } - - /** Returns a symbol at the given location */ - def findSymbol( - uri: Uri, - line: Int, - column: Int - ): Option[(Symbol, TokenEditDistance)] = { - for { - (name, edit) <- resolveName(uri, line, column) - symbol = Symbol(name.symbol) - _ = scribe.info(s"Matching symbol ${symbol}") - } yield symbol -> edit - } - - /** Returns symbol definition data from the index taking into account relevant alternatives */ - def definitionData( - symbol: Symbol - ): Option[SymbolData] = { - (symbol :: symbol.definitionAlternative) - .collectFirst { - case symbolIndexer(data) if data.definition.nonEmpty => - scribe.info(s"Found definition symbol ${data.symbol}") - data - } - } - - def data(symbol: Symbol): Option[SymbolData] = - symbolIndexer.get(symbol) - - /** Returns symbol references data from the index taking into account relevant alternatives */ - def referencesData( - symbol: Symbol - ): List[SymbolData] = { - (symbol :: symbol.referenceAlternatives) - .collect { - case symbolIndexer(data) => - if (data.symbol != symbol.syntax) - scribe.info(s"Adding alternative references ${data.symbol}") - data - } - } - - def indexDependencyClasspath( - sourceJars: List[AbsolutePath] - ): Task[Effects.IndexSourcesClasspath] = Task { - if (!config().indexClasspath) Effects.IndexSourcesClasspath - else { - val sourceJarsWithJDK = - if (config().indexJDK) - CompilerConfig.jdkSources.fold(sourceJars)(_ :: sourceJars) - else sourceJars - val buf = List.newBuilder[AbsolutePath] - sourceJarsWithJDK.foreach { jar => - // ensure we only index each jar once even under race conditions. - // race conditions are not unlikely since multiple .compilerconfig - // are typically created at the same time for each project/configuration - // combination. Duplicate tasks are expensive, for example we don't want - // to index the JDK twice on first startup. - indexedJars.computeIfAbsent(jar, _ => buf += jar) - } - val sourceJarsToIndex = buf.result() - // Acquire a lock on the leveldb cache only during indexing. - LevelDBMap.withDB(cacheDirectory.resolve("leveldb").toFile) { db => - sourceJarsToIndex.foreach { path => - scribe.info(s"Indexing classpath entry $path") - try { - val database = db.getOrElseUpdate[AbsolutePath, Database]( - path, - () => Mtags.indexDatabase(path :: Nil) - ) - indexDatabase(database) - } catch { - case NonFatal(e) => - scribe.error(s"Failed to index $path", e) - } - } - } - Effects.IndexSourcesClasspath - } - } - - /** Register this Database to symbol indexer. */ - def indexDatabase(document: s.Database): Effects.IndexSemanticdb = { - document.documents.foreach { doc => - try indexDocument(doc) - catch { - case NonFatal(e) => - scribe.error(s"Failed to index ${doc.filename}", e) - } - } - Effects.IndexSemanticdb - } - - /** - * - * Register this Document to symbol indexer. - * - * Indexes definitions, denotations and references in this document. - * - * @param document Must respect the following conventions: - * - filename must be formatted as a URI - * - names must be sorted - */ - def indexDocument(document: s.Document): Effects.IndexSemanticdb = { - val uri = Uri(document.filename.replace('\\', '/')) - val input = Input.VirtualFile(document.filename, document.contents) - documentIndex.putDocument(uri, document) - document.names.foreach { - // TODO(olafur) handle local symbols on the fly from a `Document` in go-to-definition - // local symbols don't need to be indexed globally, by skipping them we should - // def isLocalSymbol(sym: String): Boolean = - // !sym.endsWith(".") && - // !sym.endsWith("#") && - // !sym.endsWith(")") - // be able to minimize the size of the global index significantly. - // case s.ResolvedName(_, sym, _) if isLocalSymbol(sym) => // Do nothing, local symbol. - case s.ResolvedName(Some(s.Position(start, end)), sym, true) => - symbolIndexer.addDefinition( - sym, - i.Position(document.filename, Some(input.toIndexRange(start, end))) - ) - case s.ResolvedName(Some(s.Position(start, end)), sym, false) => - symbolIndexer.addReference( - document.filename, - input.toIndexRange(start, end), - sym - ) - case _ => - } - document.symbols.foreach { - case s.ResolvedSymbol(sym, Some(denot)) => - symbolIndexer.addDenotation( - sym, - denot.flags, - denot.name, - denot.signature - ) - case _ => - } - Effects.IndexSemanticdb - } - - override def workspaceSymbols(query: String): List[SymbolInformation] = { - import scala.meta.metals.ScalametaEnrichments._ - import scala.meta.semanticdb._ - val result = symbolIndexer.allSymbols.toIterator - .withFilter { symbol => - symbol.definition.isDefined && symbol.definition.get.uri - .startsWith("file:") - } - .collect { - case i.SymbolData(sym, Some(pos), _, flags, name, _) - if flags.hasOneOfFlags(CLASS | TRAIT | OBJECT) && { - // NOTE(olafur) fuzzy-wuzzy doesn't seem to do a great job - // for camelcase searches like "DocSymPr" when looking for - // "DocumentSymbolProvider. We should try and port something - // like https://blog.forrestthewoods.com/reverse-engineering-sublime-text-s-fuzzy-match-4cffeed33fdb - // instead. - FuzzySearch.partialRatio(query, name) >= 90 - } => - SymbolInformation( - name, - flags.toSymbolKind, - pos.toLocation, - Some(sym.stripPrefix("_root_.")) - ) - } - result.toList - } - - def clearIndex(): Unit = indexedJars.clear() - - /** Returns the matching position in the original document. - * - * Falls back to TokenEditDistance in case the current open buffer - * is off-sync with the latest saved semanticdb document. - */ - private def findOriginalPosition( - original: Input.VirtualFile, - revised: Input.VirtualFile, - line: Int, - column: Int - ): Option[(Position, TokenEditDistance)] = { - if (original.value == revised.value) { - // Minor optimization, skip edit-distance when original is synced - Some(original.toPosition(line, column) -> TokenEditDistance.empty) - } else { - for { - edit <- TokenEditDistance(original, revised) - revisedOffset = revised.toOffset(line, column) - originalPosition <- edit.toOriginal(revisedOffset).right.toOption - } yield originalPosition -> edit - } - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndexer.scala b/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndexer.scala deleted file mode 100644 index 44e8a75c8d9..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/InMemorySymbolIndexer.scala +++ /dev/null @@ -1,81 +0,0 @@ -package scala.meta.metals.search - -import java.util.concurrent.atomic.AtomicReference -import java.util.function.UnaryOperator -import scala.collection.concurrent.TrieMap -import scala.meta.metals.index.Position -import scala.meta.metals.index.Range -import scala.meta.metals.index.Ranges -import scala.meta.metals.index.SymbolData -import org.langmeta.semanticdb.Symbol - -class InMemorySymbolIndexer( - // simplest thing I could think of to get something off the ground. - // we may want to consider using a proper key/value store instead. - symbols: collection.concurrent.Map[String, AtomicReference[SymbolData]] = - TrieMap.empty -) extends SymbolIndexer { self => - - override def get(symbol: Symbol): Option[SymbolData] = symbol match { - case Symbol.Multi(ss) => ss.collectFirst { case self(i) => i } - case s: Symbol => get(s.syntax) - } - - override def get(symbol: String): Option[SymbolData] = - symbols - .get(symbol) - .map(_.get) - - override def unapply(arg: Any): Option[SymbolData] = arg match { - case s: String => get(s) - case s: Symbol => get(s) - case _ => None - } - - override def allSymbols: Traversable[SymbolData] = - new Traversable[SymbolData] { - override def foreach[U](f: SymbolData => U): Unit = - symbols.values.foreach(s => f(s.get)) - } - - override def addDefinition( - symbol: String, - position: Position - ): Unit = updated(symbol) { index => - // NOTE(olafur): Here we override the previous definition, in some cases, - // we should accummulate them, for example non-pure JS/JVM/Native projects. - index.copy(definition = Some(position)) - } - - override def addDenotation( - symbol: String, - flags: Long, - name: String, - signature: String - ): Unit = updated(symbol) { index => - index.copy(flags = flags, signature = signature, name = name) - } - - override def addReference( - filename: String, // TODO(olafur) change to java.net.URI? - range: Range, - symbol: String // TODO(olafur) move to first argument? - ): Unit = updated(symbol) { index => - val ranges = index.references.getOrElse(filename, Ranges()) - val newRanges = ranges.addRanges(range) - val newReferences = index.references + (filename -> newRanges) - index.copy(references = newReferences) - } - - private def newValue(symbol: String) = - new AtomicReference(SymbolData(symbol = symbol)) - - private def updated(symbol: String)(f: SymbolData => SymbolData): Unit = { - val value = symbols.getOrElseUpdate(symbol, newValue(symbol)) - value.getAndUpdate(new UnaryOperator[SymbolData] { - override def apply(index: SymbolData): SymbolData = - f(index) - }) - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/search/InverseSymbolIndexer.scala b/metals/src/main/scala/scala/meta/metals/search/InverseSymbolIndexer.scala deleted file mode 100644 index 3b3ebdcbabc..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/InverseSymbolIndexer.scala +++ /dev/null @@ -1,81 +0,0 @@ -package scala.meta.metals.search - -import scala.collection.mutable -import scala.meta.metals.Uri -import scala.meta.metals.{index => i} -import scala.{meta => m} -import org.langmeta.io.AbsolutePath -import org.langmeta.languageserver.InputEnrichments._ - -object InverseSymbolIndexer { - - /** Rebuilds a scala.meta.Database with only names filled out - * - * @param cwd the working directory to relativize file URIs in the symbol index. - * @param documents store for looking up document contents. - * @param symbols symbol index, from [[SymbolIndexer.allSymbols]] - */ - def reconstructDatabase( - cwd: AbsolutePath, - documents: DocumentIndex, - symbols: Traversable[i.SymbolData] - ): m.Database = { - // Reconstruct an m.Database from the symbol index and asserts that the - // reconstructed database is identical to the original semanticdbs that - // built the symbol index. - // TODO(olafur) handle local symbols when we stop indexing them. - val db = mutable.Map.empty[String, m.Document] - def get(uri: Uri) = { - val key = if (uri.isFile) { - cwd.toNIO.relativize(uri.toPath).toString - } else uri.value - db.getOrElseUpdate( - key, - m.Document( - m.Input.VirtualFile( - key, - documents.getDocument(uri).fold("")(_.contents) - ), - "Scala212", - Nil, - Nil, - Nil, - Nil - ) - ) - } - def handleResolvedName( - uri: Uri, - symbol: String, - range: i.Range, - definition: Boolean - ): Unit = { - val doc = get(uri) - val pos = doc.input.toPosition(range) - val rs = - m.ResolvedName(pos, m.Symbol(symbol), isDefinition = definition) - val newDoc = doc.copy(names = rs :: doc.names) - db(doc.input.syntax) = newDoc - } - symbols.foreach { symbol => - symbol.definition.collect { - case i.Position(Uri(uri), Some(range)) => - handleResolvedName(uri, symbol.symbol, range, definition = true) - } - symbol.references.collect { - case (Uri(uri), ranges) => - ranges.ranges.foreach { range => - handleResolvedName(uri, symbol.symbol, range, definition = false) - } - } - } - val reconstructedDatabase = m.Database( - db.values.iterator - .filter(!_.input.syntax.startsWith("jar:")) - .filter(_.input.chars.nonEmpty) - .toArray - .sortBy(_.input.syntax) - ) - reconstructedDatabase - } -} diff --git a/metals/src/main/scala/scala/meta/metals/search/MatchingToken.scala b/metals/src/main/scala/scala/meta/metals/search/MatchingToken.scala deleted file mode 100644 index 2f3d284a551..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/MatchingToken.scala +++ /dev/null @@ -1,9 +0,0 @@ -package scala.meta.metals.search - -import scala.meta.Token - -/** A pair of tokens that align with each other across two different files */ -case class MatchingToken(original: Token, revised: Token) { - override def toString: String = - s"${original.structure} <-> ${revised.structure}" -} diff --git a/metals/src/main/scala/scala/meta/metals/search/SymbolIndex.scala b/metals/src/main/scala/scala/meta/metals/search/SymbolIndex.scala deleted file mode 100644 index 8b9d62e224b..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/SymbolIndex.scala +++ /dev/null @@ -1,81 +0,0 @@ -package scala.meta.metals.search - -import scala.meta.metals.Buffers -import scala.meta.metals.Configuration -import scala.meta.metals.Effects -import scala.meta.metals.Uri -import scala.meta.metals.index.SymbolData -import scala.meta.lsp.SymbolInformation -import scala.meta.jsonrpc.JsonRpcClient -import org.langmeta.internal.semanticdb.{schema => s} -import org.langmeta.io.AbsolutePath -import org.langmeta.semanticdb.Symbol -import monix.eval.Task -import monix.execution.Scheduler -import monix.reactive.Observable - -trait SymbolIndex { - - /** Returns a symbol at the given location */ - def findSymbol( - path: Uri, - line: Int, - column: Int - ): Option[(Symbol, TokenEditDistance)] - - /** Returns symbol definition data from the index taking into account relevant alternatives */ - def definitionData(symbol: Symbol): Option[SymbolData] - - def findDefinition(path: Uri, line: Int, column: Int): Option[SymbolData] = - for { - (symbol, edit) <- findSymbol(path, line, column) - data <- definitionData(symbol) - } yield edit.toRevisedDefinition(data) - - /** Returns symbol data for this exact Symbol */ - def data(symbol: Symbol): Option[SymbolData] - - /** Returns symbol references data from the index taking into account relevant alternatives */ - def referencesData(symbol: Symbol): List[SymbolData] - - def findReferences(path: Uri, line: Int, column: Int): List[SymbolData] = - for { - (symbol, edit) <- findSymbol(path, line, column).toList - data <- referencesData(symbol) - } yield edit.toRevisedReferences(data) - - /** Returns symbol definitions in this workspace */ - def workspaceSymbols(query: String): List[SymbolInformation] - - def indexDependencyClasspath( - sourceJars: List[AbsolutePath] - ): Task[Effects.IndexSourcesClasspath] - - /** Register this Database to symbol indexer. */ - def indexDatabase(document: s.Database): Effects.IndexSemanticdb - - /** Remove any persisted files from index returning to a clean start */ - def clearIndex(): Unit - - def documentIndex: DocumentIndex - -} - -object SymbolIndex { - def apply( - cwd: AbsolutePath, - buffers: Buffers, - configuration: Observable[Configuration] - )(implicit s: Scheduler, client: JsonRpcClient): SymbolIndex = { - val symbolIndexer = new InMemorySymbolIndexer() - val documentIndex = new InMemoryDocumentIndex() - new InMemorySymbolIndex( - symbolIndexer, - documentIndex, - cwd, - buffers, - configuration - ) - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/search/SymbolIndexer.scala b/metals/src/main/scala/scala/meta/metals/search/SymbolIndexer.scala deleted file mode 100644 index 44e3c2d3320..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/SymbolIndexer.scala +++ /dev/null @@ -1,79 +0,0 @@ -package scala.meta.metals.search - -import scala.meta.metals.index._ -import org.langmeta.semanticdb.Symbol - -/** - * A key/value store with String keys (by symbol syntax) and - * SymbolData as values. - * - * A good implementation of this trait should be: - * - Fast: lookups should be instant to be useful from the editor. - * - Compact: memory footprint should be small to fit in-memory even for - * large corpora (>millions of loc) on commodity hardware (dev laptop). - * - Incremental: can register references to a symbol without the symbol's - * definition, and vice-versa. - * - Parallel: all updates are thread safe. - * - Persistable: it's possible to dump this index to file, and load up later. - * (TODO(olafur) not yet implemented) - * All of these features may not be fully implemented yet, but the plan is to - * reach there eventually. - * - * It's possible to rebuild a [[scala.meta.Database]] from a SymbolIndexer with - * [[InverseSymbolIndexer]]. - */ -trait SymbolIndexer { self => - - /** Lookup scala.meta.Symbol */ - def get(symbol: Symbol): Option[SymbolData] - - /** Lookup symbol by its syntax. */ - def get(symbol: String): Option[SymbolData] - - /** Lookup symbol from inside a pattern match */ - def unapply(arg: Any): Option[SymbolData] = arg match { - case s: String => get(s) - case s: Symbol => get(s) - case _ => None - } - - /** Iterator for all indexed symbols */ - def allSymbols: Traversable[SymbolData] - - /** Register the definition of a symbol at a given position. - * - * Overrides existing registered definition. - */ - def addDefinition( - symbol: String, - position: Position - ): Unit - - /** - * Register metadata about a symbol. - * - * @param flags the modifiers of this symbol, see org.langmeta.semanticdb.HasFlags - * @param name the name of the symbol, example "get" for scala.Option.get - * @param signature the type signature of this symbol, example "List[T]" for List.tail - */ - def addDenotation( - symbol: String, - flags: Long, - name: String, - signature: String - ): Unit - - /** - * Reguster a reference/call-site to this symbol. - * - * @param filename must be URI, can either be file on local disk or entry - * in jar/zip. - * @param range start/end offset where this symbol is referenced. - * @param symbol - */ - def addReference( - filename: String, - range: Range, - symbol: String - ): Unit -} diff --git a/metals/src/main/scala/scala/meta/metals/search/TokenEditDistance.scala b/metals/src/main/scala/scala/meta/metals/search/TokenEditDistance.scala deleted file mode 100644 index fdc445652b9..00000000000 --- a/metals/src/main/scala/scala/meta/metals/search/TokenEditDistance.scala +++ /dev/null @@ -1,211 +0,0 @@ -package scala.meta.metals.search - -import scala.annotation.tailrec -import scala.meta._ -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.{index => i} -import difflib._ -import difflib.myers.Equalizer -import org.langmeta.languageserver.InputEnrichments._ - -sealed trait EmptyResult -object EmptyResult { - case object Unchanged extends EmptyResult - case object NoMatch extends EmptyResult - def unchanged: Either[EmptyResult, Position] = Left(Unchanged) - def noMatch: Either[EmptyResult, Position] = Left(NoMatch) -} - -/** Helper to map between position between two similar strings. */ -final class TokenEditDistance private (matching: Array[MatchingToken]) { - private val isEmpty: Boolean = matching.length == 0 - - private val ThisUri: String = originalInput match { - case Input.VirtualFile(uri, _) => uri - case _ => originalInput.syntax - } - private def originalInput: Input = - if (isEmpty) Input.None - else matching(0).original.input - - private def revisedInput: Input = - if (isEmpty) Input.None - else matching(0).revised.input - - def toRevised( - originalLine: Int, - originalColumn: Int - ): Either[EmptyResult, Position] = { - toRevised(originalInput.toOffset(originalLine, originalColumn)) - } - - /** Convert from offset in original string to offset in revised string */ - def toRevised(originalOffset: Int): Either[EmptyResult, Position] = { - if (isEmpty) EmptyResult.unchanged - else { - BinarySearch - .array[MatchingToken]( - matching, - mt => compare(mt.original.pos, originalOffset) - ) - .fold(EmptyResult.noMatch)(m => Right(m.revised.pos)) - } - } - - private object RevisedRange { - def unapply(range: i.Range): Option[i.Range] = - toRevised(range.startLine, range.startColumn) match { - case Left(EmptyResult.NoMatch) => None - case Left(EmptyResult.Unchanged) => Some(range) - case Right(newPos) => Some(newPos.toIndexRange) - } - } - - /** Convert the reference positions to match the revised input. */ - def toRevisedReferences(data: i.SymbolData): i.SymbolData = { - val referencesAdjusted = data.references.get(ThisUri) match { - case Some(i.Ranges(ranges)) => - val newRanges = ranges.collect { case RevisedRange(range) => range } - val newData = data.copy( - references = data.references + (ThisUri -> i.Ranges(newRanges)) - ) - newData - case _ => data - } - toRevisedDefinition(referencesAdjusted) - } - - /** Convert the definition position to match the revised input. */ - def toRevisedDefinition(data: i.SymbolData): i.SymbolData = { - data.definition match { - case Some(i.Position(ThisUri, Some(range))) => - toRevised(range.startLine, range.startColumn) match { - case Left(EmptyResult.NoMatch) => data.copy(definition = None) - case Left(EmptyResult.Unchanged) => data - case Right(newPos) => - val newData = data.copy( - definition = Some( - i.Position( - ThisUri, - Some(newPos.toIndexRange) - ) - ) - ) - newData - } - case _ => data - } - } - - def toOriginal( - revisedLine: Int, - revisedColumn: Int - ): Either[EmptyResult, Position] = { - toOriginal(revisedInput.toOffset(revisedLine, revisedColumn)) - } - - /** Convert from offset in revised string to offset in original string */ - def toOriginal(revisedOffset: Int): Either[EmptyResult, Position] = { - if (isEmpty) EmptyResult.unchanged - else { - BinarySearch - .array[MatchingToken]( - matching, - mt => compare(mt.revised.pos, revisedOffset) - ) - .fold(EmptyResult.noMatch)(m => Right(m.original.pos)) - } - } - - private def compare( - pos: Position, - offset: Int - ): BinarySearch.ComparisonResult = - if (pos.contains(offset)) BinarySearch.Equal - else if (pos.end <= offset) BinarySearch.Smaller - else BinarySearch.Greater - -} - -object TokenEditDistance { - - lazy val empty: TokenEditDistance = new TokenEditDistance(Array.empty) - - /** - * Build utility to map offsets between two slightly different strings. - * - * @param original The original snapshot of a string, for example the latest - * semanticdb snapshot. - * @param revised The current snapshot of a string, for example open buffer - * in an editor. - */ - def apply(original: Tokens, revised: Tokens): TokenEditDistance = { - val buffer = Array.newBuilder[MatchingToken] - buffer.sizeHint(math.max(original.length, revised.length)) - @tailrec - def loop( - i: Int, - j: Int, - ds: List[Delta[Token]] - ): Unit = { - val isDone: Boolean = - i >= original.length || - j >= revised.length - if (isDone) () - else { - val o = original(i) - val r = revised(j) - if (TokenEqualizer.equals(o, r)) { - buffer += MatchingToken(o, r) - loop(i + 1, j + 1, ds) - } else { - ds match { - case Nil => - loop(i + 1, j + 1, ds) - case delta :: tail => - loop( - i + delta.getOriginal.size(), - j + delta.getRevised.size(), - tail - ) - } - } - } - } - val deltas = { - import scala.collection.JavaConverters._ - DiffUtils - .diff(original.asJava, revised.asJava, TokenEqualizer) - .getDeltas - .iterator() - .asScala - .toList - } - loop(0, 0, deltas) - new TokenEditDistance(buffer.result()) - } - - def apply( - originalInput: Input, - revisedInput: Input - ): Option[TokenEditDistance] = { - for { - revised <- revisedInput.tokenize.toOption - original <- { - if (originalInput == revisedInput) Some(revised) - else originalInput.tokenize.toOption - } - } yield apply(original, revised) - } - - def apply(original: String, revised: String): Option[TokenEditDistance] = - apply(Input.String(original), Input.String(revised)) - - /** Compare tokens only by their text and token category. */ - private object TokenEqualizer extends Equalizer[Token] { - override def equals(original: Token, revised: Token): Boolean = - original.productPrefix == revised.productPrefix && - original.pos.text == revised.pos.text - } - -} diff --git a/metals/src/main/scala/scala/meta/metals/storage/Bytes.scala b/metals/src/main/scala/scala/meta/metals/storage/Bytes.scala deleted file mode 100644 index 98fa5a917db..00000000000 --- a/metals/src/main/scala/scala/meta/metals/storage/Bytes.scala +++ /dev/null @@ -1,35 +0,0 @@ -package scala.meta.metals.storage - -import java.nio.charset.StandardCharsets -import org.langmeta.internal.semanticdb.schema.Database -import org.langmeta.io.AbsolutePath - -trait FromBytes[A] { self => - def fromBytes(bytes: Array[Byte]): A - def map[B](f: A => B): FromBytes[B] = - bytes => f(self.fromBytes(bytes)) -} -object FromBytes { - implicit val StringFromBytes: FromBytes[String] = - new String(_, StandardCharsets.UTF_8) - implicit val ByteArrayFromBytes: FromBytes[Array[Byte]] = - identity[Array[Byte]] - implicit val DatabaseFromBytes: FromBytes[Database] = - bytes => Database.parseFrom(bytes) -} - -trait ToBytes[A] { self => - def toBytes(e: A): Array[Byte] - def contramap[B](f: B => A): ToBytes[B] = - e => self.toBytes(f(e)) -} -object ToBytes { - implicit val StringToBytes: ToBytes[String] = - _.getBytes(StandardCharsets.UTF_8) - implicit val ByteArrayToBytes: ToBytes[Array[Byte]] = - identity[Array[Byte]] - implicit val DatabaseToBytes: ToBytes[Database] = - _.toByteArray - implicit val AbsolutePathToBytes: ToBytes[AbsolutePath] = - _.toString().getBytes(StandardCharsets.UTF_8) -} diff --git a/metals/src/main/scala/scala/meta/metals/storage/LevelDBMap.scala b/metals/src/main/scala/scala/meta/metals/storage/LevelDBMap.scala deleted file mode 100644 index 76e84c53ea7..00000000000 --- a/metals/src/main/scala/scala/meta/metals/storage/LevelDBMap.scala +++ /dev/null @@ -1,90 +0,0 @@ -package scala.meta.metals.storage - -import java.io.File -import org.fusesource.leveldbjni.JniDBFactory -import org.iq80.leveldb.DB -import org.iq80.leveldb.DBException -import org.iq80.leveldb.Options - -/** - * A Scala-friendly wrapper around the JniDBFactory Java-wrapper around leveldb. - * - * @param db The leveldb, remember to close it after using. This wrapper will NOT - * close the db for you. - */ -class LevelDBMap(db: DB) { - - /** Returns the value matching key, if any. */ - @throws[DBException] - def get[Key, Value](key: Key)( - implicit - keys: ToBytes[Key], - values: FromBytes[Value] - ): Option[Value] = { - Option(db.get(keys.toBytes(key))).map(values.fromBytes) - } - - /** - * Gets the value if it exists, otherwise computes the fallback value and stores it. - * - * This method is not thread-safe, the computed fallback value may get overwritten. - */ - @throws[DBException] - def getOrElseUpdate[Key, Value](key: Key, orElse: () => Value)( - implicit - keys: ToBytes[Key], - valuesFrom: FromBytes[Value], - valuesTo: ToBytes[Value] - ): Value = { - get(key) match { - case Some(value) => value - case None => - val computed = orElse() - put(key, computed) - } - } - - /** Inserts a new value for the given key. */ - @throws[DBException] - def put[Key, Value](key: Key, value: Value)( - implicit - keys: ToBytes[Key], - values: ToBytes[Value] - ): Value = { - db.put(keys.toBytes(key), values.toBytes(value)) - value - } - - def close(): Unit = db.close() -} - -object LevelDBMap { - - /** Construct new wrapper around a leveldb. */ - def apply(db: DB): LevelDBMap = - new LevelDBMap(db) - - /** - * Creates a new leveldb in the given directory. - * - * Make sure to `db.close()`. - */ - def createDBThatIPromiseToClose(directory: File): DB = { - val options = new Options - options.createIfMissing(true) - options.maxOpenFiles() - JniDBFactory.factory.open(directory, options) - } - - def withDB[T](directory: File)(f: LevelDBMap => T): T = { - // TODO(olafur) gracefully fallback when the db is in use by another thread. - // can happen with multiple language servers running at the same time. - val db = createDBThatIPromiseToClose(directory) - try { - f(apply(db)) - } finally { - db.close() - } - } - -} diff --git a/metals/src/main/scala/scalafix/languageserver/ScalafixEnrichments.scala b/metals/src/main/scala/scalafix/languageserver/ScalafixEnrichments.scala deleted file mode 100644 index 63dea8575b5..00000000000 --- a/metals/src/main/scala/scalafix/languageserver/ScalafixEnrichments.scala +++ /dev/null @@ -1,60 +0,0 @@ -package scalafix.languageserver - -import scala.meta.Tree -import scala.meta.metals.ScalametaEnrichments._ -import scala.{meta => m} -import scalafix.Rule -import scalafix.SemanticdbIndex -import scalafix.internal.config.ScalafixConfig -import scalafix.internal.util.EagerInMemorySemanticdbIndex -import scalafix.lint.LintMessage -import scalafix.lint.LintSeverity -import scalafix.patch.Patch -import scalafix.rule.RuleCtx -import scalafix.rule.RuleName -import scala.meta.{lsp => l} - -object ScalafixEnrichments { - implicit class XtensionLintMessageLSP(val msg: LintMessage) extends AnyVal { - def toLSP: l.Diagnostic = - l.Diagnostic( - range = msg.position.toRange, - severity = Some(msg.category.severity.toLSP), - code = Some(msg.category.id), - source = Some("scalafix"), - message = msg.message - ) - } - implicit class XtensionLintSeverityLSP(val severity: LintSeverity) - extends AnyVal { - def toLSP: l.DiagnosticSeverity = severity match { - case LintSeverity.Error => l.DiagnosticSeverity.Error - case LintSeverity.Warning => l.DiagnosticSeverity.Warning - case LintSeverity.Info => l.DiagnosticSeverity.Information - } - } - implicit class XtensionRuleCtxLSP(val `_`: RuleCtx.type) extends AnyVal { - def applyInternal(tree: Tree, config: ScalafixConfig): RuleCtx = - RuleCtx(tree, config) - } - implicit class XtensionPatchLSPObject(val `_`: Patch.type) extends AnyVal { - def lintMessagesInternal( - patches: Map[RuleName, Patch], - ctx: RuleCtx - ): List[LintMessage] = - Patch.lintMessages(patches, ctx) - } - implicit class XtensionRuleLSP(val rule: Rule) extends AnyVal { - def fixWithNameInternal(ctx: RuleCtx): Map[RuleName, Patch] = - rule.fixWithName(ctx) - } - implicit class XtensionSemanticdbIndexObject(val `_`: SemanticdbIndex.type) - extends AnyVal { - def load(document: m.Document): SemanticdbIndex = - EagerInMemorySemanticdbIndex( - m.Database(document :: Nil), - m.Sourcepath(Nil), - m.Classpath(Nil) - ) - } -} diff --git a/metals/src/main/scala/scalafix/languageserver/ScalafixPatchEnrichments.scala b/metals/src/main/scala/scalafix/languageserver/ScalafixPatchEnrichments.scala deleted file mode 100644 index 79cf721fbea..00000000000 --- a/metals/src/main/scala/scalafix/languageserver/ScalafixPatchEnrichments.scala +++ /dev/null @@ -1,117 +0,0 @@ -package scalafix.languageserver - -import scala.collection.immutable.Seq -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.lsp.TextEdit -import scalafix.SemanticdbIndex -import scalafix.internal.patch.ImportPatchOps -import scalafix.internal.patch.ReplaceSymbolOps -import scalafix.internal.util.Failure -import scalafix.internal.util.TokenOps -import scalafix.patch.Concat -import scalafix.patch.EmptyPatch -import scalafix.patch.LintPatch -import scalafix.patch.Patch -import scalafix.patch.TokenPatch -import scalafix.patch.TreePatch.ImportPatch -import scalafix.patch.TreePatch.ReplaceSymbol -import scalafix.rule.RuleCtx - -// Copy-pasta from scalafix because all of these methods are private. -// We should expose a package private API to get a list of token patches from -// a Patch. -// TODO(olafur): Figure out how to expose a minimal public API in scalafix.Patch -// that supports this use-case. -// All of the copy-paste below could be avoided with a single: -// Patch.toTokenPatches(Patch): Iterable[TokenPatch] -object ScalafixPatchEnrichments { - - implicit class XtensionPatchLSP(val patch: Patch) extends AnyVal { - - /** Converts a scalafix.Patch to precise languageserver.types.TextEdit. - * - * We could take a shortcut and apply the patch to a String and return one - * large TextEdit that replaces the whole file. However, in scalafix - * we treat each token individually so we can provide more precise changes. - */ - def toTextEdits( - implicit ctx: RuleCtx, - index: SemanticdbIndex - ): List[TextEdit] = { - val mergedTokenPatches = tokenPatches(patch) - .groupBy(x => TokenOps.hash(x.tok)) - .values - .map(_.reduce(merge)) - mergedTokenPatches.toArray - .sortBy(_.tok.pos.start) - .iterator - .map { tokenPatch => - TextEdit(tokenPatch.tok.pos.toRange, tokenPatch.newTok) - } - .toList - } - } - private def tokenPatches( - patch: Patch - )(implicit ctx: RuleCtx, index: SemanticdbIndex): Iterable[TokenPatch] = { - val base = underlying(patch) - val moveSymbol = underlying( - ReplaceSymbolOps.naiveMoveSymbolPatch(base.collect { - case m: ReplaceSymbol => m - }) - ) - val patches = base.filterNot(_.isInstanceOf[ReplaceSymbol]) ++ moveSymbol - val tokenPatches = patches.collect { case e: TokenPatch => e } - val importPatches = patches.collect { case e: ImportPatch => e } - val importTokenPatches = { - val result = ImportPatchOps.superNaiveImportPatchToTokenPatchConverter( - ctx, - importPatches - ) - underlying(result.asPatch) - .collect { - case x: TokenPatch => x - case els => - throw Failure.InvariantFailedException( - s"Expected TokenPatch, got $els" - ) - } - } - importTokenPatches ++ tokenPatches - } - private def underlying(patch: Patch): Seq[Patch] = { - val builder = Seq.newBuilder[Patch] - foreach(patch) { - case _: LintPatch => - case els => - builder += els - } - builder.result() - } - private def foreach(patch: Patch)(f: Patch => Unit): Unit = { - def loop(patch: Patch): Unit = patch match { - case Concat(a, b) => - loop(a) - loop(b) - case EmptyPatch => // do nothing - case els => - f(els) - } - loop(patch) - } - - import scalafix.patch.TokenPatch._ - private def merge(a: TokenPatch, b: TokenPatch): TokenPatch = (a, b) match { - case (add1: Add, add2: Add) => - Add( - add1.tok, - add1.addLeft + add2.addLeft, - add1.addRight + add2.addRight, - add1.keepTok && add2.keepTok - ) - case (_: Remove, add: Add) => add.copy(keepTok = false) - case (add: Add, _: Remove) => add.copy(keepTok = false) - case (rem: Remove, rem2: Remove) => rem - case _ => throw Failure.TokenPatchMergeError(a, b) - } -} diff --git a/metals/src/test/scala/tests/DiffAsserts.scala b/metals/src/test/scala/tests/DiffAsserts.scala deleted file mode 100644 index 9fa3009ae65..00000000000 --- a/metals/src/test/scala/tests/DiffAsserts.scala +++ /dev/null @@ -1,84 +0,0 @@ -package tests - -import scala.util.matching.Regex - -object DiffAsserts { - - def assertNoDiff( - obtained: String, - expected: String, - title: String = "" - ): Boolean = { - val result = compareContents(obtained, expected) - if (result.isEmpty) true - else { - throw DiffFailure(title, expected, obtained, result) - } - } - - private def header[T](t: T): String = { - val line = s"=" * (t.toString.length + 3) - s"$line\n=> $t\n$line" - } - - private case class DiffFailure( - title: String, - expected: String, - obtained: String, - diff: String - ) extends Exception( - title + "\n" + error2message(obtained, expected) - ) - - def error2message(obtained: String, expected: String): String = { - val sb = new StringBuilder - val obtainedStr = - if (obtained.length < 1000) stripTrailingWhitespace(obtained) - else s"<...truncated>" - sb.append( - s"""#${header("Obtained")} - #$obtainedStr - # - #""".stripMargin('#') - ) - sb.append( - s"""#${header("Diff")} - #${stripTrailingWhitespace(compareContents(obtained, expected))}""" - .stripMargin('#') - ) - sb.toString() - } - - val linebreak: Regex = "(\n|\r\n|\r)".r - - private def stripTrailingWhitespace(str: String): String = - str.replaceAll(" \n", "∙\n") - - def compareContents(original: String, revised: String): String = - compareContents( - linebreak.split(original.trim), - linebreak.split(revised.trim) - ) - - private def compareContents( - original: Seq[String], - revised: Seq[String] - ): String = { - import collection.JavaConverters._ - val diff = difflib.DiffUtils.diff(original.asJava, revised.asJava) - if (diff.getDeltas.isEmpty) "" - else - difflib.DiffUtils - .generateUnifiedDiff( - "original", - "revised", - original.asJava, - diff, - 1 - ) - .asScala - .drop(3) - .mkString("\n") - } - -} diff --git a/metals/src/test/scala/tests/FormatterTest.scala b/metals/src/test/scala/tests/FormatterTest.scala deleted file mode 100644 index e9655cdd11e..00000000000 --- a/metals/src/test/scala/tests/FormatterTest.scala +++ /dev/null @@ -1,45 +0,0 @@ -package tests - -import java.nio.file.Files -import scala.meta.metals.Formatter -import org.langmeta.internal.io.PathIO -import org.langmeta.io.AbsolutePath - -object FormatterTest extends MegaSuite { - - test("noop does nothing") { - assertNoDiff(Formatter.noop.format("blah", ""), "blah") - assertNoDiff( - Formatter.noop.format("blah", "", PathIO.workingDirectory), - "blah" - ) - } - - lazy val scalafmt: Formatter = Formatter.classloadScalafmt("1.3.0") - val original = "object a { val x = 2}" - - test("scalafmt with no config") { - val obtained = scalafmt.format(original, "a.scala") - val expected = "object a { val x = 2 }" - assertNoDiff(obtained, expected) - } - - test("scalafmt with config") { - val config = "maxColumn = 10" - val file = Files.createTempFile("scalafmt", ".scalafmt.conf") - file.toFile.deleteOnExit() - Files.write(file, config.getBytes) - val obtained = scalafmt.format( - original, - "a.scala", - AbsolutePath(file) - ) - val expected = - """object a { - | val x = - | 2 - |}""".stripMargin - assertNoDiff(obtained, expected) - } - -} diff --git a/metals/src/test/scala/tests/mtags/BaseMtagsTest.scala b/metals/src/test/scala/tests/mtags/BaseMtagsTest.scala deleted file mode 100644 index 82b060a36e7..00000000000 --- a/metals/src/test/scala/tests/mtags/BaseMtagsTest.scala +++ /dev/null @@ -1,23 +0,0 @@ -package tests.mtags - -import scala.meta.metals.mtags.Mtags -import org.langmeta.internal.semanticdb.schema.Database -import tests.MegaSuite - -class BaseMtagsTest extends MegaSuite { - def checkIgnore( - filename: String, - original: String, - expected: String - ): Unit = { - ignore(filename) {} - } - def check(filename: String, original: String, expected: String): Unit = { - test(filename) { - val sdb = Database(Mtags.index(filename, original) :: Nil) - val obtained = sdb.toDb(None).documents.head.syntax -// println(obtained) - assertNoDiff(obtained, expected) - } - } -} diff --git a/metals/src/test/scala/tests/mtags/ClasspathMtagsTest.scala b/metals/src/test/scala/tests/mtags/ClasspathMtagsTest.scala deleted file mode 100644 index 7e263cef511..00000000000 --- a/metals/src/test/scala/tests/mtags/ClasspathMtagsTest.scala +++ /dev/null @@ -1,269 +0,0 @@ -package tests.mtags - -import java.nio.file.Paths -import scala.meta.metals.Jars -import scala.meta.metals.mtags.Mtags -import org.langmeta.internal.semanticdb.schema.Database -import tests.MegaSuite - -object ClasspathMtagsTest extends MegaSuite { - - // NOTE(olafur) this test is a bit slow since it downloads jars from the internet. - ignore("index classpath") { - val classpath = Jars.fetch( - "com.lihaoyi", - "sourcecode_2.12", - "0.1.4", - System.out, - fetchSourceJars = true - ) - val Compat = Paths.get("sourcecode").resolve("Compat.scala") - val SourceContext = Paths.get("sourcecode").resolve("SourceContext.scala") - val Predef = Paths.get("scala").resolve("io").resolve("AnsiColor.scala") - val CharRef = Paths.get("scala").resolve("runtime").resolve("CharRef.java") - val docs = List.newBuilder[String] - Mtags.index( - classpath, - shouldIndex = { path => - path.toNIO.endsWith(CharRef) || - path.toNIO.endsWith(Compat) || - path.toNIO.endsWith(SourceContext) || - path.toNIO.endsWith(Predef) - } - ) { doc => - val path = Paths.get(doc.filename).getFileName.toString - val underline = "-" * path.length - val mdoc = Database(doc :: Nil).toDb(None).documents.head.toString() - docs += - s"""$path - |$underline - | - |$mdoc""".stripMargin - } - val obtained = docs.result().sorted.mkString("\n\n") - val expected = - """ - |AnsiColor.scala - |--------------- - | - |Language: - |Scala212 - | - |Names: - |[3580..3589): AnsiColor <= _root_.scala.io.AnsiColor# - |[3674..3679): BLACK <= _root_.scala.io.AnsiColor#BLACK. - |[3778..3781): RED <= _root_.scala.io.AnsiColor#RED. - |[3886..3891): GREEN <= _root_.scala.io.AnsiColor#GREEN. - |[3996..4002): YELLOW <= _root_.scala.io.AnsiColor#YELLOW. - |[4102..4106): BLUE <= _root_.scala.io.AnsiColor#BLUE. - |[4214..4221): MAGENTA <= _root_.scala.io.AnsiColor#MAGENTA. - |[4320..4324): CYAN <= _root_.scala.io.AnsiColor#CYAN. - |[4428..4433): WHITE <= _root_.scala.io.AnsiColor#WHITE. - |[4537..4544): BLACK_B <= _root_.scala.io.AnsiColor#BLACK_B. - |[4641..4646): RED_B <= _root_.scala.io.AnsiColor#RED_B. - |[4749..4756): GREEN_B <= _root_.scala.io.AnsiColor#GREEN_B. - |[4859..4867): YELLOW_B <= _root_.scala.io.AnsiColor#YELLOW_B. - |[4965..4971): BLUE_B <= _root_.scala.io.AnsiColor#BLUE_B. - |[5077..5086): MAGENTA_B <= _root_.scala.io.AnsiColor#MAGENTA_B. - |[5183..5189): CYAN_B <= _root_.scala.io.AnsiColor#CYAN_B. - |[5291..5298): WHITE_B <= _root_.scala.io.AnsiColor#WHITE_B. - |[5388..5393): RESET <= _root_.scala.io.AnsiColor#RESET. - |[5475..5479): BOLD <= _root_.scala.io.AnsiColor#BOLD. - |[5568..5578): UNDERLINED <= _root_.scala.io.AnsiColor#UNDERLINED. - |[5656..5661): BLINK <= _root_.scala.io.AnsiColor#BLINK. - |[5747..5755): REVERSED <= _root_.scala.io.AnsiColor#REVERSED. - |[5839..5848): INVISIBLE <= _root_.scala.io.AnsiColor#INVISIBLE. - |[5874..5883): AnsiColor <= _root_.scala.io.AnsiColor. - | - |Symbols: - |_root_.scala.io.AnsiColor# => trait AnsiColor - |_root_.scala.io.AnsiColor#BLACK. => def BLACK - |_root_.scala.io.AnsiColor#BLACK_B. => def BLACK_B - |_root_.scala.io.AnsiColor#BLINK. => def BLINK - |_root_.scala.io.AnsiColor#BLUE. => def BLUE - |_root_.scala.io.AnsiColor#BLUE_B. => def BLUE_B - |_root_.scala.io.AnsiColor#BOLD. => def BOLD - |_root_.scala.io.AnsiColor#CYAN. => def CYAN - |_root_.scala.io.AnsiColor#CYAN_B. => def CYAN_B - |_root_.scala.io.AnsiColor#GREEN. => def GREEN - |_root_.scala.io.AnsiColor#GREEN_B. => def GREEN_B - |_root_.scala.io.AnsiColor#INVISIBLE. => def INVISIBLE - |_root_.scala.io.AnsiColor#MAGENTA. => def MAGENTA - |_root_.scala.io.AnsiColor#MAGENTA_B. => def MAGENTA_B - |_root_.scala.io.AnsiColor#RED. => def RED - |_root_.scala.io.AnsiColor#RED_B. => def RED_B - |_root_.scala.io.AnsiColor#RESET. => def RESET - |_root_.scala.io.AnsiColor#REVERSED. => def REVERSED - |_root_.scala.io.AnsiColor#UNDERLINED. => def UNDERLINED - |_root_.scala.io.AnsiColor#WHITE. => def WHITE - |_root_.scala.io.AnsiColor#WHITE_B. => def WHITE_B - |_root_.scala.io.AnsiColor#YELLOW. => def YELLOW - |_root_.scala.io.AnsiColor#YELLOW_B. => def YELLOW_B - |_root_.scala.io.AnsiColor. => object AnsiColor - | - | - |CharRef.java - |------------ - | - |Language: - |Java - | - |Names: - |[267..272): scala => _root_.scala. - |[542..549): runtime => _root_.scala.runtime. - |[566..573): CharRef <= _root_.scala.runtime.CharRef. - |[566..573): CharRef <= _root_.scala.runtime.CharRef# - |[638..654): serialVersionUID <= _root_.scala.runtime.CharRef.serialVersionUID. - |[696..700): elem <= _root_.scala.runtime.CharRef#elem. - |[772..780): toString <= _root_.scala.runtime.CharRef#toString. - |[857..863): create <= _root_.scala.runtime.CharRef.create. - |[925..929): zero <= _root_.scala.runtime.CharRef.zero. - | - |Symbols: - |_root_.scala. => package scala - |_root_.scala.runtime. => package runtime - |_root_.scala.runtime.CharRef# => class CharRef - |_root_.scala.runtime.CharRef#elem. => var elem - |_root_.scala.runtime.CharRef#toString. => def toString - |_root_.scala.runtime.CharRef. => object CharRef - |_root_.scala.runtime.CharRef.create. => def create - |_root_.scala.runtime.CharRef.serialVersionUID. => val serialVersionUID - |_root_.scala.runtime.CharRef.zero. => def zero - | - | - |Compat.scala - |------------ - | - |Language: - |Scala212 - | - |Names: - |[27..33): Compat <= _root_.sourcecode.Compat. - |[96..110): enclosingOwner <= _root_.sourcecode.Compat.enclosingOwner. - |[158..176): enclosingParamList <= _root_.sourcecode.Compat.enclosingParamList. - | - |Symbols: - |_root_.sourcecode.Compat. => object Compat - |_root_.sourcecode.Compat.enclosingOwner. => def enclosingOwner - |_root_.sourcecode.Compat.enclosingParamList. => def enclosingParamList - | - | - |SourceContext.scala - |------------------- - | - |Language: - |Scala212 - | - |Names: - |[65..69): Util <= _root_.sourcecode.Util. - |[77..88): isSynthetic <= _root_.sourcecode.Util.isSynthetic. - |[160..175): isSyntheticName <= _root_.sourcecode.Util.isSyntheticName. - |[279..286): getName <= _root_.sourcecode.Util.getName. - |[367..378): SourceValue <= _root_.sourcecode.SourceValue# - |[415..430): SourceCompanion <= _root_.sourcecode.SourceCompanion# - |[477..482): apply <= _root_.sourcecode.SourceCompanion#apply. - |[528..532): wrap <= _root_.sourcecode.SourceCompanion#wrap. - |[566..570): Name <= _root_.sourcecode.Name# - |[621..625): Name <= _root_.sourcecode.Name. - |[728..732): impl <= _root_.sourcecode.Name.impl. - |[1015..1022): Machine <= _root_.sourcecode.Name.Machine# - |[1075..1082): Machine <= _root_.sourcecode.Name.Machine. - |[1197..1201): impl <= _root_.sourcecode.Name.Machine.impl. - |[1435..1443): FullName <= _root_.sourcecode.FullName# - |[1494..1502): FullName <= _root_.sourcecode.FullName. - |[1617..1621): impl <= _root_.sourcecode.FullName.impl. - |[1952..1959): Machine <= _root_.sourcecode.FullName.Machine# - |[2012..2019): Machine <= _root_.sourcecode.FullName.Machine. - |[2135..2139): impl <= _root_.sourcecode.FullName.Machine.impl. - |[2366..2370): File <= _root_.sourcecode.File# - |[2421..2425): File <= _root_.sourcecode.File. - |[2539..2543): impl <= _root_.sourcecode.File.impl. - |[2735..2739): Line <= _root_.sourcecode.Line# - |[2784..2788): Line <= _root_.sourcecode.Line. - |[2898..2902): impl <= _root_.sourcecode.Line.impl. - |[3087..3096): Enclosing <= _root_.sourcecode.Enclosing# - |[3148..3157): Enclosing <= _root_.sourcecode.Enclosing. - |[3274..3278): impl <= _root_.sourcecode.Enclosing.impl. - |[3395..3402): Machine <= _root_.sourcecode.Enclosing.Machine# - |[3455..3462): Machine <= _root_.sourcecode.Enclosing.Machine. - |[3577..3581): impl <= _root_.sourcecode.Enclosing.Machine.impl. - |[3678..3681): Pkg <= _root_.sourcecode.Pkg# - |[3732..3735): Pkg <= _root_.sourcecode.Pkg. - |[3834..3838): impl <= _root_.sourcecode.Pkg.impl. - |[3924..3928): Text <= _root_.sourcecode.Text# - |[3965..3969): Text <= _root_.sourcecode.Text. - |[4102..4106): Args <= _root_.sourcecode.Args# - |[4179..4183): Args <= _root_.sourcecode.Args. - |[4297..4301): impl <= _root_.sourcecode.Args.impl. - |[4627..4632): Impls <= _root_.sourcecode.Impls. - |[4640..4644): text <= _root_.sourcecode.Impls.text. - |[5312..5317): Chunk <= _root_.sourcecode.Impls.Chunk# - |[5327..5332): Chunk <= _root_.sourcecode.Impls.Chunk. - |[5349..5352): Pkg <= _root_.sourcecode.Impls.Chunk.Pkg# - |[5396..5399): Obj <= _root_.sourcecode.Impls.Chunk.Obj# - |[5443..5446): Cls <= _root_.sourcecode.Impls.Chunk.Cls# - |[5490..5493): Trt <= _root_.sourcecode.Impls.Chunk.Trt# - |[5537..5540): Val <= _root_.sourcecode.Impls.Chunk.Val# - |[5584..5587): Var <= _root_.sourcecode.Impls.Chunk.Var# - |[5631..5634): Lzy <= _root_.sourcecode.Impls.Chunk.Lzy# - |[5678..5681): Def <= _root_.sourcecode.Impls.Chunk.Def# - |[5722..5731): enclosing <= _root_.sourcecode.Impls.enclosing. - | - |Symbols: - |_root_.sourcecode.Args# => class Args - |_root_.sourcecode.Args. => object Args - |_root_.sourcecode.Args.impl. => def impl - |_root_.sourcecode.Enclosing# => class Enclosing - |_root_.sourcecode.Enclosing. => object Enclosing - |_root_.sourcecode.Enclosing.Machine# => class Machine - |_root_.sourcecode.Enclosing.Machine. => object Machine - |_root_.sourcecode.Enclosing.Machine.impl. => def impl - |_root_.sourcecode.Enclosing.impl. => def impl - |_root_.sourcecode.File# => class File - |_root_.sourcecode.File. => object File - |_root_.sourcecode.File.impl. => def impl - |_root_.sourcecode.FullName# => class FullName - |_root_.sourcecode.FullName. => object FullName - |_root_.sourcecode.FullName.Machine# => class Machine - |_root_.sourcecode.FullName.Machine. => object Machine - |_root_.sourcecode.FullName.Machine.impl. => def impl - |_root_.sourcecode.FullName.impl. => def impl - |_root_.sourcecode.Impls. => object Impls - |_root_.sourcecode.Impls.Chunk# => trait Chunk - |_root_.sourcecode.Impls.Chunk. => object Chunk - |_root_.sourcecode.Impls.Chunk.Cls# => class Cls - |_root_.sourcecode.Impls.Chunk.Def# => class Def - |_root_.sourcecode.Impls.Chunk.Lzy# => class Lzy - |_root_.sourcecode.Impls.Chunk.Obj# => class Obj - |_root_.sourcecode.Impls.Chunk.Pkg# => class Pkg - |_root_.sourcecode.Impls.Chunk.Trt# => class Trt - |_root_.sourcecode.Impls.Chunk.Val# => class Val - |_root_.sourcecode.Impls.Chunk.Var# => class Var - |_root_.sourcecode.Impls.enclosing. => def enclosing - |_root_.sourcecode.Impls.text. => def text - |_root_.sourcecode.Line# => class Line - |_root_.sourcecode.Line. => object Line - |_root_.sourcecode.Line.impl. => def impl - |_root_.sourcecode.Name# => class Name - |_root_.sourcecode.Name. => object Name - |_root_.sourcecode.Name.Machine# => class Machine - |_root_.sourcecode.Name.Machine. => object Machine - |_root_.sourcecode.Name.Machine.impl. => def impl - |_root_.sourcecode.Name.impl. => def impl - |_root_.sourcecode.Pkg# => class Pkg - |_root_.sourcecode.Pkg. => object Pkg - |_root_.sourcecode.Pkg.impl. => def impl - |_root_.sourcecode.SourceCompanion# => class SourceCompanion - |_root_.sourcecode.SourceCompanion#apply. => def apply - |_root_.sourcecode.SourceCompanion#wrap. => def wrap - |_root_.sourcecode.SourceValue# => class SourceValue - |_root_.sourcecode.Text# => class Text - |_root_.sourcecode.Text. => object Text - |_root_.sourcecode.Util. => object Util - |_root_.sourcecode.Util.getName. => def getName - |_root_.sourcecode.Util.isSynthetic. => def isSynthetic - |_root_.sourcecode.Util.isSyntheticName. => def isSyntheticName - """.stripMargin - assertNoDiff(obtained, expected) - } -} diff --git a/metals/src/test/scala/tests/mtags/JavaMtagsTest.scala b/metals/src/test/scala/tests/mtags/JavaMtagsTest.scala deleted file mode 100644 index a64f39a3076..00000000000 --- a/metals/src/test/scala/tests/mtags/JavaMtagsTest.scala +++ /dev/null @@ -1,216 +0,0 @@ -package tests.mtags - -import java.nio.file.Paths -import scala.meta.metals.compiler.CompilerConfig -import scala.meta.metals.mtags.Mtags - -object JavaMtagsTest extends BaseMtagsTest { - check( - "interface.java", - """package a.b; - |interface A { - | public String a(); - |} - |""".stripMargin, - """ - |Language: - |Java - | - |Names: - |[8..9): a => _root_.a. - |[10..11): b => _root_.a.b. - |[23..24): A <= _root_.a.b.A. - |[23..24): A <= _root_.a.b.A# - |[43..44): a <= _root_.a.b.A#a. - | - |Symbols: - |_root_.a. => package a - |_root_.a.b. => package b - |_root_.a.b.A# => trait A - |_root_.a.b.A#a. => def a - |_root_.a.b.A. => object A - |""".stripMargin - ) - - check( - "class.java", - """ - |class B { - | public static void c() { } - | public int d() { } - | public class E {} - | public static class F {} - |} - """.stripMargin, - """ - |Language: - |Java - | - |Names: - |[7..8): B <= _root_.B. - |[7..8): B <= _root_.B# - |[18..19): c <= _root_.B.c. - |[53..54): d <= _root_.B#d. - |[76..77): E <= _root_.B#E. - |[76..77): E <= _root_.B#E# - |[103..104): F <= _root_.B.F. - |[103..104): F <= _root_.B.F# - | - |Symbols: - |_root_.B# => class B - |_root_.B#E# => class E - |_root_.B#E. => object E - |_root_.B#d. => def d - |_root_.B. => object B - |_root_.B.F# => class F - |_root_.B.F. => object F - |_root_.B.c. => def c - """.stripMargin - ) - - check( - "enum.java", - """ - |enum G { - | H, - | I - |} - """.stripMargin, - """ - | - |Language: - |Java - | - |Names: - |[6..7): G <= _root_.G. - |[12..13): H <= _root_.G.H. - |[12..13): H <= _root_.G.H. - |[17..18): I <= _root_.G.I. - |[17..18): I <= _root_.G.I. - | - |Symbols: - |_root_.G. => object G - |_root_.G.H. => val H - |_root_.G.H. => val H - |_root_.G.I. => val I - |_root_.G.I. => val I - |""".stripMargin - ) - - check( - "field.java", - """ - |public class J { - | public static final int FIELD = 1; - |} - """.stripMargin, - """ - |Language: - |Java - | - |Names: - |[14..15): J <= _root_.J. - |[14..15): J <= _root_.J# - |[46..51): FIELD <= _root_.J.FIELD. - | - |Symbols: - |_root_.J# => class J - |_root_.J. => object J - |_root_.J.FIELD. => val FIELD - """.stripMargin - ) - -// I came across this example here -// {{{ -// public interface Extension { -// Set EMPTY_SET = new HashSet(); -// } -// }}} -// from Flexmark where EMPTY_SET is static but doesn't have isStatic = true. -// JavaMtags currently marks it as Extension#EMPTY_SET but scalac sees it as Extension.EMPTY_SET - checkIgnore( - "default.java", - """package k; - |public interface K { - | L l = new L; - |} - """.stripMargin, - """ - |Language: - |Java - | - |Names: - |[8..9): k => _root_.k. - |[28..29): K <= _root_.k.K. - |[28..29): K <= _root_.k.K# - |[36..37): l <= _root_.k.K.l. - | - |Symbols: - |_root_.k. => package k - |_root_.k.K# => trait K - |_root_.k.K#m. => def m - |_root_.k.K. => object K - """.stripMargin - ) - - test("index a few sources from the JDK") { - val jdk = CompilerConfig.jdkSources.get - val DefaultFileSystem = - Paths.get("java").resolve("io").resolve("DefaultFileSystem.java") - val db = Mtags.indexDatabase(jdk :: Nil, shouldIndex = { path => - path.toNIO.endsWith(DefaultFileSystem) - }) - - val obtained = db - .toDb(None) - .syntax - .replaceFirst("jar:file://.+!", "jar:file://JAVA_HOME!") - .replaceAll("\\[\\d+\\.\\.\\d+\\)", "[)") - .replaceAll("-+", "------------------") // consistent across machines. - - val expected = - """jar:file://JAVA_HOME!/java/io/DefaultFileSystem.java - |------------------ - |Language: - |Java - | - |Names: - |[): java => _root_.java. - |[): io => _root_.java.io. - |[): DefaultFileSystem <= _root_.java.io.DefaultFileSystem. - |[): DefaultFileSystem <= _root_.java.io.DefaultFileSystem# - |[): getFileSystem <= _root_.java.io.DefaultFileSystem.getFileSystem. - | - |Symbols: - |_root_.java. => package java - |_root_.java.io. => package io - |_root_.java.io.DefaultFileSystem# => class DefaultFileSystem - |_root_.java.io.DefaultFileSystem. => object DefaultFileSystem - |_root_.java.io.DefaultFileSystem.getFileSystem. => def getFileSystem - """.stripMargin - assertNoDiff(obtained, expected) - } - - test("check issue #280 case") { - val jdk = CompilerConfig.jdkSources.get - val parserConstants = - Paths - .get("com") - .resolve("sun") - .resolve("jmx") - .resolve("snmp") - .resolve("IPAcl") - .resolve("ParserConstants.java") - val db = Mtags.indexDatabase(jdk :: Nil, shouldIndex = { path => - path.toNIO.endsWith(parserConstants) - }) - - db.toDb(None).syntax - } - - // Ignored because it's slow - ignore("index JDK") { - val db = Mtags.indexDatabase(CompilerConfig.jdkSources.get :: Nil) -// pprint.log(db.documents.length) - } -} diff --git a/metals/src/test/scala/tests/mtags/ScalaMtagsTest.scala b/metals/src/test/scala/tests/mtags/ScalaMtagsTest.scala deleted file mode 100644 index b021389be94..00000000000 --- a/metals/src/test/scala/tests/mtags/ScalaMtagsTest.scala +++ /dev/null @@ -1,158 +0,0 @@ -package tests.mtags - -object ScalaMtagsTest extends BaseMtagsTest { - check( - "vanilla.scala", - """ - |package a.b.c - |object D { - | def e = { def x = 3; x } - | val f = 2 - | var g = 2 - | class H { def x = 3 } - | trait I { - | def x: Int - | val y: Int - | var z: Int - | } - | object J { def k = 2 } - |} - """.stripMargin, - """ - |Language: - |Scala212 - | - |Names: - |[22..23): D <= _root_.a.b.c.D. - |[33..34): e <= _root_.a.b.c.D.e. - |[61..62): f <= _root_.a.b.c.D.f. - |[74..75): g <= _root_.a.b.c.D.g. - |[89..90): H <= _root_.a.b.c.D.H# - |[97..98): x <= _root_.a.b.c.D.H#x. - |[114..115): I <= _root_.a.b.c.D.I# - |[127..128): x <= _root_.a.b.c.D.I#x. - |[143..144): y <= _root_.a.b.c.D.I#y. - |[159..160): z <= _root_.a.b.c.D.I#z. - |[181..182): J <= _root_.a.b.c.D.J. - |[189..190): k <= _root_.a.b.c.D.J.k. - | - |Symbols: - |_root_.a.b.c.D. => object D - |_root_.a.b.c.D.H# => class H - |_root_.a.b.c.D.H#x. => def x - |_root_.a.b.c.D.I# => trait I - |_root_.a.b.c.D.I#x. => def x - |_root_.a.b.c.D.I#y. => val y - |_root_.a.b.c.D.I#z. => var z - |_root_.a.b.c.D.J. => object J - |_root_.a.b.c.D.J.k. => def k - |_root_.a.b.c.D.e. => def e - |_root_.a.b.c.D.f. => val f - |_root_.a.b.c.D.g. => var g - """.stripMargin - ) - - check( - "pkgobject.scala", - """ - |package object K { - | def l = 2 - |} - """.stripMargin, - """ - |Language: - |Scala212 - | - |Names: - |[16..17): K <= _root_.K. - |[16..17): K <= _root_.K.package. - |[26..27): l <= _root_.K.package.l. - | - |Symbols: - |_root_.K. => packageobject K - |_root_.K.package. => object package - |_root_.K.package.l. => def l - """.stripMargin - ) - - check( - "pats.scala", - """ - |object pats { - | val o, p = 2 - | val q, r: Int - | var s, t = 2 - | var v, w: Int - |} - """.stripMargin, - """ - |Language: - |Scala212 - | - |Names: - |[8..12): pats <= _root_.pats. - |[21..22): o <= _root_.pats.o. - |[24..25): p <= _root_.pats.p. - |[36..37): q <= _root_.pats.q. - |[39..40): r <= _root_.pats.r. - |[52..53): s <= _root_.pats.s. - |[55..56): t <= _root_.pats.t. - |[67..68): v <= _root_.pats.v. - |[70..71): w <= _root_.pats.w. - | - |Symbols: - |_root_.pats. => object pats - |_root_.pats.o. => val o - |_root_.pats.p. => val p - |_root_.pats.q. => val q - |_root_.pats.r. => val r - |_root_.pats.s. => var s - |_root_.pats.t. => var t - |_root_.pats.v. => var v - |_root_.pats.w. => var w - """.stripMargin - ) - - check( - "type.scala", - """ - |trait Tpe { - | type M - | type N = F - |} - """.stripMargin, - """ - |Language: - |Scala212 - | - |Names: - |[7..10): Tpe <= _root_.Tpe# - |[20..21): M <= _root_.Tpe#M# - |[29..30): N <= _root_.Tpe#N# - | - |Symbols: - |_root_.Tpe# => trait Tpe - |_root_.Tpe#M# => type M - |_root_.Tpe#N# => type N - """.stripMargin - ) - - check( - "class-field.scala", - "case class A(a: Int, b: String)", - """ - |Language: - |Scala212 - | - |Names: - |[11..12): A <= _root_.A# - |[13..14): a <= _root_.A#(a) - |[21..22): b <= _root_.A#(b) - | - |Symbols: - |_root_.A# => class A - |_root_.A#(a) => val param a - |_root_.A#(b) => val param b - |""".stripMargin - ) -} diff --git a/metals/src/test/scala/tests/refactoring/BacktickTest.scala b/metals/src/test/scala/tests/refactoring/BacktickTest.scala deleted file mode 100644 index 476179e050b..00000000000 --- a/metals/src/test/scala/tests/refactoring/BacktickTest.scala +++ /dev/null @@ -1,35 +0,0 @@ -package tests.refactoring - -import scala.meta.metals.refactoring.Backtick -import tests.MegaSuite - -object BacktickTest extends MegaSuite { - - def checkOK(identifier: String, expected: String): Unit = { - test(s"OK $identifier") { - Backtick.backtickWrap(identifier) match { - case Right(obtained) => - assertNoDiff(obtained, expected) - case Left(err) => fail(err) - } - } - } - - def checkFail(identifier: String): Unit = { - test(s"FAIL $identifier") { - Backtick.backtickWrap(identifier) match { - case Right(obtained) => fail(s"Expected error, obtained: $obtained") - case Left(_) => // OK - } - } - } - - checkFail("`") - checkFail("`a ``") - checkOK("a b", "`a b`") - checkOK("++", "++") - checkOK("foo_", "foo_") - checkOK("foo_ a", "`foo_ a`") - checkOK("a.b", "`a.b`") - -} diff --git a/metals/src/test/scala/tests/search/SymbolIndexTest.scala b/metals/src/test/scala/tests/search/SymbolIndexTest.scala deleted file mode 100644 index 5482c5d52b2..00000000000 --- a/metals/src/test/scala/tests/search/SymbolIndexTest.scala +++ /dev/null @@ -1,280 +0,0 @@ -package tests.search - -import java.io.PipedOutputStream -import java.nio.file.Files -import java.nio.file.Paths -import scala.meta.metals.MSchedulers -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.MetalsServices -import scala.meta.metals.Uri -import scala.meta.metals.internal.BuildInfo -import scala.meta.{lsp => l} -import scala.meta.lsp.ClientCapabilities -import scala.meta.lsp.InitializeParams -import scala.meta.lsp.Location -import scala.meta.lsp.Position -import scala.meta.lsp.Range -import scala.meta.metals.search.InMemorySymbolIndex -import scala.meta.metals.search.InverseSymbolIndexer -import scala.meta.metals.search.SymbolIndex -import scala.{meta => m} -import monix.execution.schedulers.TestScheduler -import org.langmeta.io.AbsolutePath -import org.langmeta.io.Classpath -import scala.meta.jsonrpc.LanguageClient -import org.langmeta.semanticdb.Symbol -import tests.MegaSuite -import utest._ - -object SymbolIndexTest extends MegaSuite { - implicit val cwd: AbsolutePath = - AbsolutePath(BuildInfo.testWorkspaceBaseDirectory) - object path { - val User = cwd - .resolve("src") - .resolve("main") - .resolve("scala") - .resolve("example") - .resolve("User.scala") - val UserUri = Uri(User) - val UserReferenceLine = 3 - - val UserTest = cwd - .resolve("src") - .resolve("test") - .resolve("scala") - .resolve("example") - .resolve("UserTest.scala") - val UserTestUri = Uri(UserTest) - } - Predef.assert( - Files.isRegularFile(path.User.toNIO), - path.User.toString() - ) - Predef.assert( - Files.isRegularFile(path.UserTest.toNIO), - path.UserTest.toString() - ) - val s = TestScheduler() - val mscheduler = new MSchedulers(s, s, s) - val stdout = new PipedOutputStream() - // TODO(olafur) run this as part of utest.runner.Framework.setup() - val client = new LanguageClient(stdout, scribe.Logger.root) - val metals = new MetalsServices(cwd, client, mscheduler) - metals - .initialize( - InitializeParams(Some(0L), cwd.toString(), ClientCapabilities()) - ) - .runAsync(s) - while (s.tickOne()) () // Trigger indexing - val index: SymbolIndex = metals.symbolIndex - val reminderMsg = "Did you run metalsSetup from sbt?" - override val tests = Tests { - - /** Checks that there is a symbol at given position, it's in the index and has expected name */ - def assertSymbolFound(line: Int, column: Int)( - expected: String - ): Symbol = { - val (symbol, _) = index - .findSymbol(path.UserTestUri, line, column) - .getOrElse( - fail( - s"Symbol not found at $path.UserTest:$line:$column. ${reminderMsg}" - ) - ) - assertNoDiff(symbol.syntax, expected) - val symbolData = index - .referencesData(symbol) - .headOption - .getOrElse( - fail(s"Symbol ${symbol} is not found in the index. ${reminderMsg}") - ) - assertNoDiff(symbolData.symbol, expected) - symbol - } - - /** Checks that given symbol has a definition with expected name */ - def assertSymbolDefinition(line: Int, column: Int)( - expectedSymbol: String, - expectedDefn: String - ): Unit = { - val symbol = assertSymbolFound(line, column)(expectedSymbol) - val data = index - .definitionData(symbol) - .getOrElse( - fail(s"Definition not found for term ${symbol}") - ) - assertNoDiff(data.symbol, expectedDefn) - } - - /** Checks that given symbol has a definition with expected name */ - def assertSymbolReferences( - line: Int, - column: Int, - withDefinition: Boolean - )( - expected: Location* - ): Unit = { - val (symbol, _) = index - .findSymbol(path.UserTestUri, line, column) - .getOrElse( - fail( - s"Symbol not found at $path.UserTest:$line:$column. ${reminderMsg}" - ) - ) - val dataList = index.referencesData(symbol) - if (dataList.isEmpty) fail(s"References not found for term ${symbol}") - // TODO: use `dataList` to test expected alternatives - val found = for { - data <- dataList - pos <- data.referencePositions(withDefinition) - } yield pos.toLocation - - val missingLocations = found.toSet diff expected.toSet - assert(missingLocations.isEmpty) - val unexpectedLocations = expected.toSet diff found.toSet - assert(unexpectedLocations.isEmpty) - } - - def ref( - path: AbsolutePath, - start: (Int, Int), - end: (Int, Int) - ): Location = - l.Location( - path.toURI.toString, - Range( - Position(start._1, start._2), - l.Position(end._1, end._2) - ) - ) - - "definition" - { - "<>(...)" - - assertSymbolDefinition(path.UserReferenceLine, 17)( - "_root_.a.User.", - "_root_.a.User#" - ) - "User.<>(...)" - - assertSymbolDefinition(3, 22)( - "_root_.a.User.apply(Ljava/lang/String;I)La/User;.", - "_root_.a.User#" - ) - "User.<>(...)" - - assertSymbolDefinition(4, 9)( - "_root_.a.User#copy(Ljava/lang/String;I)La/User;.", - "_root_.a.User#" - ) - "User.apply(<> ...)" - - assertSymbolDefinition(3, 28)( - "_root_.a.User.apply(Ljava/lang/String;I)La/User;.(name)", - "_root_.a.User#(name)" - ) - "user.copy(<> = ...)" - - assertSymbolDefinition(4, 14)( - "_root_.a.User#copy(Ljava/lang/String;I)La/User;.(age)", - "_root_.a.User#(age)" - ) - } - - "classpath" - { - "<>(...)" - // ScalaMtags - assertSymbolFound(5, 5)("_root_.scala.collection.immutable.List.") - "<>.create(...)" - // JavaMtags - assertSymbolFound(8, 19)("_root_.scala.runtime.CharRef.") - } - - "references" - { - "<>(...)" - - assertSymbolReferences(3, 17, withDefinition = true)( - ref(path.User, (2, 11), (2, 15)), - ref(path.UserTest, (3, 15), (3, 19)) - ) - "<>" - - assertSymbolReferences(5, 5, withDefinition = false)( - ref(path.User, (6, 10), (6, 14)), - ref(path.UserTest, (5, 2), (5, 6)) - ) - "a.a.<>" - - assertSymbolReferences(9, 28, withDefinition = true)( - ref(path.User, (5, 6), (5, 7)), - ref(path.User, (6, 18), (6, 19)), - ref(path.UserTest, (9, 28), (9, 29)) - ) - "User.apply(<> ...)" - - assertSymbolReferences(3, 27, withDefinition = false)( - // ref(path.User, (2,16), (2,20)), // definition - ref(path.UserTest, (3, 26), (3, 30)), - ref(path.UserTest, (9, 17), (9, 21)) - ) - } - - "workspace" - { - def checkQuery( - expected: String* - )(implicit path: utest.framework.TestPath): Unit = { - while (s.tickOne()) () - val result = metals.symbolIndex.workspaceSymbols(path.value.last) - val obtained = result.toIterator.map(_.name).mkString("\n") - assertNoDiff(obtained, expected.mkString("\n")) - } - "EmptyResult" - checkQuery() - "User" - checkQuery("User", "UserTest") - "Test" - checkQuery("UserTest") - } - - "bijection" - { - val target = cwd.resolve("target").resolve("scala-2.12") - val originalDatabase = { - val complete = m.Database.load( - Classpath( - target.resolve("classes") :: - target.resolve("test-classes") :: - Nil - ) - ) - val slimDocuments = complete.documents.map { d => - d.copy(messages = Nil, synthetics = Nil, symbols = Nil) - } - m.Database(slimDocuments) - } - index match { - case index: InMemorySymbolIndex => - val reconstructedDatabase = InverseSymbolIndexer.reconstructDatabase( - cwd, - index.documentIndex, - index.symbolIndexer.allSymbols - ) - val filenames = reconstructedDatabase.documents.toIterator.map { d => - Paths.get(d.input.syntax).getFileName.toString - }.toList - assert(filenames.nonEmpty) - assert( - filenames == List( - "User.scala", - "UserTest.scala" - ) - ) - assertNoDiff(reconstructedDatabase.syntax, originalDatabase.syntax) - case _ => - fail(s"Unsupported index ${index.getClass}") - } - } - - "edit-distance" - { - val user = path.UserTestUri.toInput(metals.buffers) - val newUser = user.copy(value = "// leading comment\n" + user.value) - metals.buffers.changed(newUser) - assertSymbolDefinition(path.UserReferenceLine + 1, 17)( - "_root_.a.User.", - "_root_.a.User#" - ) - } - } - - override def utestAfterAll(): Unit = { - println("Shutting down server...") - metals.shutdown() - while (s.tickOne()) () - } -} diff --git a/metals/src/test/scala/tests/search/TokenEditDistanceTest.scala b/metals/src/test/scala/tests/search/TokenEditDistanceTest.scala deleted file mode 100644 index b2424138cee..00000000000 --- a/metals/src/test/scala/tests/search/TokenEditDistanceTest.scala +++ /dev/null @@ -1,121 +0,0 @@ -package tests.search - -import scala.meta.metals.ScalametaEnrichments._ -import scala.meta.metals.search.TokenEditDistance -import org.langmeta.languageserver.InputEnrichments._ -import tests.MegaSuite - -object TokenEditDistanceTest extends MegaSuite { - def check( - name: String, - revised: String, - original: String, - expected: String, - keyword: String = "List" - ): Unit = { - test(name) { - val edit = TokenEditDistance(original, revised).get - val offset = revised.indexOf(keyword) - val obtained = edit - .toOriginal(offset) - .map { pos => - val Right(reverse) = edit.toRevised(pos.start) - assert(reverse.contains(offset)) - s"""|${pos.lineContent} - |${pos.caret}""".stripMargin - } - .getOrElse("") - assertNoDiff(obtained, expected) - } - } - - check( - "insert", - revised = """ - |object a { - | "msg".substrin - | List(1) - |}""".stripMargin, - original = """ - |object a { - | List(1) // <-- - |} - """.stripMargin, - expected = """ - | List(1) // <-- - | ^ - |""".stripMargin, - ) - - check( - "change", - revised = """ - |object a { - | "msg".substrin - | List(1) - |}""".stripMargin, - original = """ - |object a { - | this.changed() - | List(1) // <-- - |} - """.stripMargin, - expected = """ - | List(1) // <-- - | ^ - |""".stripMargin, - ) - - check( - "delete", - revised = """ - |object a { - | List(1) - |}""".stripMargin, - original = """ - |object a { - | remove(this) - | List(1) // <-- - |} - """.stripMargin, - expected = """ - | List(1) // <-- - | ^ - |""".stripMargin, - ) - - check( - "none", - revised = """ - |object a { - | List(1) - |}""".stripMargin, - original = """ - |object a { - | Something() - |} - """.stripMargin, - expected = "", - ) - - check( - "moved", - revised = """ - |object a { - | def foo = { - | println(1) - | } - | List(1) - |}""".stripMargin, - original = """ - |object a { - | List(1) - | def foo = { - | Fuz(1) - | } - |} - """.stripMargin, - expected = "", - ) - -} diff --git a/metals/src/test/scala/tests/storage/LevelDBMapTest.scala b/metals/src/test/scala/tests/storage/LevelDBMapTest.scala deleted file mode 100644 index 1e8287d2b99..00000000000 --- a/metals/src/test/scala/tests/storage/LevelDBMapTest.scala +++ /dev/null @@ -1,58 +0,0 @@ -package tests.storage - -import java.nio.file.Files -import scala.meta.metals.storage.FromBytes -import scala.meta.metals.storage.LevelDBMap -import scala.meta.metals.storage.ToBytes -import tests.MegaSuite - -object LevelDBMapTest extends MegaSuite { - val tmp = Files.createTempDirectory("metals").toFile - tmp.deleteOnExit() - val db = LevelDBMap.createDBThatIPromiseToClose(tmp) - val map = LevelDBMap(db) - - test("get/put") { - map.put("key", "value") - assert(map.get[String, String]("key").contains("value")) - assert(map.get[String, String]("blah").isEmpty) - } - - test("mapValues") { - case class User(name: String) - object User { - implicit val UserToBytes: ToBytes[User] = - ToBytes.StringToBytes.contramap[User](_.name) - implicit val UserFromBytes: FromBytes[User] = - FromBytes.StringFromBytes.map[User](User.apply) - } - map.put[String, User]("John", User("John")) - assert(map.get[String, User]("John").contains(User("John"))) - assert(map.get[String, User]("Susan").isEmpty) - } - - test("mapKeys") { - implicit val IntToBytes: ToBytes[Int] = _.toString.getBytes - map.put(1, "2") - assert(map.get[Int, String](1).contains("2")) - assert(map.get[Int, String](2).isEmpty) - } - - test("getOrElseUpdate") { - var count = 0 - val obtained = - map.getOrElseUpdate[String, String]("unknown", () => { - count += 1; count.toString - }) - assert(obtained == "1") - val obtained2 = - map.getOrElseUpdate[String, String]("unknown", () => { - count += 1; count.toString - }) - assert(obtained2 == "1") - } - - override def utestAfterAll(): Unit = { - db.close() - } -} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ClasspathLoader.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ClasspathLoader.scala new file mode 100644 index 00000000000..b0c2c143dee --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ClasspathLoader.scala @@ -0,0 +1,32 @@ +package scala.meta.internal.mtags + +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath +import scala.meta.io.RelativePath + +/** + * Utility to load relative paths from a classpath. + * + * Provides similar functionality as URLClassLoader but uses idiomatic + * Scala data structures like `AbsolutePath` and `Option[T]` instead of + * `java.net.URL` and nulls. + */ +final class ClasspathLoader(classpath: Classpath) { + private val loader = new OpenClassLoader + classpath.entries.foreach(loader.addEntry) + override def toString: String = loader.getURLs.toList.toString() + + def addEntry(entry: AbsolutePath): Unit = { + loader.addEntry(entry) + } + + /** Load a resource from the classpath. */ + def load(path: RelativePath): Option[AbsolutePath] = { + loader.resolve(path) + } + + /** Load a resource from the classpath. */ + def load(path: String): Option[AbsolutePath] = { + loader.resolve(path) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/DefinitionAlternatives.scala b/mtags/src/main/scala/scala/meta/internal/mtags/DefinitionAlternatives.scala new file mode 100644 index 00000000000..af711f73175 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/DefinitionAlternatives.scala @@ -0,0 +1,90 @@ +package scala.meta.internal.mtags + +import scala.meta.internal.semanticdb.Scala._ + +object DefinitionAlternatives { + + /** Returns a list of fallback symbols that can act instead of given symbol. */ + def apply(symbol: Symbol): List[Symbol] = { + List( + caseClassCompanionToType(symbol), + caseClassApplyOrCopy(symbol), + caseClassApplyOrCopyParams(symbol), + methodOwner(symbol), + ).flatten + } + + private object GlobalSymbol { + def apply(owner: Symbol, desc: Descriptor): Symbol = + Symbol(Symbols.Global(owner.value, desc)) + def unapply(sym: Symbol): Option[(Symbol, Descriptor)] = + Some(sym.owner -> sym.value.desc) + } + + /** If `case class A(a: Int)` and there is no companion object, resolve + * `A` in `A(1)` to the class definition. + */ + private def caseClassCompanionToType(symbol: Symbol): Option[Symbol] = + Option(symbol).collect { + case GlobalSymbol(owner, Descriptor.Term(name)) => + GlobalSymbol(owner, Descriptor.Type(name)) + } + + /** If `case class Foo(a: Int)`, then resolve + * `a` in `Foo.apply(a = 1)`, and + * `a` in `Foo(1).copy(a = 2)` + * to the `Foo.a` primary constructor definition. + */ + private def caseClassApplyOrCopyParams(symbol: Symbol): Option[Symbol] = + Option(symbol).collect { + case GlobalSymbol( + GlobalSymbol( + GlobalSymbol(owner, signature), + Descriptor.Method("copy" | "apply", _) + ), + Descriptor.Parameter(param) + ) => + GlobalSymbol( + GlobalSymbol(owner, Descriptor.Type(signature.name.value)), + Descriptor.Term(param) + ) + } + + /** If `case class Foo(a: Int)`, then resolve + * `apply` in `Foo.apply(1)`, and + * `copy` in `Foo(1).copy(a = 2)` + * to the `Foo` class definition. + */ + private def caseClassApplyOrCopy(symbol: Symbol): Option[Symbol] = + Option(symbol).collect { + case GlobalSymbol( + GlobalSymbol(owner, signature), + Descriptor.Method("apply" | "copy", _) + ) => + GlobalSymbol(owner, Descriptor.Type(signature.name.value)) + } + + /** + * For methods and vals, fall back to the enclosing class + * + * This fallback is desirable for cases like + * - macro annotation generated members + * - `java/lang/Object#==` and friends + * + * The general idea is that we want goto definition to jump somewhere close to + * the definition if we can't jump to the exact symbol. The risk of false + * positives is low because if we jump with this fallback method we jump at least + * to the source file where that symbol is defined. We can't jump to a totally + * unrelated source file. + */ + private def methodOwner(symbol: Symbol): Option[Symbol] = + Option(symbol).flatMap { + case GlobalSymbol(owner, _: Descriptor.Method | _: Descriptor.Term) => + Some(owner) + case GlobalSymbol(owner, _: Descriptor.Parameter) => + methodOwner(owner) + case _ => + None + } + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Enrichments.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Enrichments.scala new file mode 100644 index 00000000000..e3cae96c1c2 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Enrichments.scala @@ -0,0 +1,86 @@ +package scala.meta.internal.mtags + +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import scala.meta.internal.io.FileIO +import scala.meta.internal.semanticdb.Language +import scala.meta.io.AbsolutePath +import scala.meta.internal.{semanticdb => s} + +object Enrichments { + implicit class XtensionRange(range: s.Range) { + def encloses(other: s.Range): Boolean = { + range.startLine <= other.startLine && + range.endLine >= other.startLine && + range.startCharacter <= other.startCharacter && + range.endCharacter > other.startCharacter // end character is non-inclusive + } + } + private def filenameToLanguage(filename: String): Language = { + if (filename.endsWith(".java")) Language.JAVA + else if (filename.endsWith(".scala")) Language.SCALA + else Language.UNKNOWN_LANGUAGE + } + implicit class XtensionPathMetals(file: Path) { + def toLanguage: Language = { + filenameToLanguage(file.getFileName.toString) + } + } + implicit class XtensionAbsolutePathMetals(file: AbsolutePath) { + def toLanguage: Language = { + file.toNIO.toLanguage + } + def toInput: Input.VirtualFile = { + val text = FileIO.slurp(file, StandardCharsets.UTF_8) + val path = file.toString() + val input = Input.VirtualFile(path, text) + input + } + } + + implicit class XtensionInputOffset(input: Input) { + def toLanguage: Language = input match { + case Input.VirtualFile(path, _) => + filenameToLanguage(path) + case _ => + Language.UNKNOWN_LANGUAGE + } + + /** Returns offset position with end == start == offset */ + def toOffsetPosition(offset: Int): Position = + Position.Range(input, offset, offset) + + /** Returns an offset for this input */ + def toOffset(line: Int, column: Int): Int = + input.lineToOffset(line) + column + + /** Returns an offset position for this input */ + def toPosition(startLine: Int, startColumn: Int): Position.Range = + toPosition(startLine, startColumn, startLine, startColumn) + + def toPosition(occ: s.SymbolOccurrence): Position.Range = { + val range = occ.range.getOrElse(s.Range()) + toPosition( + range.startLine, + range.startCharacter, + range.endLine, + range.endCharacter + ) + } + + /** Returns a range position for this input */ + def toPosition( + startLine: Int, + startColumn: Int, + endLine: Int, + endColumn: Int + ): Position.Range = + Position.Range( + input, + toOffset(startLine, startColumn), + toOffset(endLine, endColumn) + ) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala b/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala new file mode 100644 index 00000000000..24db38c0baa --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/GlobalSymbolIndex.scala @@ -0,0 +1,91 @@ +package scala.meta.internal.mtags + +import scala.meta.io.AbsolutePath + +/** + * An index to lookup the definition of global symbols. + * + * Only indexes plain Scala and Java source files, no compilation needed. + */ +trait GlobalSymbolIndex { + + /** + * Lookup the definition of a global symbol. + * + * Returns the path of the file that defines the symbol but does not include + * the exact position of the definition. Computing the range position of the + * definition is not handled by this method, it is left for the user and can + * be done using the mtags module. + * + * @param symbol a global SemanticDB symbol. For comprehensive documentation + * of how a symbol is formatted consule the specification: + * https://scalameta.org/docs/semanticdb/specification.html#symbol + * + * Examples: {{{ + * "scala/Option#" // Option class + * "scala/Option." // Option companion object + * "scala/Option#.get()." // `get` method for Option + * "scala/Predef.String#" // String type alias in Predef + * "java/lang/String#format(+1)." // Static `format` method for strings + * "java/util/Map#Entry#" // Static inner interface. + * }}} + * @return the definition of the symbol, if any. + */ + def definition(symbol: Symbol): Option[SymbolDefinition] + + /** + * Add an individual Java or Scala source file to the index. + * + * @param file the absolute path to the source file, can be a path + * on disk or inside of a jar/zip file. + * @param sourceDirectory the enclosing project source directory if + * file is on disk, used to relativize `file`. + * Can be None if file is inside a zip file + * assuming the file path is already relativized + * by that point. + * @throws Exception in case of problems processing the source file + * such as tokenization failure due to an unclosed + * literal. + */ + def addSourceFile( + file: AbsolutePath, + sourceDirectory: Option[AbsolutePath] + ): Unit + + /** + * Index a jar or zip file containing Scala and Java source files. + * + * Published artifacts typically have accompanying sources.jar files that + * include the project sources. + * + * Sources of a published library can be fetched from the command-line with + * coursier using the --sources flag: + * {{{ + * $ coursier fetch com.thoughtworks.qdox:qdox:2.0-M9 --sources + * $HOME/.coursier/cache/v1/https/repo1.maven.org/maven2/com/thoughtworks/qdox/qdox/2.0-M9/qdox-2.0-M9-sources.jar.coursier/cache/v1/https/repo1.maven.org/maven2/com/thoughtworks/qdox/qdox/2.0-M9/qdox-2.0-M9-sources.jar + * }}} + * + * Sources can be fetched from an sbt build through the `updateClassifiers` task: + * {{{ + * val sourceJars = for { + * configurationReport <- updateClassifiers.in(input).value.configurations + * moduleReport <- configurationReport.modules + * (artifact, file) <- moduleReport.artifacts + * if artifact.classifier.contains("sources") + * } yield file + * }}} + * + * @param jar the path to a single jar or zip file. + * @throws Exception in case of problems processing the source file + * such as tokenization failure due to an unclosed + * literal. + */ + def addSourceJar(jar: AbsolutePath): Unit + +} + +case class SymbolDefinition( + querySymbol: Symbol, + definitionSymbol: Symbol, + path: AbsolutePath +) diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/InverseLegacyToken.scala b/mtags/src/main/scala/scala/meta/internal/mtags/InverseLegacyToken.scala new file mode 100644 index 00000000000..41574734361 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/InverseLegacyToken.scala @@ -0,0 +1,103 @@ +package scala.meta.internal.mtags + +import scala.meta.internal.tokenizers.LegacyToken._ + +/** Utility to print helpful messages for parse errors. */ +object InverseLegacyToken { + val category: Map[Int, String] = Map[Int, String]( + EMPTY -> "EMPTY", + UNDEF -> "UNDEF", + ERROR -> "ERROR", + EOF -> "EOF", + /** literals */ + CHARLIT -> "CHARLIT", + INTLIT -> "INTLIT", + LONGLIT -> "LONGLIT", + FLOATLIT -> "FLOATLIT", + DOUBLELIT -> "DOUBLELIT", + STRINGLIT -> "STRINGLIT", + STRINGPART -> "STRINGPART", + SYMBOLLIT -> "SYMBOLLIT", + INTERPOLATIONID -> "INTERPOLATIONID", + XMLLIT -> "XMLLIT", + XMLLITEND -> "XMLLITEND", + /** identifiers */ + IDENTIFIER -> "IDENTIFIER", + BACKQUOTED_IDENT -> "BACKQUOTED_IDENT", + /** keywords */ + NEW -> "NEW", + THIS -> "THIS", + SUPER -> "SUPER", + NULL -> "NULL", + TRUE -> "TRUE", + FALSE -> "FALSE", + /** modifiers */ + IMPLICIT -> "IMPLICIT", + OVERRIDE -> "OVERRIDE", + PROTECTED -> "PROTECTED", + PRIVATE -> "PRIVATE", + ABSTRACT -> "ABSTRACT", + FINAL -> "FINAL", + SEALED -> "SEALED", + LAZY -> "LAZY", + MACRO -> "MACRO", + /** templates */ + PACKAGE -> "PACKAGE", + IMPORT -> "IMPORT", + CLASS -> "CLASS", + CASECLASS -> "CASECLASS", + OBJECT -> "OBJECT", + CASEOBJECT -> "CASEOBJECT", + TRAIT -> "TRAIT", + EXTENDS -> "EXTENDS", + WITH -> "WITH", + TYPE -> "TYPE", + FORSOME -> "FORSOME", + DEF -> "DEF", + VAL -> "VAL", + VAR -> "VAR", + ENUM -> "ENUM", + /** control structures */ + IF -> "IF", + THEN -> "THEN", + ELSE -> "ELSE", + WHILE -> "WHILE", + DO -> "DO", + FOR -> "FOR", + YIELD -> "YIELD", + THROW -> "THROW", + TRY -> "TRY", + CATCH -> "CATCH", + FINALLY -> "FINALLY", + CASE -> "CASE", + RETURN -> "RETURN", + MATCH -> "MATCH", + /** parenthesis */ + LPAREN -> "LPAREN", + RPAREN -> "RPAREN", + LBRACKET -> "LBRACKET", + RBRACKET -> "RBRACKET", + LBRACE -> "LBRACE", + RBRACE -> "RBRACE", + /** special symbols */ + COMMA -> "COMMA", + SEMI -> "SEMI", + DOT -> "DOT", + COLON -> "COLON", + EQUALS -> "EQUALS", + AT -> "AT", + /** special symbols */ + HASH -> "HASH", + USCORE -> "USCORE", + ARROW -> "ARROW", + LARROW -> "LARROW", + SUBTYPE -> "SUBTYPE", + SUPERTYPE -> "SUPERTYPE", + VIEWBOUND -> "VIEWBOUND", + WHITESPACE -> "WHITESPACE", + COMMENT -> "COMMENT", + UNQUOTE -> "UNQUOTE", + ELLIPSIS -> "ELLIPSIS", + ) + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala new file mode 100644 index 00000000000..444d3d362e1 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/JavaMtags.scala @@ -0,0 +1,149 @@ +package scala.meta.internal.mtags + +import com.thoughtworks.qdox._ +import com.thoughtworks.qdox.model.JavaClass +import com.thoughtworks.qdox.model.JavaField +import com.thoughtworks.qdox.model.JavaMember +import com.thoughtworks.qdox.model.JavaMethod +import com.thoughtworks.qdox.model.JavaModel +import com.thoughtworks.qdox.parser.ParseException +import java.io.StringReader +import java.util.Comparator +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.semanticdb.SymbolInformation.Kind +import scala.meta.internal.semanticdb.SymbolInformation.Property +import scala.meta.internal.mtags.Enrichments._ + +object JavaMtags { + def index(input: Input.VirtualFile): MtagsIndexer = { + val builder = new JavaProjectBuilder() + new MtagsIndexer { self => + override def language: Language = Language.JAVA + override def indexRoot(): Unit = { + try { + val source = builder.addSource(new StringReader(input.value)) + if (source.getPackage != null) { + source.getPackageName.split("\\.").foreach { p => + pkg( + p, + toRangePosition(source.getPackage.lineNumber, p) + ) + } + } + source.getClasses.forEach(visitClass) + } catch { + case _: ParseException | _: NullPointerException => + // Parse errors are ignored because the Java source files we process + // are not written by the user so there is nothing they can do about it. + } + } + + /** Computes the start/end offsets from a name in a line number. + * + * Applies a simple heuristic to find the name: the first occurence of + * name in that line. If the name does not appear in the line then + * 0 is returned. If the name appears for example in the return type + * of a method then we get the position of the return type, not the + * end of the world. + */ + def toRangePosition(line: Int, name: String): Position = { + val offset = input.toOffset(line, 0) + val columnAndLength = { + val fromIndex = { + // HACK(olafur) avoid hitting on substrings of "package". + if (input.value.startsWith("package", offset)) "package".length + else offset + } + val idx = input.value.indexOf(" " + name, fromIndex) + if (idx == -1) (0, 0) + else (idx - offset + " ".length, name.length) + } + input.toPosition( + line, + columnAndLength._1, + line, + columnAndLength._1 + columnAndLength._2 + ) + } + + def visitMembers[T <: JavaMember](fields: java.util.List[T]): Unit = + if (fields == null) () + else fields.forEach(visitMember) + + def visitClasses(classes: java.util.List[JavaClass]): Unit = + if (classes == null) () + else classes.forEach(visitClass) + + def visitClass(cls: JavaClass): Unit = + withOwner(owner) { + val kind = if (cls.isInterface) Kind.INTERFACE else Kind.CLASS + val pos = toRangePosition(cls.lineNumber, cls.getName) + tpe( + cls.getName, + pos, + kind, + if (cls.isEnum) Property.ENUM.value else 0 + ) + visitClasses(cls.getNestedClasses) + visitMethods(cls) + visitConstructors(cls) + visitMembers(cls.getFields) + } + + def visitConstructors(cls: JavaClass): Unit = { + val overloads = new OverloadDisambiguator() + cls.getConstructors.forEach { ctor => + val name = cls.getName + val disambiguator = overloads.disambiguator(name) + val pos = toRangePosition(ctor.lineNumber, name) + withOwner() { + super.ctor(disambiguator, pos, 0) + } + } + } + def visitMethods(cls: JavaClass): Unit = { + val overloads = new OverloadDisambiguator() + val methods = cls.getMethods + methods.sort(new Comparator[JavaMethod] { + override def compare(o1: JavaMethod, o2: JavaMethod): Int = { + java.lang.Boolean.compare(o1.isStatic, o2.isStatic) + } + }) + methods.forEach { method => + val name = method.getName + val disambiguator = overloads.disambiguator(name) + val pos = toRangePosition(method.lineNumber, name) + withOwner() { + super.method(name, disambiguator, pos, 0) + } + } + } + def visitMember[T <: JavaMember](m: T): Unit = + withOwner(owner) { + val name = m.getName + val line = m match { + case c: JavaMethod => c.lineNumber + case c: JavaField => c.lineNumber + case _ => 0 + } + val pos = toRangePosition(line, name) + val kind: Kind = m match { + case _: JavaMethod => Kind.METHOD + case _: JavaField => Kind.FIELD + case c: JavaClass => + if (c.isInterface) Kind.INTERFACE + else Kind.CLASS + case _ => Kind.UNKNOWN_KIND + } + term(name, pos, kind, 0) + } + } + } + + private implicit class XtensionJavaModel(val m: JavaModel) extends AnyVal { + def lineNumber: Int = m.getLineNumber - 1 + } + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/MD5.scala b/mtags/src/main/scala/scala/meta/internal/mtags/MD5.scala new file mode 100644 index 00000000000..b6761203fc7 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/MD5.scala @@ -0,0 +1,28 @@ +package scala.meta.internal.mtags + +import java.nio.ByteBuffer +import java.nio.charset.StandardCharsets +import java.security.MessageDigest + +object MD5 { + def compute(string: String): String = { + compute(ByteBuffer.wrap(string.getBytes(StandardCharsets.UTF_8))) + } + def compute(buffer: ByteBuffer): String = { + val md = MessageDigest.getInstance("MD5") + md.update(buffer) + bytesToHex(md.digest()) + } + private val hexArray = "0123456789ABCDEF".toCharArray + private def bytesToHex(bytes: Array[Byte]): String = { + val hexChars = new Array[Char](bytes.length * 2) + var j = 0 + while (j < bytes.length) { + val v: Int = bytes(j) & 0xFF + hexChars(j * 2) = hexArray(v >>> 4) + hexChars(j * 2 + 1) = hexArray(v & 0x0F) + j += 1 + } + new String(hexChars) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala new file mode 100644 index 00000000000..c998fbca080 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Mtags.scala @@ -0,0 +1,78 @@ +package scala.meta.internal.mtags + +import scala.meta.inputs.Input +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.semanticdb.TextDocument +import scala.meta.internal.mtags.Enrichments._ + +class Mtags { + def totalLinesOfCode: Long = javaLines + scalaLines + def totalLinesOfScala: Long = scalaLines + def totalLinesOfJava: Long = javaLines + def toplevels(input: Input.VirtualFile): List[String] = { + val language = input.toLanguage + if (language.isJava) { + // NOTE(olafur): this is incorrect in the following cases: + // - the source file has multiple top-level classes, in which case we + // don't index the package private classes. + // - if the path is not relative to the source directory, in which case + // the produced symbol is incorrect. + val toplevelClass = input.path.stripPrefix("/").stripSuffix(".java") + "#" + List(toplevelClass) + } else if (language.isScala) { + addLines(language, input.text) + new ScalaToplevelMtags(input) + .index() + .occurrences + .iterator + .filterNot(_.symbol.isPackage) + .map(_.symbol) + .toList + } else { + Nil + } + } + def index(language: Language, input: Input.VirtualFile): TextDocument = { + addLines(language, input.text) + val result = + if (language.isJava) { + JavaMtags.index(input).index() + } else if (language.isScala) { + ScalaMtags.index(input).index() + } else { + TextDocument() + } + result + .withUri(input.path) + .withText(input.text) + } + private var javaLines: Long = 0L + private var scalaLines: Long = 0L + private def addLines(language: Language, text: String): Unit = { + if (language.isJava) { + javaLines += text.lines.length + } else if (language.isScala) { + scalaLines += text.lines.length + } + } +} +object Mtags { + def index(input: Input.VirtualFile): TextDocument = { + new Mtags().index(input.toLanguage, input) + } + + def toplevels(document: TextDocument): List[String] = { + document.occurrences.iterator + .filter { occ => + occ.role.isDefinition && + Symbol(occ.symbol).isToplevel + } + .map(_.symbol) + .toList + } + + def toplevels(input: Input.VirtualFile): List[String] = { + new Mtags().toplevels(input) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/MtagsIndexer.scala b/mtags/src/main/scala/scala/meta/internal/mtags/MtagsIndexer.scala new file mode 100644 index 00000000000..f630bbe591a --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/MtagsIndexer.scala @@ -0,0 +1,134 @@ +package scala.meta.internal.mtags + +import scala.meta.Name +import scala.meta.Term +import scala.{meta => m} +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.{semanticdb => s} +import scala.meta.internal.semanticdb.SymbolInformation.Kind +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.inputs._ + +trait MtagsIndexer { + def language: Language + def indexRoot(): Unit + def index(): s.TextDocument = { + indexRoot() + s.TextDocument( + language = language, + occurrences = names.result(), + symbols = symbols.result() + ) + } + private val root: String = + Symbols.RootPackage + var currentOwner: String = root + def owner = currentOwner + def withOwner[A](owner: String = currentOwner)(thunk: => A): A = { + val old = currentOwner + currentOwner = owner + val result = thunk + currentOwner = old + result + } + def term(name: String, pos: m.Position, kind: Kind, properties: Int): Unit = + addSignature(Descriptor.Term(name), pos, kind, properties) + def term(name: Term.Name, kind: Kind, properties: Int): Unit = + addSignature(Descriptor.Term(name.value), name.pos, kind, properties) + def tparam(name: Name, kind: Kind, properties: Int): Unit = + addSignature( + Descriptor.TypeParameter(name.value), + name.pos, + kind, + properties + ) + def param(name: Name, kind: Kind, properties: Int): Unit = + addSignature( + Descriptor.Parameter(name.value), + name.pos, + kind, + properties + ) + def ctor( + disambiguator: String, + pos: m.Position, + properties: Int + ): Unit = + addSignature( + Descriptor.Method(Names.Constructor.value, disambiguator), + pos, + Kind.CONSTRUCTOR, + properties + ) + def method( + name: String, + disambiguator: String, + pos: m.Position, + properties: Int + ): Unit = + addSignature( + Descriptor.Method(name, disambiguator), + pos, + Kind.METHOD, + properties + ) + def method( + name: Name, + disambiguator: String, + kind: Kind, + properties: Int + ): Unit = { + val methodName = name match { + case Name.Anonymous() => Names.Constructor.value + case _ => name.value + } + addSignature( + Descriptor.Method(methodName, disambiguator), + name.pos, + kind, + properties + ) + } + def tpe(name: String, pos: m.Position, kind: Kind, properties: Int): Unit = + addSignature(Descriptor.Type(name), pos, kind, properties) + def tpe(name: Name, kind: Kind, properties: Int): Unit = + addSignature(Descriptor.Type(name.value), name.pos, kind, properties) + def pkg(name: String, pos: m.Position): Unit = { + addSignature(Descriptor.Package(name), pos, Kind.PACKAGE, 0) + } + def pkg(ref: Term): Unit = ref match { + case Name(name) => + currentOwner = symbol(Descriptor.Package(name)) + case Term.Select(qual, Name(name)) => + pkg(qual) + currentOwner = symbol(Descriptor.Package(name)) + } + private val names = List.newBuilder[s.SymbolOccurrence] + private val symbols = List.newBuilder[s.SymbolInformation] + private def addSignature( + signature: Descriptor, + definition: m.Position, + kind: s.SymbolInformation.Kind, + properties: Int + ): Unit = { + currentOwner = symbol(signature) + val syntax = currentOwner + val role = + if (kind.isPackage) s.SymbolOccurrence.Role.REFERENCE + else s.SymbolOccurrence.Role.DEFINITION + names += s.SymbolOccurrence( + range = Some(definition.toRange), + syntax, + role + ) + symbols += s.SymbolInformation( + symbol = syntax, + language = language, + kind = kind, + properties = properties, + displayName = signature.name.value + ) + } + def symbol(signature: Descriptor): String = + Symbols.Global(currentOwner, signature) +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala new file mode 100644 index 00000000000..0ed55cd551e --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OnDemandSymbolIndex.scala @@ -0,0 +1,191 @@ +package scala.meta.internal.mtags + +import java.nio.CharBuffer +import java.nio.charset.StandardCharsets +import scala.collection.mutable +import scala.meta.inputs.Input +import scala.meta.internal.io.PathIO +import scala.meta.internal.{semanticdb => s} +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath +import scala.meta.internal.mtags.Enrichments._ +import scala.meta.internal.io._ +import scala.meta.internal.semanticdb.Scala._ + +/** + * An implementation of GlobalSymbolIndex with fast indexing and low memory usage. + * + * Fast indexing is enabled by ScalaToplevelMtags, a custom parser that extracts + * only toplevel symbols from a Scala source file. Java source files don't need indexing + * because their file location can be inferred from the symbol with the limitation + * that it doesn't work for Java source files with multiple package-private top-level classes. + * + * Low memory usage is enabled by only storing "non-trivial toplevel" symbols. + * A symbol is "toplevel" when its owner is a package. A symbol is "non-trivial" + * when it doesn't match the path of the file it's defined in, for example `Some#` + * in Option.scala is non-trivial while `Option#` in Option.scala is trivial. + * + * @param toplevels keys are non-trivial toplevel symbols and values are the file + * the symbols are defined in. + * @param definitions keys are global symbols and the values are the files the symbols + * are defined in. Difference between toplevels and definitions + * is that toplevels contains only symbols generated by ScalaToplevelMtags + * while definitions contains only symbols generated by ScalaMtags. + */ +final case class OnDemandSymbolIndex( + toplevels: mutable.Map[String, AbsolutePath] = mutable.Map.empty, + definitions: mutable.Map[String, AbsolutePath] = mutable.Map.empty +) extends GlobalSymbolIndex { + val mtags = new Mtags + private val sourceJars = new ClasspathLoader(Classpath(Nil)) + var indexedSources = 0L + + override def definition(symbol: Symbol): Option[SymbolDefinition] = { + findSymbolDefinition(symbol, symbol) + } + + // Traverses all source files in the given jar file and records + // all non-trivial toplevel Scala symbols. + override def addSourceJar(jar: AbsolutePath): Unit = { + sourceJars.addEntry(jar) + FileIO.withJarFileSystem(jar, create = false) { root => + FileIO.listAllFilesRecursively(root).foreach { source => + if (source.toLanguage.isScala) { + addSourceFile(source, None) + } + } + } + } + + // Enters nontrivial toplevel symbols for Scala source files. + // All other symbols can be inferred on the fly. + override def addSourceFile( + source: AbsolutePath, + sourceDirectory: Option[AbsolutePath] + ): Unit = { + indexedSources += 1 + val path = sourceDirectory match { + case Some(directory) => source.toRelative(directory).toString() + case _ => source.toString() + } + val text = FileIO.slurp(source, StandardCharsets.UTF_8) + val input = Input.VirtualFile(path, text) + val sourceToplevels = mtags.toplevels(input) + sourceToplevels.foreach { toplevel => + if (!isTrivialToplevelSymbol(path, toplevel)) { + toplevels(toplevel) = source + } + } + } + + // Returns true if symbol is com/foo/Bar# and path is /com/foo/Bar.scala + // Such symbols are "trivial" because their definition location can be computed + // on the fly. + private def isTrivialToplevelSymbol(path: String, symbol: String): Boolean = { + val pathBuffer = + CharBuffer.wrap(path).subSequence(1, path.length - ".scala".length) + val symbolBuffer = + CharBuffer.wrap(symbol).subSequence(0, symbol.length - 1) + pathBuffer.equals(symbolBuffer) + } + + // similar as addSourceFile except indexes all global symbols instead of + // only non-trivial toplevel symbols. + private def addMtagsSourceFile(file: AbsolutePath): Unit = { + val docs: s.TextDocuments = PathIO.extension(file.toNIO) match { + case "scala" | "java" => + val language = file.toLanguage + val input = file.toInput + val document = mtags.index(language, input) + s.TextDocuments(List(document)) + case _ => + s.TextDocuments(Nil) + } + if (docs.documents.nonEmpty) { + addTextDocuments(file, docs) + } + } + + // Records all global symbol definitions. + private def addTextDocuments( + file: AbsolutePath, + docs: s.TextDocuments + ): Unit = { + docs.documents.foreach { document => + document.occurrences.foreach { occ => + if (occ.symbol.isGlobal && occ.role.isDefinition) { + definitions.put(occ.symbol, file) + } else { + // do nothing, we only care about global symbol definitions. + } + } + } + } + + /** + * Returns the file where symbol is defined, if any. + * + * Uses two strategies to recover from missing symbol definitions: + * - try to enter the toplevel symbol definition, then lookup symbol again. + * - if the symbol is synthetic, for examples from a case class of macro annotation, + * fall back to related symbols from the enclosing class, see `DefinitionAlternatives`. + * + * @param querySymbol The original symbol that was queried by the user. + * @param symbol The symbol that + * @return + */ + private def findSymbolDefinition( + querySymbol: Symbol, + symbol: Symbol + ): Option[SymbolDefinition] = { + if (!definitions.contains(symbol.value)) { + // Fallback 1: enter the toplevel symbol definition + val toplevel = symbol.toplevel + toplevels.get(toplevel.value) match { + case Some(file) => + addMtagsSourceFile(file) + case _ => + loadFromSourceJars(trivialPaths(toplevel)).foreach(addMtagsSourceFile) + } + } + if (!definitions.contains(symbol.value)) { + // Fallback 2: guess related symbols from the enclosing class. + DefinitionAlternatives(symbol) + .flatMap(alternative => findSymbolDefinition(querySymbol, alternative)) + .headOption + } else { + definitions.get(symbol.value).map { path => + SymbolDefinition( + querySymbol = querySymbol, + definitionSymbol = symbol, + path + ) + } + } + } + + // Returns the first path that resolves to a file. + private def loadFromSourceJars(paths: List[String]): Option[AbsolutePath] = { + paths match { + case Nil => None + case head :: tail => + sourceJars.load(head) match { + case Some(file) => Some(file) + case _ => loadFromSourceJars(tail) + } + } + } + + // Returns relative file paths for trivial toplevel symbols, example: + // Input: scala/collection/immutable/List# + // Output: scala/collection/immutable/List.scala + // scala/collection/immutable/List.java + private def trivialPaths(toplevel: Symbol): List[String] = { + val noExtension = toplevel.value.stripSuffix(".").stripSuffix("#") + List( + noExtension + ".scala", + noExtension + ".java" + ) + } + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/OpenClassLoader.scala b/mtags/src/main/scala/scala/meta/internal/mtags/OpenClassLoader.scala new file mode 100644 index 00000000000..1d398be6fa2 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OpenClassLoader.scala @@ -0,0 +1,21 @@ +package scala.meta.internal.mtags + +import java.net.URLClassLoader +import java.nio.file.Paths +import scala.meta.io.AbsolutePath +import scala.meta.io.RelativePath + +class OpenClassLoader extends URLClassLoader(Array.empty) { + def addEntry(entry: AbsolutePath): Unit = { + super.addURL(entry.toNIO.toUri.toURL) + } + def resolve(uri: String): Option[AbsolutePath] = { + Option(super.findResource(uri)).map { url => + AbsolutePath(Paths.get(url.toURI)) + } + } + def resolve(relpath: RelativePath): Option[AbsolutePath] = { + val uri = relpath.toURI(isDirectory = false).toString + resolve(uri) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/OverloadDisambiguator.scala b/mtags/src/main/scala/scala/meta/internal/mtags/OverloadDisambiguator.scala new file mode 100644 index 00000000000..00912561669 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/OverloadDisambiguator.scala @@ -0,0 +1,19 @@ +package scala.meta.internal.mtags + +import scala.collection.mutable + +/** + * Utility to generate method symbol disambiguators according to SemanticDB spec. + * + * See https://scalameta.org/docs/semanticdb/specification.html#scala-symbol + */ +final class OverloadDisambiguator( + names: mutable.Map[String, Int] = mutable.Map.empty +) { + def disambiguator(name: String): String = { + val n = names.getOrElseUpdate(name, 0) + names(name) = n + 1 + if (n == 0) "()" + else s"(+$n)" + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala new file mode 100644 index 00000000000..cb604285a90 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaMtags.scala @@ -0,0 +1,143 @@ +package scala.meta.internal.mtags + +import scala.meta._ +import scala.meta.inputs.Input +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.semanticdb.SymbolInformation.Kind +import scala.meta.internal.semanticdb.SymbolInformation.Property +import scala.meta.internal.tokenizers.PlatformTokenizerCache +import scala.meta.internal.semanticdb.Scala._ + +// TODO, emit correct method overload symbols https://github.com/scalameta/metals/issues/282 +object ScalaMtags { + def index(input: Input.VirtualFile): MtagsIndexer = { + val root: Source = input.parse[Source].get + new Traverser with MtagsIndexer { + override def language: Language = Language.SCALA + override def indexRoot(): Unit = { + apply(root) + + // :facepalm: https://github.com/scalameta/scalameta/issues/1068 + PlatformTokenizerCache.megaCache.clear() + } + override def apply(tree: Tree): Unit = withOwner() { + def continue(): Unit = super.apply(tree) + def stop(): Unit = () + def enterTermParameters( + paramss: List[List[Term.Param]], + isPrimaryCtor: Boolean + ): Unit = { + for { + params <- paramss + param <- params + } { + withOwner() { + if (isPrimaryCtor) { + param.name match { + case name: Term.Name => + term(name, Kind.METHOD, Property.VAL.value) + case _ => + } + } else { + super.param(param.name, Kind.PARAMETER, 0) + } + } + } + + } + def enterTypeParameters(tparams: List[Type.Param]): Unit = { + for { + tparam <- tparams + } { + withOwner() { + super.tparam(tparam.name, Kind.TYPE_PARAMETER, 0) + } + } + } + def enterPatterns(ps: List[Pat], kind: Kind, properties: Int): Unit = { + ps.foreach { pat => + pat.traverse { + case Pat.Var(name) => + withOwner() { + if (kind.isMethod && properties == Property.VAL.value) { + term(name, kind, properties) + } else { + method(name, "()", kind, properties) + } + } + case _ => + } + } + } + tree match { + case _: Source => continue() + case t: Template => + val overloads = new OverloadDisambiguator() + overloads.disambiguator("") // primary constructor + def disambiguatedMethod( + name: Name, + tparams: List[Type.Param], + paramss: List[List[Term.Param]], + kind: Kind + ): Unit = { + val disambiguator = overloads.disambiguator(name.value) + withOwner() { + method(name, disambiguator, kind, 0) + enterTypeParameters(tparams) + enterTermParameters(paramss, isPrimaryCtor = false) + } + } + t.stats.foreach { + case t: Ctor.Secondary => + disambiguatedMethod(t.name, Nil, t.paramss, Kind.CONSTRUCTOR) + case t: Defn.Def => + disambiguatedMethod(t.name, t.tparams, t.paramss, Kind.METHOD) + case t: Decl.Def => + disambiguatedMethod(t.name, t.tparams, t.paramss, Kind.METHOD) + case _ => + } + continue() + case t: Pkg => pkg(t.ref); continue() + case t: Pkg.Object => + currentOwner = + Symbols.Global(currentOwner, Descriptor.Package(t.name.value)) + term("package", t.name.pos, Kind.OBJECT, 0) + continue() + case t: Defn.Class => + val isImplicit = t.mods.has[Mod.Implicit] + if (isImplicit) { + // emit symbol for implicit conversion + withOwner() { + method(t.name, "()", Kind.METHOD, Property.IMPLICIT.value) + } + } + tpe(t.name, Kind.CLASS, 0) + enterTypeParameters(t.tparams) + enterTermParameters(t.ctor.paramss, isPrimaryCtor = true) + continue() + case t: Defn.Trait => + tpe(t.name, Kind.TRAIT, 0); continue() + enterTypeParameters(t.tparams) + case t: Defn.Object => + term(t.name, Kind.OBJECT, 0); continue() + case t: Defn.Type => + tpe(t.name, Kind.TYPE, 0); stop() + enterTypeParameters(t.tparams) + case t: Decl.Type => + tpe(t.name, Kind.TYPE, 0); stop() + enterTypeParameters(t.tparams) + case t: Defn.Val => + enterPatterns(t.pats, Kind.METHOD, Property.VAL.value); stop() + case t: Decl.Val => + enterPatterns(t.pats, Kind.METHOD, Property.VAL.value); stop() + case t: Defn.Var => + enterPatterns(t.pats, Kind.METHOD, Property.VAR.value); stop() + case t: Decl.Var => + enterPatterns(t.pats, Kind.METHOD, Property.VAR.value); stop() + case _ => stop() + } + } + } + } + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala new file mode 100644 index 00000000000..17fce3fba05 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala @@ -0,0 +1,211 @@ +package scala.meta.internal.mtags + +import scala.meta.dialects +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import scala.meta.internal.semanticdb.Language +import scala.meta.internal.tokenizers.LegacyScanner +import scala.meta.internal.semanticdb.SymbolInformation.Kind +import scala.meta.internal.tokenizers.LegacyToken._ +import scala.meta.internal.inputs._ +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.parsers.ParseException + +final class Identifier(val name: String, val pos: Position) { + override def toString: String = pos.formatMessage("info", name) +} + +/** + * Custom parser that extracts toplevel members from a Scala source file. + * + * Example input: {{{ + * package com.zoo + * class Animal { class Dog } + * object Park { trait Bench } + * }}} + * emits the following symbols: com/zoo/Animal# and com/zoo/Park. + * The inner classes Animal.Dog and Park.Bench are ignored. + * + * This class exists for performance reasons. The ScalaMtags indexer provides + * the same functionality but it is much slower. Performance is important + * because toplevel symbol indexing is on a critical path when users import + * a new project. + */ +class ScalaToplevelMtags(input: Input.VirtualFile) extends MtagsIndexer { + private val scanner = new LegacyScanner(input, dialects.Scala212) + scanner.reader.nextChar() + def isDone: Boolean = scanner.curr.token == EOF + def isNewline: Boolean = + scanner.curr.token == WHITESPACE && + (scanner.curr.strVal match { + case "\n" | "\r" => true + case _ => false + }) + override def language: Language = Language.SCALA + override def indexRoot(): Unit = { + parseStats() + } + + def parseStats(): Unit = { + while (!isDone) { + parseStat() + } + } + + def parseStat(): Unit = { + scanner.curr.token match { + case PACKAGE => + emitPackage() + case CLASS | TRAIT | OBJECT => + emitMember(isPackageObject = false) + + // Ignore everything enclosed within parentheses, braces and brackets. + case LPAREN => + acceptBalancedDelimeters(LPAREN, RPAREN) + case LBRACE => + acceptBalancedDelimeters(LBRACE, RBRACE) + case LBRACKET => + acceptBalancedDelimeters(LBRACKET, RBRACKET) + + // Ignore other tokens + case _ => + scanner.nextToken() + } + } + + def emitPackage(): Unit = { + require(scanner.curr.token == PACKAGE, failMessage("package")) + val old = currentOwner + acceptTrivia() + scanner.curr.token match { + case IDENTIFIER => + val paths = parsePath() + paths.foreach { path => + pkg(path.name, path.pos) + } + case OBJECT => + emitMember(isPackageObject = true) + case _ => + sys.error(failMessage("package name or package object")) + } + if (scanner.curr.token == LBRACE) { + // Handle sibling packages in the same file + // package foo1 { ... } + // package foo2 { ... } + var count = 1 + scanner.nextToken() + while (!isDone && count > 0) { + parseStat() + scanner.curr.token match { + case RBRACE => count -= 1 + case LBRACE => count += 1 + case _ => + } + } + currentOwner = old + } + } + + /** Enters a toplevel symbol such as class, trait or object */ + def emitMember(isPackageObject: Boolean): Unit = { + val kind = scanner.curr.token + acceptTrivia() + val name = newIdentifier + val old = currentOwner + kind match { + case CLASS => + tpe(name.name, name.pos, Kind.CLASS, 0) + case TRAIT => + tpe(name.name, name.pos, Kind.TRAIT, 0) + case OBJECT => + if (isPackageObject) { + withOwner(symbol(Descriptor.Package(name.name))) { + term("package", name.pos, Kind.OBJECT, 0) + } + } else { + term(name.name, name.pos, Kind.OBJECT, 0) + } + } + currentOwner = old + } + + /** Returns position of the current token */ + def newPosition: Position = { + val start = scanner.curr.offset + val end = scanner.curr.endOffset + 1 + Position.Range(input, start, end) + } + + /** Returns a name and position for the current identifier token */ + def newIdentifier: Identifier = { + scanner.curr.token match { + case IDENTIFIER | BACKQUOTED_IDENT => // OK + case _ => fail("identifier") + } + val pos = newPosition + val name = scanner.curr.name + new Identifier(name, pos) + } + + /** Consume token stream like "a.b.c" and return List(a, b, c) */ + def parsePath(): List[Identifier] = { + val buf = List.newBuilder[Identifier] + def loop(): Unit = { + buf += newIdentifier + acceptTrivia() + scanner.curr.token match { + case DOT => + acceptTrivia() + loop() + case _ => + } + } + loop() + buf.result() + } + + /** Consumes the token stream until the matching closing delimiter */ + def acceptBalancedDelimeters(Open: Int, Close: Int): Unit = { + require(scanner.curr.token == Open, failMessage("open delimeter { or (")) + var count = 1 + while (!isDone && count > 0) { + scanner.nextToken() + scanner.curr.token match { + case Open => count += 1 + case Close => count -= 1 + case _ => + } + } + } + + /** Consumes the token stream until the next non-trivia token */ + def acceptTrivia(): Unit = { + scanner.nextToken() + while (!isDone && + (scanner.curr.token match { + case WHITESPACE | COMMENT => true + case _ => false + })) { + scanner.nextToken() + } + } + + // ======= + // Utility + // ======= + + def debug(message: String = ""): Unit = { + val pos = newPosition + pprint.log(pos.formatMessage("info", message)) + } + def fail(expected: String): Nothing = { + throw new ParseException(newPosition, failMessage(expected)) + } + def failMessage(expected: String): String = { + val obtained = InverseLegacyToken.category(scanner.curr.token).toLowerCase() + newPosition.formatMessage( + "error", + s"expected $expected; obtained $obtained" + ) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala b/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala new file mode 100644 index 00000000000..bc8b8006be4 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/SemanticdbClasspath.scala @@ -0,0 +1,46 @@ +package scala.meta.internal.mtags + +import java.nio.charset.Charset +import java.nio.charset.StandardCharsets +import scala.meta.AbsolutePath +import scala.meta.io.RelativePath +import scala.meta.Classpath +import scala.meta.internal.mtags.Enrichments._ + +class SemanticdbClasspath( + sourceroot: AbsolutePath, + classpath: Classpath, + charset: Charset = StandardCharsets.UTF_8 +) { + private val loader = new ClasspathLoader(classpath) + def getSemanticdbPath(scalaPath: AbsolutePath): AbsolutePath = { + semanticdbPath(scalaPath).getOrElse( + throw new NoSuchElementException(scalaPath.toString()) + ) + } + def semanticdbPath(scalaPath: AbsolutePath): Option[AbsolutePath] = { + loader.load(fromScala(scalaPath.toRelative(sourceroot))) + } + def textDocument(scalaPath: AbsolutePath): TextDocumentLookup = { + val scalaRelativePath = scalaPath.toRelative(sourceroot) + val semanticdbRelativePath = fromScala(scalaRelativePath) + loader.load(semanticdbRelativePath) match { + case None => + TextDocumentLookup.NotFound(scalaPath) + case Some(semanticdbPath) => + Semanticdbs.loadTextDocument( + scalaPath, + scalaRelativePath, + semanticdbPath, + charset + ) + } + } + + private def fromScala(path: RelativePath): RelativePath = { + require(path.toNIO.toLanguage.isScala, path.toString) + val semanticdbSibling = path.resolveSibling(_ + ".semanticdb") + val semanticdbPrefix = RelativePath("META-INF").resolve("semanticdb") + semanticdbPrefix.resolve(semanticdbSibling) + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala new file mode 100644 index 00000000000..ad3728d47af --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Semanticdbs.scala @@ -0,0 +1,73 @@ +package scala.meta.internal.mtags + +import java.nio.charset.Charset +import java.nio.file.Files +import scala.meta.AbsolutePath +import scala.meta.inputs.Input +import scala.meta.inputs.Position +import scala.meta.internal.io.FileIO +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.{semanticdb => s} +import scala.meta.io.RelativePath +import SymbolOccurrenceOrdering._ + +object Semanticdbs { + def loadTextDocuments(path: AbsolutePath): s.TextDocuments = { + val in = Files.newInputStream(path.toNIO) + try s.TextDocuments.parseFrom(in) + finally in.close() + } + def loadTextDocument( + scalaPath: AbsolutePath, + scalaRelativePath: RelativePath, + semanticdbPath: AbsolutePath, + charset: Charset + ): TextDocumentLookup = { + val reluri = scalaRelativePath.toURI(false).toString + val sdocs = loadTextDocuments(semanticdbPath) + sdocs.documents.find(_.uri == reluri) match { + case None => TextDocumentLookup.NoMatchingUri(scalaPath, sdocs) + case Some(sdoc) => + val text = FileIO.slurp(scalaPath, charset) + val md5 = MD5.compute(text) + if (sdoc.md5 != md5) { + TextDocumentLookup.Stale(scalaPath, md5, sdoc) + } else { + TextDocumentLookup.Success(sdoc.withText(text)) + } + } + } + def printTextDocument(doc: s.TextDocument): String = { + val symtab = doc.symbols.iterator.map(info => info.symbol -> info).toMap + val sb = new StringBuilder + val occurrences = doc.occurrences.sorted + val input = Input.String(doc.text) + var offset = 0 + occurrences.foreach { occ => + val range = occ.range.get + val pos = Position.Range( + input, + range.startLine, + range.startCharacter, + range.endLine, + range.endCharacter + ) + sb.append(doc.text.substring(offset, pos.end)) + val isPrimaryConstructor = + symtab.get(occ.symbol).exists(_.isPrimary) + if (!occ.symbol.isPackage && !isPrimaryConstructor) { + printSymbol(sb, occ.symbol) + } + offset = pos.end + } + sb.append(doc.text.substring(offset)) + sb.toString() + } + + def printSymbol(sb: StringBuilder, symbol: String): Unit = { + sb.append("/*") + // replace package / with dot . to not upset GitHub syntax highlighting. + .append(symbol.replace('/', '.')) + .append("*/") + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala new file mode 100644 index 00000000000..24e63503276 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/Symbol.scala @@ -0,0 +1,92 @@ +package scala.meta.internal.mtags + +import scala.meta.internal.semanticdb.Scala._ + +/** + * Represents a unique definitions such as a Scala `val`, `object`, `class`, or Java field/method. + * + * Examples: {{{ + * "scala/Predef.String#" + * "scala/collection/immutable/`::`#" + * "scala/Option#get()." + * "scala/Option.apply()." + * }}} + * + * @param value The unique string representation for this symbol. + */ +final class Symbol private (val value: String) { + + def isNone: Boolean = value.isNone + def isRootPackage: Boolean = value.isRootPackage + def isEmptyPackage: Boolean = value.isEmptyPackage + def isGlobal: Boolean = value.isGlobal + def isLocal: Boolean = value.isLocal + def isTerm: Boolean = desc.isTerm + def isMethod: Boolean = desc.isMethod + def isType: Boolean = desc.isType + def isPackage: Boolean = desc.isPackage + def isParameter: Boolean = desc.isParameter + def isTypeParameter: Boolean = desc.isTypeParameter + private def desc: Descriptor = value.desc + + def owner: Symbol = Symbol(value.owner) + def displayName: String = desc.name.value + + def toplevel: Symbol = { + if (value.isNone) this + else if (value.isPackage) this + else { + val owner = value.owner + if (owner.isPackage) this + else Symbol(owner).toplevel + } + } + def isToplevel: Boolean = { + !value.isPackage && + value.owner.isPackage + } + def asNonEmpty: Option[Symbol] = + if (isNone) None + else Some(this) + + override def toString: String = + if (isNone) "" + else value + def structure: String = + if (isNone) "Symbol.None" + else if (isRootPackage) "Symbol.RootPackage" + else if (isEmptyPackage) "Symbol.EmptyPackage" + else s"""Symbol("$value")""" + override def equals(obj: Any): Boolean = + this.eq(obj.asInstanceOf[AnyRef]) || (obj match { + case s: Symbol => value == s.value + case _ => false + }) + override def hashCode(): Int = value.## +} + +object Symbol { + val RootPackage: Symbol = new Symbol(Symbols.RootPackage) + val EmptyPackage: Symbol = new Symbol(Symbols.EmptyPackage) + val None: Symbol = new Symbol(Symbols.None) + def apply(sym: String): Symbol = { + if (sym.isEmpty) Symbol.None + else new Symbol(sym) + } + + object Local { + def unapply(sym: Symbol): Option[Symbol] = + if (sym.isLocal) Some(sym) + else scala.None + } + + object Global { + def unapply(sym: Symbol): Option[(Symbol, Symbol)] = + if (sym.isGlobal) { + val owner = Symbol(sym.value.owner) + Some(owner -> sym) + } else { + scala.None + } + } +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/SymbolOccurrenceOrdering.scala b/mtags/src/main/scala/scala/meta/internal/mtags/SymbolOccurrenceOrdering.scala new file mode 100644 index 00000000000..7e65d80eeaa --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/SymbolOccurrenceOrdering.scala @@ -0,0 +1,31 @@ +package scala.meta.internal.mtags +import scala.meta.internal.semanticdb.SymbolOccurrence + +object SymbolOccurrenceOrdering { + + implicit val occurrenceOrdering: Ordering[SymbolOccurrence] = + new Ordering[SymbolOccurrence] { + override def compare(x: SymbolOccurrence, y: SymbolOccurrence): Int = { + if (x.range.isEmpty) 0 + else if (y.range.isEmpty) 0 + else { + val a = x.range.get + val b = y.range.get + val byLine = Integer.compare( + a.startLine, + b.startLine + ) + if (byLine != 0) { + byLine + } else { + val byCharacter = Integer.compare( + a.startCharacter, + b.startCharacter + ) + byCharacter + } + } + } + } + +} diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/TextDocumentLookup.scala b/mtags/src/main/scala/scala/meta/internal/mtags/TextDocumentLookup.scala new file mode 100644 index 00000000000..3173e2c9a64 --- /dev/null +++ b/mtags/src/main/scala/scala/meta/internal/mtags/TextDocumentLookup.scala @@ -0,0 +1,32 @@ +package scala.meta.internal.mtags + +import scala.meta.AbsolutePath +import scala.meta.internal.{semanticdb => s} + +sealed abstract class TextDocumentLookup { + case class MissingSemanticdb(file: AbsolutePath) + extends Exception(s"missing SemanticDB: $file") + case class StaleSemanticdb(file: AbsolutePath) + extends Exception(s"stale SemanticDB: $file") + final def get: s.TextDocument = this match { + case TextDocumentLookup.Success(document) => + document + case TextDocumentLookup.NotFound(file) => + throw MissingSemanticdb(file) + case TextDocumentLookup.NoMatchingUri(file, _) => + throw MissingSemanticdb(file) + case TextDocumentLookup.Stale(file, _, _) => + throw StaleSemanticdb(file) + } +} +object TextDocumentLookup { + case class Success(document: s.TextDocument) extends TextDocumentLookup + case class NotFound(file: AbsolutePath) extends TextDocumentLookup + case class NoMatchingUri(file: AbsolutePath, documents: s.TextDocuments) + extends TextDocumentLookup + case class Stale( + file: AbsolutePath, + expectedMd5: String, + document: s.TextDocument + ) extends TextDocumentLookup +} diff --git a/project/InputProperties.scala b/project/InputProperties.scala new file mode 100644 index 00000000000..9103dbf94df --- /dev/null +++ b/project/InputProperties.scala @@ -0,0 +1,59 @@ +import java.io.File +import sbt.Keys._ +import sbt._ + +object InputProperties extends AutoPlugin { + var file: Option[File] = None + def resourceGenerator(input: Reference): Def.Initialize[Task[Seq[File]]] = + Def.taskDyn { + file.synchronized { + file match { + case Some(value) => + Def.task(List(value)) + case None => + resourceGeneratorImpl(input) + } + } + } + def resourceGeneratorImpl(input: Reference): Def.Initialize[Task[Seq[File]]] = + Def.task { + val out = managedResourceDirectories + .in(Compile) + .value + .head / "metals-input.properties" + val props = new java.util.Properties() + props.put( + "sourceroot", + baseDirectory.in(ThisBuild).value.toString + ) + val sourceJars = for { + configurationReport <- updateClassifiers.in(input).value.configurations + moduleReport <- configurationReport.modules + (artifact, file) <- moduleReport.artifacts + if artifact.classifier.contains("sources") + } yield file + props.put( + "dependencySources", + sourceJars.map(_.toPath).distinct.mkString(File.pathSeparator) + ) + props.put( + "sourceDirectories", + List( + unmanagedSourceDirectories.in(input, Compile).value, + unmanagedSourceDirectories.in(input, Test).value + ).flatten.mkString(File.pathSeparator) + ) + props.put( + "classpath", + fullClasspath + .in(input, Test) + .value + .map(_.data) + .mkString(File.pathSeparator) + ) + IO.write(props, "input", out) + file = Some(out) + List(out) + } + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 93d9b468076..58b9ef509bc 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,3 +1,5 @@ +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.4") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.0") addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.2.2") addSbtPlugin("com.geirsson" % "sbt-docusaurus" % "0.3.2") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0") diff --git a/sbt-metals/src/main/scala/scala/meta/sbt/MetalsPlugin.scala b/sbt-metals/src/main/scala/scala/meta/sbt/MetalsPlugin.scala index 8b3d785eb10..6fcaf638c65 100644 --- a/sbt-metals/src/main/scala/scala/meta/sbt/MetalsPlugin.scala +++ b/sbt-metals/src/main/scala/scala/meta/sbt/MetalsPlugin.scala @@ -36,11 +36,11 @@ package scala.meta.sbt { def scala211 = "2.11.12" - def scala212 = "2.12.4" + def scala212 = "2.12.7" def supportedScalaVersions = List(scala212, scala211) - def semanticdbVersion = "2.1.7" + def semanticdbVersion = "4.0.0" val metalsBuildInfo = taskKey[Map[String, String]]( "List of key/value pairs for build information such as classpath/sourceDirectories" diff --git a/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/build.sbt b/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/build.sbt index 1a166bea742..1c3d07424cd 100644 --- a/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/build.sbt +++ b/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/build.sbt @@ -1 +1 @@ -scalaVersion := "2.12.4" +scalaVersion := "2.12.7" diff --git a/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/test b/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/test index fe814289cc5..344636d18eb 100644 --- a/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/test +++ b/sbt-metals/src/sbt-test/sbt-metals/semanticdb-scalac/test @@ -3,6 +3,6 @@ $ absent target/ # check that semanticdb-scalac is enabled > compile:compile -$ exists target/scala-2.12/classes/META-INF/semanticdb/src/main/scala/main.semanticdb +$ exists target/scala-2.12/classes/META-INF/semanticdb/src/main/scala/main.scala.semanticdb > test:compile -$ exists target/scala-2.12/test-classes/META-INF/semanticdb/src/test/scala/test.semanticdb +$ exists target/scala-2.12/test-classes/META-INF/semanticdb/src/test/scala/test.scala.semanticdb diff --git a/test-workspace/build.sbt b/test-workspace/build.sbt index c7ef5308c96..d73a1f28066 100644 --- a/test-workspace/build.sbt +++ b/test-workspace/build.sbt @@ -1,3 +1,3 @@ addCompilerPlugin(MetalsPlugin.semanticdbScalac) -scalaVersion := "2.12.4" +scalaVersion := "2.12.7" scalacOptions += "-Yrangepos" diff --git a/tests/input/src/main/java/example/JavaClass.java b/tests/input/src/main/java/example/JavaClass.java new file mode 100644 index 00000000000..8211e757f87 --- /dev/null +++ b/tests/input/src/main/java/example/JavaClass.java @@ -0,0 +1,56 @@ +package example; + +public class JavaClass { + + public JavaClass(int d) { + this.d = d; + } + + public static void a() { + } + + public int b() { + return 1; + } + + public static int c = 2; + public int d = 2; + + public class InnerClass { + public int b() { + return 1; + } + + public int d = 2; + } + + public static class InnerStaticClass { + public static void a() { + } + + public int b() { + return 1; + } + + public static int c = 2; + public int d = 2; + } + + public interface InnerInterface { + public static void a() { + } + + public int b(); + } + + public String publicName() { + return "name"; + } + + // Weird formatting + @Override + public String + toString() { + return ""; + } +} diff --git a/tests/input/src/main/java/example/JavaEnum.java b/tests/input/src/main/java/example/JavaEnum.java new file mode 100644 index 00000000000..186c82c3b9c --- /dev/null +++ b/tests/input/src/main/java/example/JavaEnum.java @@ -0,0 +1,44 @@ +package example; + +public enum JavaEnum { + A(1), + B(2); + + + JavaEnum(int d) { + this.d = d; + } + + public static void a() { + } + + public int b() { + return 1; + } + + ; + public static int c = 2; + public int d = 2; + + public class C { + public int b() { + return 1; + } + + ; + public int d = 2; + } + + public static class F { + public static void a() { + } + + public int b() { + return 1; + } + + ; + public static int c = 2; + public int d = 2; + } +} diff --git a/tests/input/src/main/java/example/JavaInterface.java b/tests/input/src/main/java/example/JavaInterface.java new file mode 100644 index 00000000000..d7abb492986 --- /dev/null +++ b/tests/input/src/main/java/example/JavaInterface.java @@ -0,0 +1,8 @@ +package example; + +public interface JavaInterface { + public static void a() { + } + + public int b(); +} \ No newline at end of file diff --git a/tests/input/src/main/java/example/JavaOverloading.java b/tests/input/src/main/java/example/JavaOverloading.java new file mode 100644 index 00000000000..bcf32e1c2f5 --- /dev/null +++ b/tests/input/src/main/java/example/JavaOverloading.java @@ -0,0 +1,8 @@ +package example; + +public class JavaOverloading { + public int name = 1; + public static int name(String name) { return name.length(); } + public int name() { return 1; } + public int name(int n) { return n; } +} diff --git a/tests/input/src/main/scala/example/AnonymousClasses.scala b/tests/input/src/main/scala/example/AnonymousClasses.scala new file mode 100644 index 00000000000..e0c14517c15 --- /dev/null +++ b/tests/input/src/main/scala/example/AnonymousClasses.scala @@ -0,0 +1,3 @@ +package example + +class AnonymousClasses {} diff --git a/tests/input/src/main/scala/example/Comments.scala b/tests/input/src/main/scala/example/Comments.scala new file mode 100644 index 00000000000..80c0d4dd67f --- /dev/null +++ b/tests/input/src/main/scala/example/Comments.scala @@ -0,0 +1,9 @@ +package example + +class /* comment */ Comments { + object /* comment */ A + trait /* comment */ A + val /* comment */ a = 1 + def /* comment */ b = 1 + var /* comment */ c = 1 +} diff --git a/tests/input/src/main/scala/example/Companion.scala b/tests/input/src/main/scala/example/Companion.scala new file mode 100644 index 00000000000..1cc4198a339 --- /dev/null +++ b/tests/input/src/main/scala/example/Companion.scala @@ -0,0 +1,5 @@ +package example + +abstract class Companion() extends Object() {} + +object Companion {} diff --git a/tests/input/src/main/scala/example/Definitions.scala b/tests/input/src/main/scala/example/Definitions.scala new file mode 100644 index 00000000000..b82efb56d6a --- /dev/null +++ b/tests/input/src/main/scala/example/Definitions.scala @@ -0,0 +1,15 @@ +package example + +class Definitions { + Predef.any2stringadd(1) + List[ + java.util.Map.Entry[ + java.lang.Integer, + java.lang.Double + ] + ]( + xs = null + ) + MacroAnnotation.decodeMacroAnnotation + MacroAnnotation.encodeMacroAnnotation +} diff --git a/tests/input/src/main/scala/example/ForComprehensions.scala b/tests/input/src/main/scala/example/ForComprehensions.scala new file mode 100644 index 00000000000..0d131824c16 --- /dev/null +++ b/tests/input/src/main/scala/example/ForComprehensions.scala @@ -0,0 +1,40 @@ +package example + +class ForComprehensions { + for { + a <- List(1) + b <- List(a) + if ( + a, + b + ) == (1, 2) + ( + c, + d + ) <- List((a, b)) + if ( + a, + b, + c, + d + ) == (1, 2, 3, 4) + e = ( + a, + b, + c, + d + ) + if e == (1, 2, 3, 4) + f <- List(e) + } yield { + ( + a, + b, + c, + d, + e, + f + ) + } + +} diff --git a/tests/input/src/main/scala/example/ImplicitClasses.scala b/tests/input/src/main/scala/example/ImplicitClasses.scala new file mode 100644 index 00000000000..6abdad1194c --- /dev/null +++ b/tests/input/src/main/scala/example/ImplicitClasses.scala @@ -0,0 +1,10 @@ +package example + +object ImplicitClasses { + implicit class Xtension(number: Int) { + def increment: Int = number + 1 + } + implicit class XtensionAnyVal(private val number: Int) extends AnyVal { + def double: Int = number * 2 + } +} diff --git a/tests/input/src/main/scala/example/ImplicitConversions.scala b/tests/input/src/main/scala/example/ImplicitConversions.scala new file mode 100644 index 00000000000..adaf2c0887e --- /dev/null +++ b/tests/input/src/main/scala/example/ImplicitConversions.scala @@ -0,0 +1,28 @@ +package example + +class ImplicitConversions { + implicit def string2Number( + string: String + ): Int = 42 + val message = "" + val number = 42 + val tuple = (1, 2) + val char: Char = 'a' + + // extension methods + message + .stripSuffix("h") + tuple + "Hello" + + // implicit conversions + val x: Int = message + + // interpolators + s"Hello $message $number" + s"""Hello + |$message + |$number""".stripMargin + + val a: Int = char + val b: Long = char +} diff --git a/tests/input/src/main/scala/example/Imports.scala b/tests/input/src/main/scala/example/Imports.scala new file mode 100644 index 00000000000..201218e275b --- /dev/null +++ b/tests/input/src/main/scala/example/Imports.scala @@ -0,0 +1,10 @@ +package example + +import util.{Failure => NotGood} +import math.{floor => _, _} + +class Imports { + // rename reference + NotGood(null) + max(1, 2) +} diff --git a/tests/input/src/main/scala/example/JavaThenScala.scala b/tests/input/src/main/scala/example/JavaThenScala.scala new file mode 100644 index 00000000000..ef04209a7a2 --- /dev/null +++ b/tests/input/src/main/scala/example/JavaThenScala.scala @@ -0,0 +1,5 @@ +package example + +class JavaThenScala { + new JavaClass(42) +} diff --git a/tests/input/src/main/scala/example/Locals.scala b/tests/input/src/main/scala/example/Locals.scala new file mode 100644 index 00000000000..1f64b688457 --- /dev/null +++ b/tests/input/src/main/scala/example/Locals.scala @@ -0,0 +1,8 @@ +package example + +class Locals { + { + val x = 2 + x + 2 + } +} diff --git a/tests/input/src/main/scala/example/MacroAnnotation.scala b/tests/input/src/main/scala/example/MacroAnnotation.scala new file mode 100644 index 00000000000..530c2e00914 --- /dev/null +++ b/tests/input/src/main/scala/example/MacroAnnotation.scala @@ -0,0 +1,19 @@ +package example + +import io.circe.derivation.annotations.JsonCodec + +@JsonCodec +// FIXME: https://github.com/scalameta/scalameta/issues/1789 +case class MacroAnnotation( + name: String +) { + def method = 42 +} + +object MacroAnnotations { + import scala.meta._ + // IntelliJ has never managed to goto definition for the inner classes from Trees.scala + // due to the macro annotations. + val x: Defn.Class = Defn.Class(null, null, null, null, null) + val y: Mod.Final = Mod.Final() +} diff --git a/tests/input/src/main/scala/example/MethodOverload.scala b/tests/input/src/main/scala/example/MethodOverload.scala new file mode 100644 index 00000000000..223de24dea0 --- /dev/null +++ b/tests/input/src/main/scala/example/MethodOverload.scala @@ -0,0 +1,9 @@ +package example + +class MethodOverload(b: String) { + def this() = this("") + def this(c: Int) = this("") + val a = 2 + def a(x: Int) = 2 + def a(x: Int, y: Int) = 2 +} diff --git a/tests/input/src/main/scala/example/Miscellaneous.scala b/tests/input/src/main/scala/example/Miscellaneous.scala new file mode 100644 index 00000000000..4a3890cf82d --- /dev/null +++ b/tests/input/src/main/scala/example/Miscellaneous.scala @@ -0,0 +1,12 @@ +package example + +class Miscellaneous { + // backtick identifier + val `a b` = 42 + + // infix + inferred apply/implicits/tparams + (List(1) + .map(_ + 1) + ++ + List(3)) +} diff --git a/tests/input/src/main/scala/example/NamedArguments.scala b/tests/input/src/main/scala/example/NamedArguments.scala new file mode 100644 index 00000000000..8982a6a42ca --- /dev/null +++ b/tests/input/src/main/scala/example/NamedArguments.scala @@ -0,0 +1,35 @@ +package example + +case class User( + name: String = { + // assert default values have occurrences + Map.toString + } +) +object NamedArguments { + val susan = "Susan" + val user1 = + User + .apply( + name = "John" + ) + val user2: User = + User( + // FIXME: https://github.com/scalameta/scalameta/issues/1787 + name = susan + ).copy( + name = susan + ) + + // anonymous classes + @deprecated( + message = "a", + since = susan + ) def b = 1 + + // vararg + List( + xs = 2 + ) + +} diff --git a/tests/input/src/main/scala/example/PatternMatching.scala b/tests/input/src/main/scala/example/PatternMatching.scala new file mode 100644 index 00000000000..c85a3bd64fc --- /dev/null +++ b/tests/input/src/main/scala/example/PatternMatching.scala @@ -0,0 +1,24 @@ +package example + +class PatternMatching { + val some = Some(1) + some match { + case Some(number) => + number + } + + // tuple deconstruction + val (left, right) = (1, 2) + (left, right) + + // val deconstruction + val Some(number1) = + some + number1 + + def localDeconstruction = { + val Some(number2) = + some + number2 + } +} diff --git a/tests/input/src/main/scala/example/ReflectiveInvocation.scala b/tests/input/src/main/scala/example/ReflectiveInvocation.scala new file mode 100644 index 00000000000..92c0b2f799f --- /dev/null +++ b/tests/input/src/main/scala/example/ReflectiveInvocation.scala @@ -0,0 +1,9 @@ +package example + +class ReflectiveInvocation { + new Serializable { + def message = "message" + // reflective invocation + }.message + +} diff --git a/tests/input/src/main/scala/example/Scalalib.scala b/tests/input/src/main/scala/example/Scalalib.scala new file mode 100644 index 00000000000..a3821a52221 --- /dev/null +++ b/tests/input/src/main/scala/example/Scalalib.scala @@ -0,0 +1,25 @@ +package example + +class Scalalib { + val lst = List[ + ( + Nothing, + Null, + Singleton, + Any, + AnyRef, + AnyVal, + Int, + Short, + Double, + Float, + Char + ) + ]() + lst.isInstanceOf[Any] + lst.asInstanceOf[Any] + println(lst.##) + lst ne lst + lst eq lst + lst == lst +} diff --git a/tests/input/src/main/scala/example/StructuralTypes.scala b/tests/input/src/main/scala/example/StructuralTypes.scala new file mode 100644 index 00000000000..054323f76f1 --- /dev/null +++ b/tests/input/src/main/scala/example/StructuralTypes.scala @@ -0,0 +1,19 @@ +package example + +object StructuralTypes { + type User = { + def name: String + def age: Int + } + + val user = null.asInstanceOf[User] + user.name + user.age + + val V: Object { + def scalameta: String + } = new { + def scalameta = "4.0" + } + V.scalameta +} diff --git a/tests/input/src/main/scala/example/TypeParameters.scala b/tests/input/src/main/scala/example/TypeParameters.scala new file mode 100644 index 00000000000..ab0a7bdbae4 --- /dev/null +++ b/tests/input/src/main/scala/example/TypeParameters.scala @@ -0,0 +1,8 @@ +package example + +class TypeParameters[A] { + def method[B] = 42 + trait TraitParameter[C] + type AbstractTypeAlias[D] + type TypeAlias[E] = List[E] +} diff --git a/tests/input/src/main/scala/example/nested/DoublePackage.scala b/tests/input/src/main/scala/example/nested/DoublePackage.scala new file mode 100644 index 00000000000..877a5bcd4b5 --- /dev/null +++ b/tests/input/src/main/scala/example/nested/DoublePackage.scala @@ -0,0 +1,9 @@ +package example + +package nested.x { + class DoublePackage {} +} + +package nested2.y { + class DoublePackage {} +} diff --git a/tests/input/src/main/scala/example/nested/ExampleNested.scala b/tests/input/src/main/scala/example/nested/ExampleNested.scala new file mode 100644 index 00000000000..380d1e0748b --- /dev/null +++ b/tests/input/src/main/scala/example/nested/ExampleNested.scala @@ -0,0 +1,3 @@ +package example.nested + +class ExampleNested {} diff --git a/tests/input/src/main/scala/example/nested/package.scala b/tests/input/src/main/scala/example/nested/package.scala new file mode 100644 index 00000000000..dd8cc0e4405 --- /dev/null +++ b/tests/input/src/main/scala/example/nested/package.scala @@ -0,0 +1,9 @@ +package example + +package object nested { + + class PackageObjectNestedClass + +} + +class PackageObjectSibling diff --git a/tests/input/src/main/scala/example/package.scala b/tests/input/src/main/scala/example/package.scala new file mode 100644 index 00000000000..4d7fc77d14a --- /dev/null +++ b/tests/input/src/main/scala/example/package.scala @@ -0,0 +1,5 @@ +package object example { + + class PackageObjectClass + +} diff --git a/tests/input/src/main/scala/example/type/Backtick.scala b/tests/input/src/main/scala/example/type/Backtick.scala new file mode 100644 index 00000000000..f7b0be6da06 --- /dev/null +++ b/tests/input/src/main/scala/example/type/Backtick.scala @@ -0,0 +1,3 @@ +package example.`type` + +class Backtick {} diff --git a/tests/input/src/test/scala/example/ExampleSuite.scala b/tests/input/src/test/scala/example/ExampleSuite.scala new file mode 100644 index 00000000000..037a4f40f0a --- /dev/null +++ b/tests/input/src/test/scala/example/ExampleSuite.scala @@ -0,0 +1,5 @@ +package example + +object ExampleSuite { + println(NamedArguments.user2) +} diff --git a/tests/integration/src/test/scala/sbtserver/SbtServerTest.scala b/tests/integration/src/test/scala/sbtserver/SbtServerTest.scala deleted file mode 100644 index 60cfbdf4eb4..00000000000 --- a/tests/integration/src/test/scala/sbtserver/SbtServerTest.scala +++ /dev/null @@ -1,47 +0,0 @@ -package tests.sbtserver - -import scala.concurrent.Await -import scala.concurrent.duration.Duration -import scala.meta.metals.sbtserver.Sbt -import scala.meta.metals.sbtserver.SbtServer -import scala.util.Failure -import scala.util.Success -import monix.execution.Scheduler.Implicits.global -import org.langmeta.internal.io.PathIO -import scala.meta.jsonrpc.Services -import scala.meta.lsp.TextDocument -import scala.meta.lsp.Window -import tests.MegaSuite - -case class SbtServerConnectionError(msg: String) extends Exception(msg) - -object SbtServerTest extends MegaSuite { - - test("correct sbt 1.1 project establishes successful connection") { - val services = Services - .empty(scribe.Logger.root) - .notification(Window.logMessage)(msg => ()) - .notification(TextDocument.publishDiagnostics)(msg => ()) - val program = for { - sbt <- SbtServer.connect(PathIO.workingDirectory, services).map { - case Left(err) => throw SbtServerConnectionError(err) - case Right(ok) => - println("Established connection to sbt server.") - ok - } - response <- Sbt.setting.query("metals/crossScalaVersions")(sbt.client) - } yield { - val Right(json) = response - val Right(crossScalaVersions) = json.value.as[List[String]] - sbt.runningServer.cancel() - assertEquals(crossScalaVersions, List("2.12.4")) - crossScalaVersions - } - val result = Await.result(program.materialize.runAsync, Duration(5, "s")) - result match { - case Success(_) => // hurrah :clap: - case Failure(err) => throw err - } - } - -} diff --git a/tests/unit/src/main/scala/tests/BaseExpectSuite.scala b/tests/unit/src/main/scala/tests/BaseExpectSuite.scala new file mode 100644 index 00000000000..2a0b4bd1e4a --- /dev/null +++ b/tests/unit/src/main/scala/tests/BaseExpectSuite.scala @@ -0,0 +1,32 @@ +package tests + +import scala.meta.internal.io.PathIO +import scala.meta.internal.symtab.GlobalSymbolTable +import scala.meta.io.AbsolutePath +import scala.meta.internal.mtags.SemanticdbClasspath +import scala.meta.io.Classpath + +/** Base class for all expect tests. + * + * Exposes useful methods to lookup metadata about the input project. + */ +abstract class BaseExpectSuite(val suiteName: String) extends BaseSuite { + lazy val input = InputProperties.default() + + lazy val symtab = { + val bootClasspath = + sys.props + .collectFirst { + case (k, v) if k.endsWith(".boot.class.path") => Classpath(v) + } + .getOrElse(Classpath(Nil)) + GlobalSymbolTable( + input.classpath ++ bootClasspath + ) + } + final lazy val sourceroot: AbsolutePath = + PathIO.workingDirectory + final lazy val classpath = + new SemanticdbClasspath(sourceroot, input.classpath) + def saveExpect(): Unit +} diff --git a/metals/src/test/scala/tests/MegaSuite.scala b/tests/unit/src/main/scala/tests/BaseSuite.scala similarity index 76% rename from metals/src/test/scala/tests/MegaSuite.scala rename to tests/unit/src/main/scala/tests/BaseSuite.scala index 30c89fb8671..db8789dbc43 100644 --- a/metals/src/test/scala/tests/MegaSuite.scala +++ b/tests/unit/src/main/scala/tests/BaseSuite.scala @@ -12,16 +12,15 @@ import utest.ufansi.Str import io.circe.Json import io.circe.Printer import scala.meta.metals.MetalsLogger +import utest.ufansi.Attrs /** - * Test suite that supports + * Test suite that replace utest DSL with FunSuite-style syntax from ScalaTest. + * + * Exposes * - * - beforeAll - * - afterAll - * - pretty multiline string diffing - * - FunSuite-style test("name") { => fun } */ -class MegaSuite extends TestSuite { +class BaseSuite extends TestSuite { scribe.Logger.root .clearHandlers() .withHandler( @@ -37,12 +36,8 @@ class MegaSuite extends TestSuite { def assertEquals[T](obtained: T, expected: T, hint: String = ""): Unit = { if (obtained != expected) { val hintMsg = if (hint.isEmpty) "" else s" (hint: $hint)" - // TODO(olafur) handle sequences - val diff = - DiffAsserts.error2message(obtained.toString, expected.toString) - if (diff.isEmpty) - fail(s"obtained=<$obtained> != expected=<$expected>$hintMsg") - else fail(diff + hintMsg) + assertNoDiff(obtained.toString, expected.toString, hint) + fail(s"obtained=<$obtained> != expected=<$expected>$hintMsg") } } def assertNoDiff( @@ -50,7 +45,9 @@ class MegaSuite extends TestSuite { expected: String, title: String = "" ): Unit = { - DiffAsserts.assertNoDiff(obtained, expected, title) + DiffAssertions.colored { + DiffAssertions.assertNoDiffOrPrintExpected(obtained, expected, title) + } } def assertNoDiff( obtained: Json, @@ -61,14 +58,18 @@ class MegaSuite extends TestSuite { override def utestAfterAll(): Unit = afterAll() override def utestFormatter: Formatter = new Formatter { + override def exceptionMsgColor: Attrs = Attrs.Empty override def exceptionStackFrameHighlighter( s: StackTraceElement ): Boolean = { + s.getClassName.startsWith("scala.meta.internal.mtags.") || + s.getClassName.startsWith("scala.meta.internal.metals.") || s.getClassName.startsWith("scala.meta.metals.") || (s.getClassName.startsWith("tests") && - !s.getClassName.startsWith("tests.DiffAsserts") && + !s.getClassName.startsWith("tests.DiffAssertions") && !s.getClassName.startsWith("tests.MegaSuite")) } + override def formatWrapWidth: Int = 3000 override def formatException(x: Throwable, leftIndent: String): Str = super.formatException(x, "") } @@ -83,7 +84,6 @@ class MegaSuite extends TestSuite { myTests += (name -> (() => fun)) } - private class TestFailedException(msg: String) extends Exception(msg) def fail(msg: String) = { val ex = new TestFailedException(msg) ex.setStackTrace(ex.getStackTrace.slice(1, 2)) @@ -100,3 +100,4 @@ class MegaSuite extends TestSuite { Tests.apply(names, thunks) } } +class TestFailedException(msg: String) extends Exception(msg) diff --git a/tests/unit/src/main/scala/tests/DiffAssertions.scala b/tests/unit/src/main/scala/tests/DiffAssertions.scala new file mode 100644 index 00000000000..c60c2649a66 --- /dev/null +++ b/tests/unit/src/main/scala/tests/DiffAssertions.scala @@ -0,0 +1,31 @@ +package tests + +import org.scalactic.source.Position +import scala.util.control.NonFatal +import utest.ufansi.Color + +/** Bridge for scalameta testkit DiffAssertions and utest assertions */ +object DiffAssertions extends scala.meta.testkit.DiffAssertions { + def expectNoDiff(obtained: String, expected: String, hint: String = "")( + implicit pos: Position + ): Unit = { + colored { + assertNoDiff(obtained, expected, hint) + } + } + def colored[T](thunk: => T): T = { + try { + thunk + } catch { + case NonFatal(e) => + val message = e.getMessage.lines + .map { line => + if (line.startsWith("+")) Color.Green(line) + else if (line.startsWith("-")) Color.LightRed(line) + else Color.Reset(line) + } + .mkString("\n") + throw new TestFailedException(message) + } + } +} diff --git a/tests/unit/src/main/scala/tests/DirectoryExpectSuite.scala b/tests/unit/src/main/scala/tests/DirectoryExpectSuite.scala new file mode 100644 index 00000000000..805676669f3 --- /dev/null +++ b/tests/unit/src/main/scala/tests/DirectoryExpectSuite.scala @@ -0,0 +1,46 @@ +package tests + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import scala.meta.io.AbsolutePath + +/** + * Expect test with multiple output files. + * + * @param directoryName root directory name: metals/src/test/resources/$name + */ +abstract class DirectoryExpectSuite(directoryName: String) + extends BaseExpectSuite(directoryName) { + + def testCases(): List[ExpectTestCase] + + final lazy val expectRoot: AbsolutePath = + AbsolutePath(BuildInfo.testResourceDirectory).resolve(directoryName) + final def test(unitTest: ExpectTestCase): Unit = { + val testName = + unitTest.input.file.toNIO.getFileName.toString.stripSuffix(".scala") + test(testName) { + val obtained = unitTest.obtained() + unitTest.input.slurpExpected(directoryName) match { + case Some(expected) => + DiffAssertions.expectNoDiff(obtained, expected) + case None => + val expect = unitTest.input.expectPath(directoryName).toNIO + fail(s"does not exist: $expect (run save-expect to fix this problem)") + } + } + } + + final def saveExpect(): Unit = { + RecursivelyDeleteDirectory.run(expectRoot.toNIO) + testCases().foreach { testCase => + val obtained = testCase.obtained() + val file = testCase.input.expectPath(directoryName).toNIO + Files.createDirectories(file.getParent) + println(s"write: $file") + Files.write(file, obtained.getBytes(StandardCharsets.UTF_8)) + } + } + + testCases().foreach(test) +} diff --git a/tests/unit/src/main/scala/tests/ExpectTestCase.scala b/tests/unit/src/main/scala/tests/ExpectTestCase.scala new file mode 100644 index 00000000000..0a773285912 --- /dev/null +++ b/tests/unit/src/main/scala/tests/ExpectTestCase.scala @@ -0,0 +1,6 @@ +package tests + +case class ExpectTestCase( + input: InputFile, + obtained: () => String +) diff --git a/tests/unit/src/main/scala/tests/InputFile.scala b/tests/unit/src/main/scala/tests/InputFile.scala new file mode 100644 index 00000000000..8ac13bddebe --- /dev/null +++ b/tests/unit/src/main/scala/tests/InputFile.scala @@ -0,0 +1,31 @@ +package tests + +import java.nio.charset.StandardCharsets +import scala.meta.inputs.Input +import scala.meta.internal.io.FileIO +import scala.meta.internal.io.PathIO +import scala.meta.io.AbsolutePath +import scala.meta.io.RelativePath +import scala.meta.internal.mtags.Enrichments._ + +case class InputFile( + file: AbsolutePath, + sourceDirectory: AbsolutePath, + semanticdbRelativePath: RelativePath, +) { + def sourceDirectoryRelativePath: RelativePath = + file.toRelative(sourceDirectory) + def input: Input.VirtualFile = file.toInput + def isScala: Boolean = PathIO.extension(file.toNIO) == "scala" + def expectPath(name: String): AbsolutePath = + AbsolutePath(BuildInfo.testResourceDirectory) + .resolve(name) + .resolve(sourceDirectoryRelativePath) + def slurpExpected(name: String): Option[String] = { + if (expectPath(name).isFile) { + Some(FileIO.slurp(expectPath(name), StandardCharsets.UTF_8)) + } else { + None + } + } +} diff --git a/tests/unit/src/main/scala/tests/InputProperties.scala b/tests/unit/src/main/scala/tests/InputProperties.scala new file mode 100644 index 00000000000..cbff1ec1253 --- /dev/null +++ b/tests/unit/src/main/scala/tests/InputProperties.scala @@ -0,0 +1,54 @@ +package tests + +import scala.meta.internal.io.FileIO +import scala.meta.internal.io.PathIO +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath + +case class InputProperties( + sourceroot: AbsolutePath, + sourceDirectories: List[AbsolutePath], + classpath: Classpath, + dependencySources: Classpath +) { + + def scalaFiles: List[InputFile] = { + allFiles.filter(file => PathIO.extension(file.file.toNIO) == "scala") + } + + def allFiles: List[InputFile] = { + for { + directory <- sourceDirectories + if directory.isDirectory + file <- FileIO.listAllFilesRecursively(directory) + } yield { + InputFile( + file = file, + directory, + semanticdbRelativePath = file.toRelative(sourceroot) + ) + } + } +} + +object InputProperties { + def default(): InputProperties = { + val props = new java.util.Properties() + val path = "metals-input.properties" + val in = this.getClass.getClassLoader.getResourceAsStream(path) + assert(in != null, s"no such resource: $path") + try props.load(in) + finally in.close() + def getKey(key: String): String = { + Option(props.getProperty(key)).getOrElse { + throw new IllegalArgumentException(props.toString) + } + } + InputProperties( + sourceroot = AbsolutePath(getKey("sourceroot")), + sourceDirectories = Classpath(getKey("sourceDirectories")).entries, + classpath = Classpath(getKey("classpath")), + dependencySources = Classpath(getKey("dependencySources")) + ) + } +} diff --git a/metals/src/main/scala/scala/meta/metals/Jars.scala b/tests/unit/src/main/scala/tests/Jars.scala similarity index 92% rename from metals/src/main/scala/scala/meta/metals/Jars.scala rename to tests/unit/src/main/scala/tests/Jars.scala index dfff04e0b3c..dfe8e0e1825 100644 --- a/metals/src/main/scala/scala/meta/metals/Jars.scala +++ b/tests/unit/src/main/scala/tests/Jars.scala @@ -1,15 +1,18 @@ -package scala.meta.metals +package tests +import coursier._ import java.io.OutputStreamWriter import java.io.PrintStream -import coursier._ -import org.langmeta.io.AbsolutePath +import scala.meta.io.AbsolutePath case class ModuleID(organization: String, name: String, version: String) { def toCoursier: Dependency = Dependency(Module(organization, name), version) override def toString: String = s"$organization:$name:$version" } + object ModuleID { + def scalaReflect(scalaVersion: String): ModuleID = + ModuleID("org.scala-lang", "scala-reflect", scalaVersion) def fromString(string: String): List[ModuleID] = { string .split(";") @@ -24,6 +27,7 @@ object ModuleID { .toList } } + object Jars { def fetch( org: String, @@ -53,8 +57,9 @@ object Jars { val resolution = res.process.run(fetch).unsafePerformSync val errors = resolution.metadataErrors if (errors.nonEmpty) { - sys.error(errors.mkString("\n")) +// sys.error(errors.mkString("\n")) } + val artifacts: Seq[Artifact] = if (fetchSourceJars) { resolution diff --git a/tests/unit/src/main/scala/tests/Libraries.scala b/tests/unit/src/main/scala/tests/Libraries.scala new file mode 100644 index 00000000000..503d8596120 --- /dev/null +++ b/tests/unit/src/main/scala/tests/Libraries.scala @@ -0,0 +1,93 @@ +package tests + +import java.nio.file.Files +import java.nio.file.Paths +import scala.meta.io.AbsolutePath +import scala.meta.io.Classpath + +object Libraries { + + lazy val suite: List[Library] = { + val buf = List.newBuilder[Library] + buf += Library.jdk + buf += Library.scalaLibrary + buf += Library( + "org.scalameta", + "scalameta_2.12", + "3.2.0", + provided = List( + ModuleID.scalaReflect("2.12.7") + ) + ) + buf += Library("com.typesafe.akka", "akka-testkit_2.12", "2.5.9") + buf += Library("com.typesafe.akka", "akka-actor_2.11", "2.5.9") + buf += Library( + "org.apache.spark", + "spark-sql_2.11", + "2.2.1", + provided = List( + ModuleID( + "org.eclipse.jetty", + "jetty-servlet", + "9.3.11.v20160721" + ) + ) + ) + buf += Library("org.apache.kafka", "kafka_2.12", "1.0.0") + buf += Library("org.apache.flink", "flink-parent", "1.4.1") + buf += Library("io.grpc", "grpc-all", "1.10.0") + buf += Library("io.buoyant", "linkerd-core_2.12", "1.4.3") + buf.result + } +} + +case class Library( + name: String, + classpath: () => Classpath, + sources: () => Classpath +) +object Library { + def apply( + organization: String, + artifact: String, + version: String, + provided: List[ModuleID] = Nil + ): Library = { + def fetch(sources: Boolean) = { + val jars = + Jars.fetch(organization, artifact, version, fetchSourceJars = sources) + Classpath(jars) + + } + Library( + List(organization, artifact, version).mkString(":"), + classpath = () => fetch(sources = false), + sources = () => fetch(sources = true) + ) + } + + def jdkSources: Option[AbsolutePath] = + for { + javaHome <- sys.props.get("java.home") + jdkSources = Paths.get(javaHome).getParent.resolve("src.zip") + if Files.isRegularFile(jdkSources) + } yield AbsolutePath(jdkSources) + + lazy val jdk: Library = { + val bootClasspath = Classpath( + sys.props + .collectFirst { case (k, v) if k.endsWith(".boot.class.path") => v } + .getOrElse("") + ).entries.filter(_.isFile) + Library( + "JDK", + () => Classpath(bootClasspath), + () => Classpath(jdkSources.toList) + ) + } + lazy val scalaLibrary: Library = Library( + "org.scala-lang", + "scala-library", + scala.util.Properties.versionNumberString + ) +} diff --git a/tests/unit/src/main/scala/tests/RecursivelyDeleteDirectory.scala b/tests/unit/src/main/scala/tests/RecursivelyDeleteDirectory.scala new file mode 100644 index 00000000000..35fd40ddb66 --- /dev/null +++ b/tests/unit/src/main/scala/tests/RecursivelyDeleteDirectory.scala @@ -0,0 +1,37 @@ +package tests + +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.SimpleFileVisitor +import java.nio.file.attribute.BasicFileAttributes + +object RecursivelyDeleteDirectory { + def run(root: Path): Unit = { + if (!Files.exists(root)) return + Files.walkFileTree( + root, + new SimpleFileVisitor[Path] { + override def visitFile( + file: Path, + attrs: BasicFileAttributes + ): FileVisitResult = { + Files.delete(file) + super.visitFile(file, attrs) + } + override def postVisitDirectory( + dir: Path, + exc: IOException + ): FileVisitResult = { + val stream = Files.list(dir) + if (!stream.iterator().hasNext) { + Files.delete(dir) + } + stream.close() + super.postVisitDirectory(dir, exc) + } + } + ) + } +} diff --git a/tests/unit/src/main/scala/tests/SingleFileExpectSuite.scala b/tests/unit/src/main/scala/tests/SingleFileExpectSuite.scala new file mode 100644 index 00000000000..4ef52cd90b0 --- /dev/null +++ b/tests/unit/src/main/scala/tests/SingleFileExpectSuite.scala @@ -0,0 +1,49 @@ +package tests + +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import scala.meta.internal.io.FileIO +import scala.meta.io.AbsolutePath + +/** + * An expect suite with a single output file. + * + * @param filename The name of the single expect file. + */ +abstract class SingleFileExpectSuite(filename: String) + extends BaseExpectSuite(filename) { + def obtained(): String + + final val path: AbsolutePath = + SingleFileExpectSuite.expectRoot.resolve(filename) + + final override def saveExpect(): Unit = { + println(s"write: $path") + Files.write( + path.toNIO, + obtained().getBytes(StandardCharsets.UTF_8) + ) + } + + final def expected(): String = { + if (!path.isFile) { + Files.createDirectories(path.toNIO.getParent) + Files.createFile(path.toNIO) + } + FileIO.slurp(path, StandardCharsets.UTF_8) + } + + test(filename) { + val obtainedString = obtained() + val expectedString = expected() + DiffAssertions.expectNoDiff( + obtainedString, + expectedString + ) + } +} + +object SingleFileExpectSuite { + def expectRoot: AbsolutePath = + AbsolutePath(BuildInfo.testResourceDirectory).resolve("expect") +} diff --git a/tests/unit/src/test/resources/definition/example/AnonymousClasses.scala b/tests/unit/src/test/resources/definition/example/AnonymousClasses.scala new file mode 100644 index 00000000000..956cedbcb21 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/AnonymousClasses.scala @@ -0,0 +1,3 @@ +package example + +class AnonymousClasses/*AnonymousClasses.scala*/ {} diff --git a/tests/unit/src/test/resources/definition/example/Comments.scala b/tests/unit/src/test/resources/definition/example/Comments.scala new file mode 100644 index 00000000000..3d6bfeafa94 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Comments.scala @@ -0,0 +1,9 @@ +package example + +class /* comment */ Comments/*Comments.scala*/ { + object /* comment */ A/*Comments.scala*/ + trait /* comment */ A/*Comments.scala*/ + val /* comment */ a/*Comments.scala*/ = 1 + def /* comment */ b/*Comments.scala*/ = 1 + var /* comment */ c/*Comments.scala*/ = 1 +} diff --git a/tests/unit/src/test/resources/definition/example/Companion.scala b/tests/unit/src/test/resources/definition/example/Companion.scala new file mode 100644 index 00000000000..bb169544958 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Companion.scala @@ -0,0 +1,5 @@ +package example + +abstract class Companion/*Companion.scala*/() extends Object/*Object.java*/() {} + +object Companion/*Companion.scala*/ {} diff --git a/tests/unit/src/test/resources/definition/example/Definitions.scala b/tests/unit/src/test/resources/definition/example/Definitions.scala new file mode 100644 index 00000000000..8076eaa7057 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Definitions.scala @@ -0,0 +1,15 @@ +package example + +class Definitions/*Definitions.scala*/ { + Predef/*Predef.scala*/.any2stringadd/*Predef.scala*/(1) + List/*List.scala*/[ + java.util.Map/*Map.java*/.Entry/*Map.java*/[ + java.lang.Integer/*Integer.java*/, + java.lang.Double/*Double.java*/ + ] + ]( + xs/**/ = null + ) + MacroAnnotation/*MacroAnnotation.scala*/.decodeMacroAnnotation/*MacroAnnotation.scala fallback to example.MacroAnnotation#*/ + MacroAnnotation/*MacroAnnotation.scala*/.encodeMacroAnnotation/*MacroAnnotation.scala fallback to example.MacroAnnotation#*/ +} diff --git a/tests/unit/src/test/resources/definition/example/ExampleSuite.scala b/tests/unit/src/test/resources/definition/example/ExampleSuite.scala new file mode 100644 index 00000000000..156f5f24d8a --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/ExampleSuite.scala @@ -0,0 +1,5 @@ +package example + +object ExampleSuite/*ExampleSuite.scala*/ { + println/*Predef.scala*/(NamedArguments/*NamedArguments.scala*/.user2/*NamedArguments.scala*/) +} diff --git a/tests/unit/src/test/resources/definition/example/ForComprehensions.scala b/tests/unit/src/test/resources/definition/example/ForComprehensions.scala new file mode 100644 index 00000000000..1b7a0d7e8aa --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/ForComprehensions.scala @@ -0,0 +1,40 @@ +package example + +class ForComprehensions/*ForComprehensions.scala*/ { + for { + a/*ForComprehensions.semanticdb*/ <- List/*List.scala*/(1) + b/*ForComprehensions.semanticdb*/ <- List/*List.scala*/(a/*ForComprehensions.semanticdb*/) + if ( + a/*ForComprehensions.semanticdb*/, + b/*ForComprehensions.semanticdb*/ + ) ==/*Object.java fallback to java.lang.Object#*/ (1, 2) + ( + c/*ForComprehensions.semanticdb*/, + d/*ForComprehensions.semanticdb*/ + ) <- List/*List.scala*/((a/*ForComprehensions.semanticdb*/, b/*no local definition*/)) + if ( + a/*ForComprehensions.semanticdb*/, + b/*no local definition*/, + c/*no local definition*/, + d/*no local definition*/ + ) ==/*Object.java fallback to java.lang.Object#*/ (1, 2, 3, 4) + e/*ForComprehensions.semanticdb*/ = ( + a/*ForComprehensions.semanticdb*/, + b/*no local definition*/, + c/*no local definition*/, + d/*no local definition*/ + ) + if e/*no local definition*/ ==/*Object.java fallback to java.lang.Object#*/ (1, 2, 3, 4) + f/*ForComprehensions.semanticdb*/ <- List/*List.scala*/(e/*no local definition*/) + } yield { + ( + a/*ForComprehensions.semanticdb*/, + b/*no local definition*/, + c/*no local definition*/, + d/*no local definition*/, + e/*no local definition*/, + f/*ForComprehensions.semanticdb*/ + ) + } + +} diff --git a/tests/unit/src/test/resources/definition/example/ImplicitClasses.scala b/tests/unit/src/test/resources/definition/example/ImplicitClasses.scala new file mode 100644 index 00000000000..319d4b259de --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/ImplicitClasses.scala @@ -0,0 +1,10 @@ +package example + +object ImplicitClasses/*ImplicitClasses.scala*/ { + implicit class Xtension/*ImplicitClasses.scala*/(number/*ImplicitClasses.scala*/: Int/*Int.scala*/) { + def increment/*ImplicitClasses.scala*/: Int/*Int.scala*/ = number/*ImplicitClasses.scala*/ +/*Int.scala*/ 1 + } + implicit class XtensionAnyVal/*ImplicitClasses.scala*/(private val number/*ImplicitClasses.scala*/: Int/*Int.scala*/) extends AnyVal/*AnyVal.scala*/ { + def double/*ImplicitClasses.scala*/: Int/*Int.scala*/ = number/*ImplicitClasses.scala*/ */*Int.scala*/ 2 + } +} diff --git a/tests/unit/src/test/resources/definition/example/ImplicitConversions.scala b/tests/unit/src/test/resources/definition/example/ImplicitConversions.scala new file mode 100644 index 00000000000..5445f9020d8 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/ImplicitConversions.scala @@ -0,0 +1,28 @@ +package example + +class ImplicitConversions/*ImplicitConversions.scala*/ { + implicit def string2Number/*ImplicitConversions.scala*/( + string/*ImplicitConversions.scala*/: String/*Predef.scala*/ + ): Int/*Int.scala*/ = 42 + val message/*ImplicitConversions.scala*/ = "" + val number/*ImplicitConversions.scala*/ = 42 + val tuple/*ImplicitConversions.scala*/ = (1, 2) + val char/*ImplicitConversions.scala*/: Char/*Char.scala*/ = 'a' + + // extension methods + message/*Predef.scala*/ + .stripSuffix/*StringLike.scala*/("h") + tuple/*Predef.scala*/ +/*Predef.scala*/ "Hello" + + // implicit conversions + val x/*ImplicitConversions.scala*/: Int/*Int.scala*/ = message/*ImplicitConversions.scala*/ + + // interpolators + s/*StringContext.scala*/"Hello $message/*ImplicitConversions.scala*/ $number/*ImplicitConversions.scala*/" + s/*Predef.scala*/"""Hello + |$message/*ImplicitConversions.scala*/ + |$number/*ImplicitConversions.scala*/""".stripMargin/*StringLike.scala*/ + + val a/*ImplicitConversions.scala*/: Int/*Int.scala*/ = char/*Char.scala*/ + val b/*ImplicitConversions.scala*/: Long/*Long.scala*/ = char/*Char.scala*/ +} diff --git a/tests/unit/src/test/resources/definition/example/Imports.scala b/tests/unit/src/test/resources/definition/example/Imports.scala new file mode 100644 index 00000000000..1051747d19f --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Imports.scala @@ -0,0 +1,10 @@ +package example + +import util.{Failure/*Try.scala*/ => NotGood/**/} +import math.{floor/*package.scala*/ => _, _} + +class Imports/*Imports.scala*/ { + // rename reference + NotGood/*Try.scala*/(null) + max/*package.scala*/(1, 2) +} diff --git a/tests/unit/src/test/resources/definition/example/JavaThenScala.scala b/tests/unit/src/test/resources/definition/example/JavaThenScala.scala new file mode 100644 index 00000000000..3c18c28daaf --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/JavaThenScala.scala @@ -0,0 +1,5 @@ +package example + +class JavaThenScala/*JavaThenScala.scala*/ { + new JavaClass/*JavaClass.java*/(42) +} diff --git a/tests/unit/src/test/resources/definition/example/Locals.scala b/tests/unit/src/test/resources/definition/example/Locals.scala new file mode 100644 index 00000000000..d6241062cc8 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Locals.scala @@ -0,0 +1,8 @@ +package example + +class Locals/*Locals.scala*/ { + { + val x/*Locals.semanticdb*/ = 2 + x/*Locals.semanticdb*/ +/*Int.scala*/ 2 + } +} diff --git a/tests/unit/src/test/resources/definition/example/MacroAnnotation.scala b/tests/unit/src/test/resources/definition/example/MacroAnnotation.scala new file mode 100644 index 00000000000..1cccded5612 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/MacroAnnotation.scala @@ -0,0 +1,19 @@ +package example + +import io.circe.derivation.annotations.JsonCodec/*JsonCodec.scala*/ + +@JsonCodec/*MacroAnnotation.scala*/ +// FIXME: https://github.com/scalameta/scalameta/issues/1789 +case class MacroAnnotation/**/( + name/*MacroAnnotation.scala*/: String/*Predef.scala*/ +) { + def method/*MacroAnnotation.scala*/ = 42 +} + +object MacroAnnotations/*MacroAnnotation.scala*/ { + import scala.meta._ + // IntelliJ has never managed to goto definition for the inner classes from Trees.scala + // due to the macro annotations. + val x/*MacroAnnotation.scala*/: Defn/*Trees.scala*/.Class/*Trees.scala*/ = Defn/*Trees.scala*/.Class/*Trees.scala*/(null, null, null, null, null) + val y/*MacroAnnotation.scala*/: Mod/*Trees.scala*/.Final/*Trees.scala*/ = Mod/*Trees.scala*/.Final/*Trees.scala*/() +} diff --git a/tests/unit/src/test/resources/definition/example/MethodOverload.scala b/tests/unit/src/test/resources/definition/example/MethodOverload.scala new file mode 100644 index 00000000000..64b6547c13e --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/MethodOverload.scala @@ -0,0 +1,9 @@ +package example + +class MethodOverload/*MethodOverload.scala*/(b/*MethodOverload.scala*/: String/*Predef.scala*/) { + def this/*unexpected: example.MethodOverload#``(+1).*/() = this("") + def this/*unexpected: example.MethodOverload#``(+2).*/(c/*MethodOverload.scala*/: Int/*Int.scala*/) = this("") + val a/*MethodOverload.scala*/ = 2 + def a/*MethodOverload.scala*/(x/*MethodOverload.scala*/: Int/*Int.scala*/) = 2 + def a/*MethodOverload.scala*/(x/*MethodOverload.scala*/: Int/*Int.scala*/, y/*MethodOverload.scala*/: Int/*Int.scala*/) = 2 +} diff --git a/tests/unit/src/test/resources/definition/example/Miscellaneous.scala b/tests/unit/src/test/resources/definition/example/Miscellaneous.scala new file mode 100644 index 00000000000..8dac15b0a49 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Miscellaneous.scala @@ -0,0 +1,12 @@ +package example + +class Miscellaneous/*Miscellaneous.scala*/ { + // backtick identifier + val `a b`/*Miscellaneous.scala*/ = 42 + + // infix + inferred apply/implicits/tparams + (List/*List.scala*/(1) + .map/*List.scala*/(_ +/*Int.scala*/ 1) + ++/*List.scala*/ + List/*List.scala*/(3)) +} diff --git a/tests/unit/src/test/resources/definition/example/NamedArguments.scala b/tests/unit/src/test/resources/definition/example/NamedArguments.scala new file mode 100644 index 00000000000..4d9cdca669a --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/NamedArguments.scala @@ -0,0 +1,35 @@ +package example + +case class User/*NamedArguments.scala*/( + name/*NamedArguments.scala*/: String/*Predef.scala*/ = { + // assert default values have occurrences + Map/*Predef.scala*/.toString/*Object.java*/ + } +) +object NamedArguments/*NamedArguments.scala*/ { + val susan/*NamedArguments.scala*/ = "Susan" + val user1/*NamedArguments.scala*/ = + User/*NamedArguments.scala*/ + .apply/*NamedArguments.scala fallback to example.User#*/( + name/*NamedArguments.scala fallback to example.User#name.*/ = "John" + ) + val user2/*NamedArguments.scala*/: User/*NamedArguments.scala*/ = + User/*NamedArguments.scala*/( + // FIXME: https://github.com/scalameta/scalameta/issues/1787 + name/**/ = susan/*NamedArguments.scala*/ + ).copy/*NamedArguments.scala fallback to example.User#*/( + name/*NamedArguments.scala fallback to example.User#name.*/ = susan/*NamedArguments.scala*/ + ) + + // anonymous classes + @deprecated/*deprecated.scala*/( + message/**/ = "a", + since/**/ = susan/*NamedArguments.scala*/ + ) def b/*NamedArguments.scala*/ = 1 + + // vararg + List/*List.scala*/( + xs/**/ = 2 + ) + +} diff --git a/tests/unit/src/test/resources/definition/example/PatternMatching.scala b/tests/unit/src/test/resources/definition/example/PatternMatching.scala new file mode 100644 index 00000000000..cd769da7a70 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/PatternMatching.scala @@ -0,0 +1,24 @@ +package example + +class PatternMatching/*PatternMatching.scala*/ { + val some/*PatternMatching.scala*/ = Some/*Option.scala*/(1) + some/*PatternMatching.scala*/ match { + case Some/*Option.scala*/(number/*PatternMatching.semanticdb*/) => + number/*PatternMatching.semanticdb*/ + } + + // tuple deconstruction + val (left/*PatternMatching.semanticdb*/, right/*PatternMatching.semanticdb*/) = (1, 2) + (left/*PatternMatching.scala*/, right/*PatternMatching.scala*/) + + // val deconstruction + val Some/*Option.scala*/(number1/*PatternMatching.semanticdb*/) = + some/*PatternMatching.scala*/ + number1/*PatternMatching.scala*/ + + def localDeconstruction/*PatternMatching.scala*/ = { + val Some/*Option.scala*/(number2/*PatternMatching.semanticdb*/) = + some/*PatternMatching.scala*/ + number2/*no local definition*/ + } +} diff --git a/tests/unit/src/test/resources/definition/example/ReflectiveInvocation.scala b/tests/unit/src/test/resources/definition/example/ReflectiveInvocation.scala new file mode 100644 index 00000000000..c1ae59fa7c5 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/ReflectiveInvocation.scala @@ -0,0 +1,9 @@ +package example + +class ReflectiveInvocation/*ReflectiveInvocation.scala*/ { + new Serializable/*Serializable.scala*/ { + def message/*ReflectiveInvocation.semanticdb*/ = "message" + // reflective invocation + }.message/*ReflectiveInvocation.semanticdb*/ + +} diff --git a/tests/unit/src/test/resources/definition/example/Scalalib.scala b/tests/unit/src/test/resources/definition/example/Scalalib.scala new file mode 100644 index 00000000000..144004aefee --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/Scalalib.scala @@ -0,0 +1,25 @@ +package example + +class Scalalib/*Scalalib.scala*/ { + val lst/*Scalalib.scala*/ = List/*List.scala*/[ + ( + Nothing, + Null, + Singleton, + Any, + AnyRef, + AnyVal/*AnyVal.scala*/, + Int/*Int.scala*/, + Short/*Short.scala*/, + Double/*Double.scala*/, + Float/*Float.scala*/, + Char/*Char.scala*/ + ) + ]() + lst/*Scalalib.scala*/.isInstanceOf[Any] + lst/*Scalalib.scala*/.asInstanceOf[Any] + println/*Predef.scala*/(lst/*Scalalib.scala*/.##/*Object.java fallback to java.lang.Object#*/) + lst/*Scalalib.scala*/ ne/*Object.java fallback to java.lang.Object#*/ lst/*Scalalib.scala*/ + lst/*Scalalib.scala*/ eq/*Object.java fallback to java.lang.Object#*/ lst/*Scalalib.scala*/ + lst/*Scalalib.scala*/ ==/*Object.java fallback to java.lang.Object#*/ lst/*Scalalib.scala*/ +} diff --git a/tests/unit/src/test/resources/definition/example/StructuralTypes.scala b/tests/unit/src/test/resources/definition/example/StructuralTypes.scala new file mode 100644 index 00000000000..184f3733bc1 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/StructuralTypes.scala @@ -0,0 +1,19 @@ +package example + +object StructuralTypes/*StructuralTypes.scala*/ { + type User/*StructuralTypes.scala*/ = { + def name/*StructuralTypes.semanticdb*/: String/*Predef.scala*/ + def age/*StructuralTypes.semanticdb*/: Int/*Int.scala*/ + } + + val user/*StructuralTypes.scala*/ = null.asInstanceOf[User/*StructuralTypes.scala*/] + user/*StructuralTypes.scala*/.name/**/ + user/*StructuralTypes.scala*/.age/**/ + + val V/*StructuralTypes.scala*/: Object/*Object.java*/ { + def scalameta/*StructuralTypes.semanticdb*/: String/*Predef.scala*/ + } = new { + def scalameta/*StructuralTypes.semanticdb*/ = "4.0" + } + V/*StructuralTypes.scala*/.scalameta/**/ +} diff --git a/tests/unit/src/test/resources/definition/example/TypeParameters.scala b/tests/unit/src/test/resources/definition/example/TypeParameters.scala new file mode 100644 index 00000000000..bc0b7463f9b --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/TypeParameters.scala @@ -0,0 +1,8 @@ +package example + +class TypeParameters/*TypeParameters.scala*/[A/*TypeParameters.scala*/] { + def method/*TypeParameters.scala*/[B/*TypeParameters.scala*/] = 42 + trait TraitParameter/*TypeParameters.scala*/[C/*TypeParameters.scala*/] + type AbstractTypeAlias/*TypeParameters.scala*/[D/*TypeParameters.scala*/] + type TypeAlias/*TypeParameters.scala*/[E/*TypeParameters.scala*/] = List/*package.scala*/[E/*TypeParameters.scala*/] +} diff --git a/tests/unit/src/test/resources/definition/example/nested/DoublePackage.scala b/tests/unit/src/test/resources/definition/example/nested/DoublePackage.scala new file mode 100644 index 00000000000..8b12c3c3cf3 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/nested/DoublePackage.scala @@ -0,0 +1,9 @@ +package example + +package nested.x { + class DoublePackage/*DoublePackage.scala*/ {} +} + +package nested2.y { + class DoublePackage/*DoublePackage.scala*/ {} +} diff --git a/tests/unit/src/test/resources/definition/example/nested/ExampleNested.scala b/tests/unit/src/test/resources/definition/example/nested/ExampleNested.scala new file mode 100644 index 00000000000..b527e29de28 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/nested/ExampleNested.scala @@ -0,0 +1,3 @@ +package example.nested + +class ExampleNested/*ExampleNested.scala*/ {} diff --git a/tests/unit/src/test/resources/definition/example/nested/package.scala b/tests/unit/src/test/resources/definition/example/nested/package.scala new file mode 100644 index 00000000000..c521306fa2b --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/nested/package.scala @@ -0,0 +1,9 @@ +package example + +package object nested/*package.scala*/ { + + class PackageObjectNestedClass/*package.scala*/ + +} + +class PackageObjectSibling/*package.scala*/ diff --git a/tests/unit/src/test/resources/definition/example/package.scala b/tests/unit/src/test/resources/definition/example/package.scala new file mode 100644 index 00000000000..3528196976e --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/package.scala @@ -0,0 +1,5 @@ +package object example/*package.scala*/ { + + class PackageObjectClass/*package.scala*/ + +} diff --git a/tests/unit/src/test/resources/definition/example/type/Backtick.scala b/tests/unit/src/test/resources/definition/example/type/Backtick.scala new file mode 100644 index 00000000000..11ac432be92 --- /dev/null +++ b/tests/unit/src/test/resources/definition/example/type/Backtick.scala @@ -0,0 +1,3 @@ +package example.`type` + +class Backtick/*Backtick.scala*/ {} diff --git a/tests/unit/src/test/resources/expect/toplevels.expect b/tests/unit/src/test/resources/expect/toplevels.expect new file mode 100644 index 00000000000..0c27c192f7f --- /dev/null +++ b/tests/unit/src/test/resources/expect/toplevels.expect @@ -0,0 +1,34 @@ +example/AnonymousClasses.scala -> example/AnonymousClasses# +example/Comments.scala -> example/Comments# +example/Companion.scala -> example/Companion# +example/Companion.scala -> example/Companion. +example/Definitions.scala -> example/Definitions# +example/ExampleSuite.scala -> example/ExampleSuite. +example/ForComprehensions.scala -> example/ForComprehensions# +example/ImplicitClasses.scala -> example/ImplicitClasses. +example/ImplicitConversions.scala -> example/ImplicitConversions# +example/Imports.scala -> example/Imports# +example/JavaClass.java -> example/JavaClass# +example/JavaEnum.java -> example/JavaEnum# +example/JavaInterface.java -> example/JavaInterface# +example/JavaOverloading.java -> example/JavaOverloading# +example/JavaThenScala.scala -> example/JavaThenScala# +example/Locals.scala -> example/Locals# +example/MacroAnnotation.scala -> example/MacroAnnotation# +example/MacroAnnotation.scala -> example/MacroAnnotations. +example/MethodOverload.scala -> example/MethodOverload# +example/Miscellaneous.scala -> example/Miscellaneous# +example/NamedArguments.scala -> example/NamedArguments. +example/NamedArguments.scala -> example/User# +example/PatternMatching.scala -> example/PatternMatching# +example/ReflectiveInvocation.scala -> example/ReflectiveInvocation# +example/Scalalib.scala -> example/Scalalib# +example/StructuralTypes.scala -> example/StructuralTypes. +example/TypeParameters.scala -> example/TypeParameters# +example/nested/DoublePackage.scala -> example/nested/x/DoublePackage# +example/nested/DoublePackage.scala -> example/nested2/y/DoublePackage# +example/nested/ExampleNested.scala -> example/nested/ExampleNested# +example/nested/package.scala -> example/PackageObjectSibling# +example/nested/package.scala -> example/nested/package. +example/package.scala -> example/package. +example/type/Backtick.scala -> example/type/Backtick# \ No newline at end of file diff --git a/tests/unit/src/test/resources/mtags/example/AnonymousClasses.scala b/tests/unit/src/test/resources/mtags/example/AnonymousClasses.scala new file mode 100644 index 00000000000..ab1da8bfbd9 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/AnonymousClasses.scala @@ -0,0 +1,3 @@ +package example + +class AnonymousClasses/*example.AnonymousClasses#*/ {} diff --git a/tests/unit/src/test/resources/mtags/example/Comments.scala b/tests/unit/src/test/resources/mtags/example/Comments.scala new file mode 100644 index 00000000000..10757b6c3d9 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Comments.scala @@ -0,0 +1,9 @@ +package example + +class /* comment */ Comments/*example.Comments#*/ { + object /* comment */ A/*example.Comments#A.*/ + trait /* comment */ A/*example.Comments#A#*/ + val /* comment */ a/*example.Comments#a.*/ = 1 + def /* comment */ b/*example.Comments#b().*/ = 1 + var /* comment */ c/*example.Comments#c().*/ = 1 +} diff --git a/tests/unit/src/test/resources/mtags/example/Companion.scala b/tests/unit/src/test/resources/mtags/example/Companion.scala new file mode 100644 index 00000000000..23f274c164c --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Companion.scala @@ -0,0 +1,5 @@ +package example + +abstract class Companion/*example.Companion#*/() extends Object() {} + +object Companion/*example.Companion.*/ {} diff --git a/tests/unit/src/test/resources/mtags/example/Definitions.scala b/tests/unit/src/test/resources/mtags/example/Definitions.scala new file mode 100644 index 00000000000..9178cc88362 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Definitions.scala @@ -0,0 +1,15 @@ +package example + +class Definitions/*example.Definitions#*/ { + Predef.any2stringadd(1) + List[ + java.util.Map.Entry[ + java.lang.Integer, + java.lang.Double + ] + ]( + xs = null + ) + MacroAnnotation.decodeMacroAnnotation + MacroAnnotation.encodeMacroAnnotation +} diff --git a/tests/unit/src/test/resources/mtags/example/ExampleSuite.scala b/tests/unit/src/test/resources/mtags/example/ExampleSuite.scala new file mode 100644 index 00000000000..9119afc6c65 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/ExampleSuite.scala @@ -0,0 +1,5 @@ +package example + +object ExampleSuite/*example.ExampleSuite.*/ { + println(NamedArguments.user2) +} diff --git a/tests/unit/src/test/resources/mtags/example/ForComprehensions.scala b/tests/unit/src/test/resources/mtags/example/ForComprehensions.scala new file mode 100644 index 00000000000..ccc394afb0b --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/ForComprehensions.scala @@ -0,0 +1,40 @@ +package example + +class ForComprehensions/*example.ForComprehensions#*/ { + for { + a <- List(1) + b <- List(a) + if ( + a, + b + ) == (1, 2) + ( + c, + d + ) <- List((a, b)) + if ( + a, + b, + c, + d + ) == (1, 2, 3, 4) + e = ( + a, + b, + c, + d + ) + if e == (1, 2, 3, 4) + f <- List(e) + } yield { + ( + a, + b, + c, + d, + e, + f + ) + } + +} diff --git a/tests/unit/src/test/resources/mtags/example/ImplicitClasses.scala b/tests/unit/src/test/resources/mtags/example/ImplicitClasses.scala new file mode 100644 index 00000000000..866d57697b7 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/ImplicitClasses.scala @@ -0,0 +1,10 @@ +package example + +object ImplicitClasses/*example.ImplicitClasses.*/ { + implicit class Xtension/*example.ImplicitClasses.Xtension().*//*example.ImplicitClasses.Xtension#*/(number/*example.ImplicitClasses.Xtension#number.*/: Int) { + def increment/*example.ImplicitClasses.Xtension#increment().*/: Int = number + 1 + } + implicit class XtensionAnyVal/*example.ImplicitClasses.XtensionAnyVal().*//*example.ImplicitClasses.XtensionAnyVal#*/(private val number/*example.ImplicitClasses.XtensionAnyVal#number.*/: Int) extends AnyVal { + def double/*example.ImplicitClasses.XtensionAnyVal#double().*/: Int = number * 2 + } +} diff --git a/tests/unit/src/test/resources/mtags/example/ImplicitConversions.scala b/tests/unit/src/test/resources/mtags/example/ImplicitConversions.scala new file mode 100644 index 00000000000..b07efc4bc2f --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/ImplicitConversions.scala @@ -0,0 +1,28 @@ +package example + +class ImplicitConversions/*example.ImplicitConversions#*/ { + implicit def string2Number/*example.ImplicitConversions#string2Number().*/( + string/*example.ImplicitConversions#string2Number().(string)*/: String + ): Int = 42 + val message/*example.ImplicitConversions#message.*/ = "" + val number/*example.ImplicitConversions#number.*/ = 42 + val tuple/*example.ImplicitConversions#tuple.*/ = (1, 2) + val char/*example.ImplicitConversions#char.*/: Char = 'a' + + // extension methods + message + .stripSuffix("h") + tuple + "Hello" + + // implicit conversions + val x/*example.ImplicitConversions#x.*/: Int = message + + // interpolators + s"Hello $message $number" + s"""Hello + |$message + |$number""".stripMargin + + val a/*example.ImplicitConversions#a.*/: Int = char + val b/*example.ImplicitConversions#b.*/: Long = char +} diff --git a/tests/unit/src/test/resources/mtags/example/Imports.scala b/tests/unit/src/test/resources/mtags/example/Imports.scala new file mode 100644 index 00000000000..fc2a19b63da --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Imports.scala @@ -0,0 +1,10 @@ +package example + +import util.{Failure => NotGood} +import math.{floor => _, _} + +class Imports/*example.Imports#*/ { + // rename reference + NotGood(null) + max(1, 2) +} diff --git a/tests/unit/src/test/resources/mtags/example/JavaClass.java b/tests/unit/src/test/resources/mtags/example/JavaClass.java new file mode 100644 index 00000000000..20d84541171 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/JavaClass.java @@ -0,0 +1,56 @@ +package example; + +public class JavaClass/*example.JavaClass#*/ { + + public JavaClass/*example.JavaClass#``().*/(int d) { + this.d = d; + } + + public static void a/*example.JavaClass#a().*/() { + } + + public int b/*example.JavaClass#b().*/() { + return 1; + } + + public static int c/*example.JavaClass#c.*/ = 2; + public int d/*example.JavaClass#d.*/ = 2; + + public class InnerClass/*example.JavaClass#InnerClass#*/ { + public int b/*example.JavaClass#InnerClass#b().*/() { + return 1; + } + + public int d/*example.JavaClass#InnerClass#d.*/ = 2; + } + + public static class InnerStaticClass/*example.JavaClass#InnerStaticClass#*/ { + public static void a/*example.JavaClass#InnerStaticClass#a().*/() { + } + + public int b/*example.JavaClass#InnerStaticClass#b().*/() { + return 1; + } + + public static int c/*example.JavaClass#InnerStaticClass#c.*/ = 2; + public int d/*example.JavaClass#InnerStaticClass#d.*/ = 2; + } + + public interface InnerInterface/*example.JavaClass#InnerInterface#*/ { + public static void a/*example.JavaClass#InnerInterface#a().*/() { + } + + public int b/*example.JavaClass#InnerInterface#b().*/(); + } + + public String publicName/*example.JavaClass#publicName().*/() { + return "name"; + } + + // Weird formatting + @Override + public String + toString/*example.JavaClass#toString().*/() { + return ""; + } +} diff --git a/tests/unit/src/test/resources/mtags/example/JavaEnum.java b/tests/unit/src/test/resources/mtags/example/JavaEnum.java new file mode 100644 index 00000000000..e385d7ebefb --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/JavaEnum.java @@ -0,0 +1,44 @@ +package example; + +public enum JavaEnum/*example.JavaEnum#*/ { + A/*example.JavaEnum#A.*/(1), + B/*example.JavaEnum#B.*/(2); + + + JavaEnum/*example.JavaEnum#``().*/(int d) { + this.d = d; + } + + public static void a/*example.JavaEnum#a().*/() { + } + + public int b/*example.JavaEnum#b().*/() { + return 1; + } + + ; + public static int c/*example.JavaEnum#c.*/ = 2; + public int d/*example.JavaEnum#d.*/ = 2; + + public class C/*example.JavaEnum#C#*/ { + public int b/*example.JavaEnum#C#b().*/() { + return 1; + } + + ; + public int d/*example.JavaEnum#C#d.*/ = 2; + } + + public static class F/*example.JavaEnum#F#*/ { + public static void a/*example.JavaEnum#F#a().*/() { + } + + public int b/*example.JavaEnum#F#b().*/() { + return 1; + } + + ; + public static int c/*example.JavaEnum#F#c.*/ = 2; + public int d/*example.JavaEnum#F#d.*/ = 2; + } +} diff --git a/tests/unit/src/test/resources/mtags/example/JavaInterface.java b/tests/unit/src/test/resources/mtags/example/JavaInterface.java new file mode 100644 index 00000000000..d879ea25725 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/JavaInterface.java @@ -0,0 +1,8 @@ +package example; + +public interface JavaInterface/*example.JavaInterface#*/ { + public static void a/*example.JavaInterface#a().*/() { + } + + public int b/*example.JavaInterface#b().*/(); +} \ No newline at end of file diff --git a/tests/unit/src/test/resources/mtags/example/JavaOverloading.java b/tests/unit/src/test/resources/mtags/example/JavaOverloading.java new file mode 100644 index 00000000000..16b7cfb3d13 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/JavaOverloading.java @@ -0,0 +1,8 @@ +package example; + +public class JavaOverloading/*example.JavaOverloading#*/ { + public int name/*example.JavaOverloading#name.*/ = 1; + public static int name/*example.JavaOverloading#name(+2).*/(String name) { return name.length(); } + public int name/*example.JavaOverloading#name().*/() { return 1; } + public int name/*example.JavaOverloading#name(+1).*/(int n) { return n; } +} diff --git a/tests/unit/src/test/resources/mtags/example/JavaThenScala.scala b/tests/unit/src/test/resources/mtags/example/JavaThenScala.scala new file mode 100644 index 00000000000..fdc212f55fc --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/JavaThenScala.scala @@ -0,0 +1,5 @@ +package example + +class JavaThenScala/*example.JavaThenScala#*/ { + new JavaClass(42) +} diff --git a/tests/unit/src/test/resources/mtags/example/Locals.scala b/tests/unit/src/test/resources/mtags/example/Locals.scala new file mode 100644 index 00000000000..d5f01bf548a --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Locals.scala @@ -0,0 +1,8 @@ +package example + +class Locals/*example.Locals#*/ { + { + val x = 2 + x + 2 + } +} diff --git a/tests/unit/src/test/resources/mtags/example/MacroAnnotation.scala b/tests/unit/src/test/resources/mtags/example/MacroAnnotation.scala new file mode 100644 index 00000000000..dc93440b0d7 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/MacroAnnotation.scala @@ -0,0 +1,19 @@ +package example + +import io.circe.derivation.annotations.JsonCodec + +@JsonCodec +// FIXME: https://github.com/scalameta/scalameta/issues/1789 +case class MacroAnnotation/*example.MacroAnnotation#*/( + name/*example.MacroAnnotation#name.*/: String +) { + def method/*example.MacroAnnotation#method().*/ = 42 +} + +object MacroAnnotations/*example.MacroAnnotations.*/ { + import scala.meta._ + // IntelliJ has never managed to goto definition for the inner classes from Trees.scala + // due to the macro annotations. + val x/*example.MacroAnnotations.x.*/: Defn.Class = Defn.Class(null, null, null, null, null) + val y/*example.MacroAnnotations.y.*/: Mod.Final = Mod.Final() +} diff --git a/tests/unit/src/test/resources/mtags/example/MethodOverload.scala b/tests/unit/src/test/resources/mtags/example/MethodOverload.scala new file mode 100644 index 00000000000..fb920ffde64 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/MethodOverload.scala @@ -0,0 +1,9 @@ +package example + +class MethodOverload/*example.MethodOverload#*/(b/*example.MethodOverload#b.*/: String) { + def this/*example.MethodOverload#``(+1).*/() = this("") + def this/*example.MethodOverload#``(+2).*/(c/*example.MethodOverload#``(+2).(c)*/: Int) = this("") + val a/*example.MethodOverload#a.*/ = 2 + def a/*example.MethodOverload#a().*/(x/*example.MethodOverload#a().(x)*/: Int) = 2 + def a/*example.MethodOverload#a(+1).*/(x/*example.MethodOverload#a(+1).(x)*/: Int, y/*example.MethodOverload#a(+1).(y)*/: Int) = 2 +} diff --git a/tests/unit/src/test/resources/mtags/example/Miscellaneous.scala b/tests/unit/src/test/resources/mtags/example/Miscellaneous.scala new file mode 100644 index 00000000000..6593d928537 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Miscellaneous.scala @@ -0,0 +1,12 @@ +package example + +class Miscellaneous/*example.Miscellaneous#*/ { + // backtick identifier + val `a b`/*example.Miscellaneous#`a b`.*/ = 42 + + // infix + inferred apply/implicits/tparams + (List(1) + .map(_ + 1) + ++ + List(3)) +} diff --git a/tests/unit/src/test/resources/mtags/example/NamedArguments.scala b/tests/unit/src/test/resources/mtags/example/NamedArguments.scala new file mode 100644 index 00000000000..af044cf8480 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/NamedArguments.scala @@ -0,0 +1,35 @@ +package example + +case class User/*example.User#*/( + name/*example.User#name.*/: String = { + // assert default values have occurrences + Map.toString + } +) +object NamedArguments/*example.NamedArguments.*/ { + val susan/*example.NamedArguments.susan.*/ = "Susan" + val user1/*example.NamedArguments.user1.*/ = + User + .apply( + name = "John" + ) + val user2/*example.NamedArguments.user2.*/: User = + User( + // FIXME: https://github.com/scalameta/scalameta/issues/1787 + name = susan + ).copy( + name = susan + ) + + // anonymous classes + @deprecated( + message = "a", + since = susan + ) def b/*example.NamedArguments.b().*/ = 1 + + // vararg + List( + xs = 2 + ) + +} diff --git a/tests/unit/src/test/resources/mtags/example/PatternMatching.scala b/tests/unit/src/test/resources/mtags/example/PatternMatching.scala new file mode 100644 index 00000000000..ca7a399af75 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/PatternMatching.scala @@ -0,0 +1,24 @@ +package example + +class PatternMatching/*example.PatternMatching#*/ { + val some/*example.PatternMatching#some.*/ = Some(1) + some match { + case Some(number) => + number + } + + // tuple deconstruction + val (left/*example.PatternMatching#left.*/, right/*example.PatternMatching#right.*/) = (1, 2) + (left, right) + + // val deconstruction + val Some(number1/*example.PatternMatching#number1.*/) = + some + number1 + + def localDeconstruction/*example.PatternMatching#localDeconstruction().*/ = { + val Some(number2) = + some + number2 + } +} diff --git a/tests/unit/src/test/resources/mtags/example/ReflectiveInvocation.scala b/tests/unit/src/test/resources/mtags/example/ReflectiveInvocation.scala new file mode 100644 index 00000000000..9413795110e --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/ReflectiveInvocation.scala @@ -0,0 +1,9 @@ +package example + +class ReflectiveInvocation/*example.ReflectiveInvocation#*/ { + new Serializable { + def message = "message" + // reflective invocation + }.message + +} diff --git a/tests/unit/src/test/resources/mtags/example/Scalalib.scala b/tests/unit/src/test/resources/mtags/example/Scalalib.scala new file mode 100644 index 00000000000..c9f4c8eb9a4 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/Scalalib.scala @@ -0,0 +1,25 @@ +package example + +class Scalalib/*example.Scalalib#*/ { + val lst/*example.Scalalib#lst.*/ = List[ + ( + Nothing, + Null, + Singleton, + Any, + AnyRef, + AnyVal, + Int, + Short, + Double, + Float, + Char + ) + ]() + lst.isInstanceOf[Any] + lst.asInstanceOf[Any] + println(lst.##) + lst ne lst + lst eq lst + lst == lst +} diff --git a/tests/unit/src/test/resources/mtags/example/StructuralTypes.scala b/tests/unit/src/test/resources/mtags/example/StructuralTypes.scala new file mode 100644 index 00000000000..12152d4a896 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/StructuralTypes.scala @@ -0,0 +1,19 @@ +package example + +object StructuralTypes/*example.StructuralTypes.*/ { + type User/*example.StructuralTypes.User#*/ = { + def name: String + def age: Int + } + + val user/*example.StructuralTypes.user.*/ = null.asInstanceOf[User] + user.name + user.age + + val V/*example.StructuralTypes.V.*/: Object { + def scalameta: String + } = new { + def scalameta = "4.0" + } + V.scalameta +} diff --git a/tests/unit/src/test/resources/mtags/example/TypeParameters.scala b/tests/unit/src/test/resources/mtags/example/TypeParameters.scala new file mode 100644 index 00000000000..bc1f3bbae77 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/TypeParameters.scala @@ -0,0 +1,8 @@ +package example + +class TypeParameters/*example.TypeParameters#*/[A/*example.TypeParameters#[A]*/] { + def method/*example.TypeParameters#method().*/[B/*example.TypeParameters#method().[B]*/] = 42 + trait TraitParameter/*example.TypeParameters#TraitParameter#*/[C/*example.TypeParameters#TraitParameter#[C]*/] + type AbstractTypeAlias/*example.TypeParameters#AbstractTypeAlias#*/[D/*example.TypeParameters#AbstractTypeAlias#[D]*/] + type TypeAlias/*example.TypeParameters#TypeAlias#*/[E/*example.TypeParameters#TypeAlias#[E]*/] = List[E] +} diff --git a/tests/unit/src/test/resources/mtags/example/nested/DoublePackage.scala b/tests/unit/src/test/resources/mtags/example/nested/DoublePackage.scala new file mode 100644 index 00000000000..353bff71134 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/nested/DoublePackage.scala @@ -0,0 +1,9 @@ +package example + +package nested.x { + class DoublePackage/*example.nested.x.DoublePackage#*/ {} +} + +package nested2.y { + class DoublePackage/*example.nested2.y.DoublePackage#*/ {} +} diff --git a/tests/unit/src/test/resources/mtags/example/nested/ExampleNested.scala b/tests/unit/src/test/resources/mtags/example/nested/ExampleNested.scala new file mode 100644 index 00000000000..0fe5be3276e --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/nested/ExampleNested.scala @@ -0,0 +1,3 @@ +package example.nested + +class ExampleNested/*example.nested.ExampleNested#*/ {} diff --git a/tests/unit/src/test/resources/mtags/example/nested/package.scala b/tests/unit/src/test/resources/mtags/example/nested/package.scala new file mode 100644 index 00000000000..3efb637379f --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/nested/package.scala @@ -0,0 +1,9 @@ +package example + +package object nested/*example.nested.package.*/ { + + class PackageObjectNestedClass/*example.nested.package.PackageObjectNestedClass#*/ + +} + +class PackageObjectSibling/*example.PackageObjectSibling#*/ diff --git a/tests/unit/src/test/resources/mtags/example/package.scala b/tests/unit/src/test/resources/mtags/example/package.scala new file mode 100644 index 00000000000..61edd53edb1 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/package.scala @@ -0,0 +1,5 @@ +package object example/*example.package.*/ { + + class PackageObjectClass/*example.package.PackageObjectClass#*/ + +} diff --git a/tests/unit/src/test/resources/mtags/example/type/Backtick.scala b/tests/unit/src/test/resources/mtags/example/type/Backtick.scala new file mode 100644 index 00000000000..ec3c398e4f0 --- /dev/null +++ b/tests/unit/src/test/resources/mtags/example/type/Backtick.scala @@ -0,0 +1,3 @@ +package example.`type` + +class Backtick/*example.type.Backtick#*/ {} diff --git a/tests/unit/src/test/resources/semanticdb/example/AnonymousClasses.scala b/tests/unit/src/test/resources/semanticdb/example/AnonymousClasses.scala new file mode 100644 index 00000000000..ab1da8bfbd9 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/AnonymousClasses.scala @@ -0,0 +1,3 @@ +package example + +class AnonymousClasses/*example.AnonymousClasses#*/ {} diff --git a/tests/unit/src/test/resources/semanticdb/example/Comments.scala b/tests/unit/src/test/resources/semanticdb/example/Comments.scala new file mode 100644 index 00000000000..10757b6c3d9 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Comments.scala @@ -0,0 +1,9 @@ +package example + +class /* comment */ Comments/*example.Comments#*/ { + object /* comment */ A/*example.Comments#A.*/ + trait /* comment */ A/*example.Comments#A#*/ + val /* comment */ a/*example.Comments#a.*/ = 1 + def /* comment */ b/*example.Comments#b().*/ = 1 + var /* comment */ c/*example.Comments#c().*/ = 1 +} diff --git a/tests/unit/src/test/resources/semanticdb/example/Companion.scala b/tests/unit/src/test/resources/semanticdb/example/Companion.scala new file mode 100644 index 00000000000..db3c061fb48 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Companion.scala @@ -0,0 +1,5 @@ +package example + +abstract class Companion/*example.Companion#*/() extends Object/*java.lang.Object#*//*java.lang.Object#``().*/() {} + +object Companion/*example.Companion.*/ {} diff --git a/tests/unit/src/test/resources/semanticdb/example/Definitions.scala b/tests/unit/src/test/resources/semanticdb/example/Definitions.scala new file mode 100644 index 00000000000..d3bacf23998 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Definitions.scala @@ -0,0 +1,15 @@ +package example + +class Definitions/*example.Definitions#*/ { + Predef/*scala.Predef.*/.any2stringadd/*scala.Predef.any2stringadd().*/(1) + List/*scala.collection.immutable.List.*/[ + java.util.Map/*java.util.Map#*/.Entry/*java.util.Map#Entry#*/[ + java.lang.Integer/*java.lang.Integer#*/, + java.lang.Double/*java.lang.Double#*/ + ] + ]( + xs = null + ) + MacroAnnotation/*example.MacroAnnotation.*/.decodeMacroAnnotation/*example.MacroAnnotation.decodeMacroAnnotation.*/ + MacroAnnotation/*example.MacroAnnotation.*/.encodeMacroAnnotation/*example.MacroAnnotation.encodeMacroAnnotation.*/ +} diff --git a/tests/unit/src/test/resources/semanticdb/example/ExampleSuite.scala b/tests/unit/src/test/resources/semanticdb/example/ExampleSuite.scala new file mode 100644 index 00000000000..faa2193a18b --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/ExampleSuite.scala @@ -0,0 +1,5 @@ +package example + +object ExampleSuite/*example.ExampleSuite.*/ { + println/*scala.Predef.println(+1).*/(NamedArguments/*example.NamedArguments.*/.user2/*example.NamedArguments.user2.*/) +} diff --git a/tests/unit/src/test/resources/semanticdb/example/ForComprehensions.scala b/tests/unit/src/test/resources/semanticdb/example/ForComprehensions.scala new file mode 100644 index 00000000000..4f0790f131e --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/ForComprehensions.scala @@ -0,0 +1,40 @@ +package example + +class ForComprehensions/*example.ForComprehensions#*/ { + for { + a/*local0*/ <- List/*scala.collection.immutable.List.*/(1) + b/*local1*/ <- List/*scala.collection.immutable.List.*/(a/*local0*/) + if ( + a/*local0*/, + b/*local1*/ + ) ==/*java.lang.Object#`==`().*/ (1, 2) + ( + c/*local4*/, + d/*local5*/ + ) <- List/*scala.collection.immutable.List.*/((a/*local0*/, b/*local2*/)) + if ( + a/*local0*/, + b/*local2*/, + c/*local6*/, + d/*local7*/ + ) ==/*java.lang.Object#`==`().*/ (1, 2, 3, 4) + e/*local11*/ = ( + a/*local0*/, + b/*local2*/, + c/*local9*/, + d/*local10*/ + ) + if e/*local14*/ ==/*java.lang.Object#`==`().*/ (1, 2, 3, 4) + f/*local18*/ <- List/*scala.collection.immutable.List.*/(e/*local17*/) + } yield { + ( + a/*local0*/, + b/*local2*/, + c/*local15*/, + d/*local16*/, + e/*local17*/, + f/*local18*/ + ) + } + +} diff --git a/tests/unit/src/test/resources/semanticdb/example/ImplicitClasses.scala b/tests/unit/src/test/resources/semanticdb/example/ImplicitClasses.scala new file mode 100644 index 00000000000..34d5cce9358 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/ImplicitClasses.scala @@ -0,0 +1,10 @@ +package example + +object ImplicitClasses/*example.ImplicitClasses.*/ { + implicit class Xtension/*example.ImplicitClasses.Xtension#*/(number/*example.ImplicitClasses.Xtension#number.*/: Int/*scala.Int#*/) { + def increment/*example.ImplicitClasses.Xtension#increment().*/: Int/*scala.Int#*/ = number/*example.ImplicitClasses.Xtension#number.*/ +/*scala.Int#`+`(+4).*/ 1 + } + implicit class XtensionAnyVal/*example.ImplicitClasses.XtensionAnyVal#*/(private val number/*example.ImplicitClasses.XtensionAnyVal#number.*/: Int/*scala.Int#*/) extends AnyVal/*scala.AnyVal#*/ /*scala.AnyVal#``().*/{ + def double/*example.ImplicitClasses.XtensionAnyVal#double().*/: Int/*scala.Int#*/ = number/*example.ImplicitClasses.XtensionAnyVal#number.*/ */*scala.Int#`*`(+3).*/ 2 + } +} diff --git a/tests/unit/src/test/resources/semanticdb/example/ImplicitConversions.scala b/tests/unit/src/test/resources/semanticdb/example/ImplicitConversions.scala new file mode 100644 index 00000000000..d67e575f9dd --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/ImplicitConversions.scala @@ -0,0 +1,28 @@ +package example + +class ImplicitConversions/*example.ImplicitConversions#*/ { + implicit def string2Number/*example.ImplicitConversions#string2Number().*/( + string/*example.ImplicitConversions#string2Number().(string)*/: String/*scala.Predef.String#*/ + ): Int/*scala.Int#*/ = 42 + val message/*example.ImplicitConversions#message.*/ = "" + val number/*example.ImplicitConversions#number.*/ = 42 + val tuple/*example.ImplicitConversions#tuple.*/ = (1, 2) + val char/*example.ImplicitConversions#char.*/: Char/*scala.Char#*/ = 'a' + + // extension methods + message/*scala.Predef.augmentString().*/ + .stripSuffix/*scala.collection.immutable.StringLike#stripSuffix().*/("h") + tuple/*scala.Predef.any2stringadd().*/ +/*scala.Predef.any2stringadd#`+`().*/ "Hello" + + // implicit conversions + val x/*example.ImplicitConversions#x.*/: Int/*scala.Int#*/ = message/*example.ImplicitConversions#string2Number().*/ + + // interpolators + s/*scala.StringContext#s().*/"Hello $message/*example.ImplicitConversions#message.*/ $number/*example.ImplicitConversions#number.*/" + s/*scala.Predef.augmentString().*/"""Hello + |$message/*example.ImplicitConversions#message.*/ + |$number/*example.ImplicitConversions#number.*/""".stripMargin/*scala.collection.immutable.StringLike#stripMargin(+1).*/ + + val a/*example.ImplicitConversions#a.*/: Int/*scala.Int#*/ = char/*scala.Char#toInt().*/ + val b/*example.ImplicitConversions#b.*/: Long/*scala.Long#*/ = char/*scala.Char#toLong().*/ +} diff --git a/tests/unit/src/test/resources/semanticdb/example/Imports.scala b/tests/unit/src/test/resources/semanticdb/example/Imports.scala new file mode 100644 index 00000000000..5971ab25e49 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Imports.scala @@ -0,0 +1,10 @@ +package example + +import util.{Failure/*scala.util.Failure.*//*scala.util.Failure#*/ => NotGood} +import math.{floor/*scala.math.package.floor().*/ => _, _} + +class Imports/*example.Imports#*/ { + // rename reference + NotGood/*scala.util.Failure.*/(null) + max/*scala.math.package.max().*/(1, 2) +} diff --git a/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala b/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala new file mode 100644 index 00000000000..79811ad2236 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/JavaThenScala.scala @@ -0,0 +1,5 @@ +package example + +class JavaThenScala/*example.JavaThenScala#*/ { + new JavaClass/*example.JavaClass#*//*example.JavaClass#``().*/(42) +} diff --git a/tests/unit/src/test/resources/semanticdb/example/Locals.scala b/tests/unit/src/test/resources/semanticdb/example/Locals.scala new file mode 100644 index 00000000000..ab5330df6de --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Locals.scala @@ -0,0 +1,8 @@ +package example + +class Locals/*example.Locals#*/ { + { + val x/*local0*/ = 2 + x/*local0*/ +/*scala.Int#`+`(+4).*/ 2 + } +} diff --git a/tests/unit/src/test/resources/semanticdb/example/MacroAnnotation.scala b/tests/unit/src/test/resources/semanticdb/example/MacroAnnotation.scala new file mode 100644 index 00000000000..e7f057f7b93 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/MacroAnnotation.scala @@ -0,0 +1,19 @@ +package example + +import io.circe.derivation.annotations.JsonCodec/*io.circe.derivation.annotations.JsonCodec.*//*io.circe.derivation.annotations.JsonCodec#*/ + +@JsonCodec/*example.MacroAnnotation#*/ +// FIXME: https://github.com/scalameta/scalameta/issues/1789/*java.lang.Object#``().*/ +case class MacroAnnotation( + name/*example.MacroAnnotation#name.*/: String/*scala.Predef.String#*/ +) { + def method/*example.MacroAnnotation#method().*/ = 42 +} + +object MacroAnnotations/*example.MacroAnnotations.*/ { + import scala.meta._ + // IntelliJ has never managed to goto definition for the inner classes from Trees.scala + // due to the macro annotations. + val x/*example.MacroAnnotations.x.*/: Defn/*scala.meta.Defn.*/.Class/*scala.meta.Defn.Class#*/ = Defn/*scala.meta.Defn.*/.Class/*scala.meta.Defn.Class.*/(null, null, null, null, null) + val y/*example.MacroAnnotations.y.*/: Mod/*scala.meta.Mod.*/.Final/*scala.meta.Mod.Final#*/ = Mod/*scala.meta.Mod.*/.Final/*scala.meta.Mod.Final.*/() +} diff --git a/tests/unit/src/test/resources/semanticdb/example/MethodOverload.scala b/tests/unit/src/test/resources/semanticdb/example/MethodOverload.scala new file mode 100644 index 00000000000..f58814b2d31 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/MethodOverload.scala @@ -0,0 +1,9 @@ +package example + +class MethodOverload/*example.MethodOverload#*/(b/*example.MethodOverload#b.*/: String/*scala.Predef.String#*/) { + def this/*example.MethodOverload#``(+1).*/() = this("") + def this/*example.MethodOverload#``(+2).*/(c/*example.MethodOverload#``(+2).(c)*/: Int/*scala.Int#*/) = this("") + val a/*example.MethodOverload#a.*/ = 2 + def a/*example.MethodOverload#a().*/(x/*example.MethodOverload#a().(x)*/: Int/*scala.Int#*/) = 2 + def a/*example.MethodOverload#a(+1).*/(x/*example.MethodOverload#a(+1).(x)*/: Int/*scala.Int#*/, y/*example.MethodOverload#a(+1).(y)*/: Int/*scala.Int#*/) = 2 +} diff --git a/tests/unit/src/test/resources/semanticdb/example/Miscellaneous.scala b/tests/unit/src/test/resources/semanticdb/example/Miscellaneous.scala new file mode 100644 index 00000000000..e872f344f64 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Miscellaneous.scala @@ -0,0 +1,12 @@ +package example + +class Miscellaneous/*example.Miscellaneous#*/ { + // backtick identifier + val `a b`/*example.Miscellaneous#`a b`.*/ = 42 + + // infix + inferred apply/implicits/tparams + (List/*scala.collection.immutable.List.*/(1) + .map/*scala.collection.immutable.List#map().*/(_ +/*scala.Int#`+`(+4).*/ 1) + ++/*scala.collection.immutable.List#`++`().*/ + List/*scala.collection.immutable.List.*/(3)) +} diff --git a/tests/unit/src/test/resources/semanticdb/example/NamedArguments.scala b/tests/unit/src/test/resources/semanticdb/example/NamedArguments.scala new file mode 100644 index 00000000000..bd7fd032cf8 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/NamedArguments.scala @@ -0,0 +1,35 @@ +package example + +case class User/*example.User#*/( + name/*example.User#name.*/: String/*scala.Predef.String#*/ = { + // assert default values have occurrences + Map/*scala.Predef.Map.*/.toString/*java.lang.Object#toString().*/ + } +) +object NamedArguments/*example.NamedArguments.*/ { + val susan/*example.NamedArguments.susan.*/ = "Susan" + val user1/*example.NamedArguments.user1.*/ = + User/*example.User.*/ + .apply/*example.User.apply().*/( + name/*example.User.apply().(name)*/ = "John" + ) + val user2/*example.NamedArguments.user2.*/: User/*example.User#*/ = + User/*example.User.*/( + // FIXME: https://github.com/scalameta/scalameta/issues/1787 + name = susan/*example.NamedArguments.susan.*/ + ).copy/*example.User#copy().*/( + name/*example.User#copy().(name)*/ = susan/*example.NamedArguments.susan.*/ + ) + + // anonymous classes + @deprecated/*scala.deprecated#*//*scala.deprecated#``().*/( + message = "a", + since = susan/*example.NamedArguments.susan.*/ + ) def b/*example.NamedArguments.b().*/ = 1 + + // vararg + List/*scala.collection.immutable.List.*/( + xs = 2 + ) + +} diff --git a/tests/unit/src/test/resources/semanticdb/example/PatternMatching.scala b/tests/unit/src/test/resources/semanticdb/example/PatternMatching.scala new file mode 100644 index 00000000000..d74cbe020d8 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/PatternMatching.scala @@ -0,0 +1,24 @@ +package example + +class PatternMatching/*example.PatternMatching#*/ { + val some/*example.PatternMatching#some.*/ = Some/*scala.Some.*/(1) + some/*example.PatternMatching#some.*/ match { + case Some/*scala.Some.*/(number/*local0*/) => + number/*local0*/ + } + + // tuple deconstruction + val (left/*local1*/, right/*local2*/) = (1, 2) + (left/*example.PatternMatching#left.*/, right/*example.PatternMatching#right.*/) + + // val deconstruction + val Some/*scala.Some.*/(number1/*local3*/) = + some/*example.PatternMatching#some.*/ + number1/*example.PatternMatching#number1.*/ + + def localDeconstruction/*example.PatternMatching#localDeconstruction().*/ = { + val Some/*scala.Some.*/(number2/*local5*/) = + some/*example.PatternMatching#some.*/ + number2/*local4*/ + } +} diff --git a/tests/unit/src/test/resources/semanticdb/example/ReflectiveInvocation.scala b/tests/unit/src/test/resources/semanticdb/example/ReflectiveInvocation.scala new file mode 100644 index 00000000000..3eca597673b --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/ReflectiveInvocation.scala @@ -0,0 +1,9 @@ +package example + +class ReflectiveInvocation/*example.ReflectiveInvocation#*/ { + new Serializable/*scala.Serializable#*/ /*java.lang.Object#``().*/{ + def message/*local0*/ = "message" + // reflective invocation + }.message/*local0*/ + +} diff --git a/tests/unit/src/test/resources/semanticdb/example/Scalalib.scala b/tests/unit/src/test/resources/semanticdb/example/Scalalib.scala new file mode 100644 index 00000000000..1266d6a0466 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/Scalalib.scala @@ -0,0 +1,25 @@ +package example + +class Scalalib/*example.Scalalib#*/ { + val lst/*example.Scalalib#lst.*/ = List/*scala.collection.immutable.List.*/[ + ( + Nothing/*scala.Nothing#*/, + Null/*scala.Null#*/, + Singleton/*scala.Singleton#*/, + Any/*scala.Any#*/, + AnyRef/*scala.AnyRef#*/, + AnyVal/*scala.AnyVal#*/, + Int/*scala.Int#*/, + Short/*scala.Short#*/, + Double/*scala.Double#*/, + Float/*scala.Float#*/, + Char/*scala.Char#*/ + ) + ]() + lst/*example.Scalalib#lst.*/.isInstanceOf/*scala.Any#isInstanceOf().*/[Any/*scala.Any#*/] + lst/*example.Scalalib#lst.*/.asInstanceOf/*scala.Any#asInstanceOf().*/[Any/*scala.Any#*/] + println/*scala.Predef.println(+1).*/(lst/*example.Scalalib#lst.*/.##/*java.lang.Object#`##`().*/) + lst/*example.Scalalib#lst.*/ ne/*java.lang.Object#ne().*/ lst/*example.Scalalib#lst.*/ + lst/*example.Scalalib#lst.*/ eq/*java.lang.Object#eq().*/ lst/*example.Scalalib#lst.*/ + lst/*example.Scalalib#lst.*/ ==/*java.lang.Object#`==`().*/ lst/*example.Scalalib#lst.*/ +} diff --git a/tests/unit/src/test/resources/semanticdb/example/StructuralTypes.scala b/tests/unit/src/test/resources/semanticdb/example/StructuralTypes.scala new file mode 100644 index 00000000000..0f83407a392 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/StructuralTypes.scala @@ -0,0 +1,19 @@ +package example + +object StructuralTypes/*example.StructuralTypes.*/ { + type User/*example.StructuralTypes.User#*/ = { + def name/*local0*/: String/*scala.Predef.String#*/ + def age/*local1*/: Int/*scala.Int#*/ + } + + val user/*example.StructuralTypes.user.*/ = null.asInstanceOf/*scala.Any#asInstanceOf().*/[User/*example.StructuralTypes.User#*/] + user/*example.StructuralTypes.user.*/.name + user/*example.StructuralTypes.user.*/.age + + val V/*example.StructuralTypes.V.*/: Object/*java.lang.Object#*/ { + def scalameta/*local2*/: String/*scala.Predef.String#*/ + } = new { + def scalameta/*local3*/ = "4.0" + } + V/*example.StructuralTypes.V.*/.scalameta +} diff --git a/tests/unit/src/test/resources/semanticdb/example/TypeParameters.scala b/tests/unit/src/test/resources/semanticdb/example/TypeParameters.scala new file mode 100644 index 00000000000..d9fa8a0a9c3 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/TypeParameters.scala @@ -0,0 +1,8 @@ +package example + +class TypeParameters/*example.TypeParameters#*/[A/*example.TypeParameters#[A]*/] { + def method/*example.TypeParameters#method().*/[B/*example.TypeParameters#method().[B]*/] = 42 + trait TraitParameter/*example.TypeParameters#TraitParameter#*/[C/*example.TypeParameters#TraitParameter#[C]*/] + type AbstractTypeAlias/*example.TypeParameters#AbstractTypeAlias#*/[D/*example.TypeParameters#AbstractTypeAlias#[D]*/] + type TypeAlias/*example.TypeParameters#TypeAlias#*/[E/*example.TypeParameters#TypeAlias#[E]*/] = List/*scala.package.List#*/[E/*example.TypeParameters#TypeAlias#[E]*/] +} diff --git a/tests/unit/src/test/resources/semanticdb/example/nested/DoublePackage.scala b/tests/unit/src/test/resources/semanticdb/example/nested/DoublePackage.scala new file mode 100644 index 00000000000..353bff71134 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/nested/DoublePackage.scala @@ -0,0 +1,9 @@ +package example + +package nested.x { + class DoublePackage/*example.nested.x.DoublePackage#*/ {} +} + +package nested2.y { + class DoublePackage/*example.nested2.y.DoublePackage#*/ {} +} diff --git a/tests/unit/src/test/resources/semanticdb/example/nested/ExampleNested.scala b/tests/unit/src/test/resources/semanticdb/example/nested/ExampleNested.scala new file mode 100644 index 00000000000..0fe5be3276e --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/nested/ExampleNested.scala @@ -0,0 +1,3 @@ +package example.nested + +class ExampleNested/*example.nested.ExampleNested#*/ {} diff --git a/tests/unit/src/test/resources/semanticdb/example/nested/package.scala b/tests/unit/src/test/resources/semanticdb/example/nested/package.scala new file mode 100644 index 00000000000..3efb637379f --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/nested/package.scala @@ -0,0 +1,9 @@ +package example + +package object nested/*example.nested.package.*/ { + + class PackageObjectNestedClass/*example.nested.package.PackageObjectNestedClass#*/ + +} + +class PackageObjectSibling/*example.PackageObjectSibling#*/ diff --git a/tests/unit/src/test/resources/semanticdb/example/package.scala b/tests/unit/src/test/resources/semanticdb/example/package.scala new file mode 100644 index 00000000000..61edd53edb1 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/package.scala @@ -0,0 +1,5 @@ +package object example/*example.package.*/ { + + class PackageObjectClass/*example.package.PackageObjectClass#*/ + +} diff --git a/tests/unit/src/test/resources/semanticdb/example/type/Backtick.scala b/tests/unit/src/test/resources/semanticdb/example/type/Backtick.scala new file mode 100644 index 00000000000..ec3c398e4f0 --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb/example/type/Backtick.scala @@ -0,0 +1,3 @@ +package example.`type` + +class Backtick/*example.type.Backtick#*/ {} diff --git a/tests/unit/src/test/scala/tests/DefinitionSuite.scala b/tests/unit/src/test/scala/tests/DefinitionSuite.scala new file mode 100644 index 00000000000..e04a7321213 --- /dev/null +++ b/tests/unit/src/test/scala/tests/DefinitionSuite.scala @@ -0,0 +1,147 @@ +package tests + +import scala.meta._ +import scala.meta.internal.inputs._ +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.{semanticdb => s} +import scala.meta.internal.mtags.Semanticdbs +import scala.meta.internal.mtags.OnDemandSymbolIndex +import scala.meta.internal.mtags.Enrichments._ +import scala.meta.internal.mtags.Symbol + +/** + * Assert that every identifier has a definition and every non-identifier has no definition. + * + * This test suite runs the full indexing pipeline: + * - Project source files + * - Project SemanticDB files + * - Dependency sources + * We then run through all Scala files in the input project and query the index at the position of + * every token, including whitespace, delimiters and comment tokens. + * + * To keep the tests readable, we only include the filename of the definition position and leave it + * to separate test suites to assert that the range positions are accurate. + */ +object DefinitionSuite extends DirectoryExpectSuite("definition") { + override def testCases(): List[ExpectTestCase] = { + val index = OnDemandSymbolIndex() + // Step 1. Index project sources + input.allFiles.foreach { source => + index.addSourceFile(source.file, Some(source.sourceDirectory)) + } + // Step 2. Index dependency sources + index.addSourceJar(Library.jdkSources.get) + input.dependencySources.entries.foreach { jar => + index.addSourceJar(jar) + } + + def hasKnownIssues(file: InputFile): Boolean = { + val badlist = List( + "ForComprehensions.scala" // local symbols in large for comprehensions cause problems + ) + badlist.exists { filename => + file.file.toNIO.endsWith(filename) + } + } + input.scalaFiles.map { file => + ExpectTestCase( + file, { () => + val input = file.input + val tokens = input.tokenize.get + val sb = new StringBuilder + tokens.foreach(token => { + sb.append(token.syntax) + val semanticdbPath = classpath.getSemanticdbPath(file.file) + val textDocument = classpath.textDocument(file.file).get + def localDefinition(symbol: Symbol): Option[s.Range] = { + textDocument.occurrences.collectFirst { + case occ + if occ.symbol == symbol.value && occ.role.isDefinition => + occ.range.get + } + } + def symbol(path: AbsolutePath, range: s.Range): Option[Symbol] = { + for { + document <- Semanticdbs.loadTextDocuments(path).documents + occ <- document.occurrences.find( + _.range.exists(_.encloses(range)) + ) + } yield Symbol(occ.symbol) + }.headOption + val obtained = symbol(semanticdbPath, token.pos.toRange) + def filename(path: AbsolutePath): String = { + val name = path.toNIO.getFileName.toString + if (name.endsWith(".semanticdb")) name.replace(".scala", "") + else name + } + token match { + case _: Token.Ident | _: Token.Interpolation.Id => + obtained match { + case Some(symbol) => + if (symbol.isLocal) { + localDefinition(symbol) match { + case Some(_) => + Semanticdbs.printSymbol(sb, filename(semanticdbPath)) + case None => + Semanticdbs.printSymbol(sb, "no local definition") + } + } else { + val definition = index.definition(symbol) match { + case Some(defn) => + val fallback = + if (defn.querySymbol == defn.definitionSymbol) "" + else if (defn.querySymbol.value.stripSuffix(".") == + defn.definitionSymbol.value.stripSuffix("#")) { + // Ignore fallback from companion object to class. + "" + } else { + s" fallback to ${defn.definitionSymbol}" + } + filename(defn.path) + fallback + case None => + if (shouldHaveDefinition(symbol.value)) { + if (!hasKnownIssues(file)) { + scribe.error( + token.pos.formatMessage( + "error", + s"missing definition for $symbol" + ) + ) + } + "" + } else { + "" + } + } + if (!symbol.isPackage && !definition.isEmpty) { + Semanticdbs.printSymbol(sb, definition) + } + } + case None => + sb.append("/**/") + } + case _ => + obtained match { + case Some(symbol) => + Semanticdbs.printSymbol(sb, "unexpected: " + symbol) + case None => () + } + } + }) + val obtained = sb.toString() + obtained + } + ) + } + } + + def shouldHaveDefinition(symbol: String): Boolean = { + !symbol.isPackage && + !symbol.startsWith("scala/Any#") && + !symbol.startsWith("scala/Nothing#") && + !symbol.startsWith("scala/Null#") && + !symbol.startsWith("scala/Singleton#") && + !symbol.startsWith("scala/AnyRef#") && + !symbol.startsWith("java/lang/Object#") + } +} diff --git a/tests/unit/src/test/scala/tests/MtagsSuite.scala b/tests/unit/src/test/scala/tests/MtagsSuite.scala new file mode 100644 index 00000000000..9eb3b225c39 --- /dev/null +++ b/tests/unit/src/test/scala/tests/MtagsSuite.scala @@ -0,0 +1,65 @@ +package tests + +import scala.meta.internal.semanticdb.Scala._ +import scala.meta.internal.mtags.Enrichments._ +import scala.meta.internal.inputs._ +import scala.meta.internal.mtags.Mtags +import scala.meta.internal.mtags.Semanticdbs + +/** + * Assert the symbols emitted by ScalaMtags is a subset of semanticdb-scalac. + * + * It turns out ScalaMtags is actually more correct semanticdb-scalac when + * it comes to trickier cases like implicit conversions and pattern matching. + */ +object MtagsSuite extends DirectoryExpectSuite("mtags") { + + def hasSemanticdbBug(file: InputFile): Boolean = { + // don't assert fidelity where semanticdb-scalac has known bugs and mtags is correct. + List( + "ImplicitClasses", + "PatternMatching", + "ImplicitConversions", + "MacroAnnotation" + ).exists { name => + file.file.toNIO.endsWith(s"$name.scala") + } + } + + def testCases(): List[ExpectTestCase] = { + input.allFiles.map { file => + ExpectTestCase( + file, { () => + val input = file.input + val mtags = Mtags.index(input) + val obtained = Semanticdbs.printTextDocument(mtags) + val unknownSymbols = mtags.occurrences.collect { + case occ if symtab.info(occ.symbol).isEmpty => + val pos = input.toPosition(occ) + pos.formatMessage("error", s"unknown symbol: ${occ.symbol}") + } + if (unknownSymbols.nonEmpty) { + fail(unknownSymbols.mkString("\n")) + } + if (file.isScala && !hasSemanticdbBug(file)) { + // assert mtags produces same results as semanticdb-scalac + val semanticdb = classpath.textDocument(file.file).get + val globalDefinitions = semanticdb.occurrences.filter { occ => + occ.role.isDefinition && + occ.symbol.isGlobal + } + val semanticdbExpected = Semanticdbs.printTextDocument( + semanticdb.withOccurrences(globalDefinitions) + ) + DiffAssertions.expectNoDiff( + obtained, + semanticdbExpected, + "mtags == obtained, semanticdb-scalac == expected" + ) + } + obtained + } + ) + } + } +} diff --git a/tests/unit/src/test/scala/tests/SaveExpect.scala b/tests/unit/src/test/scala/tests/SaveExpect.scala new file mode 100644 index 00000000000..a989aac12f5 --- /dev/null +++ b/tests/unit/src/test/scala/tests/SaveExpect.scala @@ -0,0 +1,18 @@ +package tests + +object SaveExpect { + def main(args: Array[String]): Unit = { + List[BaseExpectSuite]( + DefinitionSuite, + SemanticdbSuite, + MtagsSuite, + ToplevelSuite + ).foreach { suite => + val header = suite.suiteName.length + 2 + println("=" * header) + println("= " + suite.suiteName) + println("=" * header) + suite.saveExpect() + } + } +} diff --git a/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala new file mode 100644 index 00000000000..3df72ea7182 --- /dev/null +++ b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala @@ -0,0 +1,29 @@ +package tests + +import scala.meta.internal.io.FileIO +import scala.meta.internal.mtags.Enrichments._ +import scala.meta.internal.mtags.Mtags + +/** + * Assert the symbols emitted by ScalaToplevelMtags is a subset of ScalaMtags + */ +class ScalaToplevelSuite extends BaseSuite { + val testClasspath = Libraries.suite.flatMap(_.sources().entries) + testClasspath.foreach { entry => + test(entry.toNIO.getFileName.toString) { + FileIO.withJarFileSystem(entry, create = false) { root => + FileIO.listAllFilesRecursively(root).foreach { file => + if (file.toNIO.getFileName.toString.endsWith(".scala")) { + val input = file.toInput + val scalaMtags = Mtags.toplevels(Mtags.index(input)) + val scalaToplevelMtags = Mtags.toplevels(input) + val obtained = scalaToplevelMtags.mkString("\n") + val expected = scalaMtags.mkString("\n") + assertNoDiff(obtained, expected, input.text) + } + } + } + } + } + +} diff --git a/tests/unit/src/test/scala/tests/SemanticdbSuite.scala b/tests/unit/src/test/scala/tests/SemanticdbSuite.scala new file mode 100644 index 00000000000..79a5ccaa5bb --- /dev/null +++ b/tests/unit/src/test/scala/tests/SemanticdbSuite.scala @@ -0,0 +1,23 @@ +package tests + +import scala.meta.internal.mtags.Semanticdbs + +/** + * Baseline test suite that documents the unprocessed output of semanticdb-scalac + * + * This test suite does not test any metals functionality, it is only to see what + * semanticdb-scalac procudes. + */ +object SemanticdbSuite extends DirectoryExpectSuite("semanticdb") { + override def testCases(): List[ExpectTestCase] = { + input.scalaFiles.map { file => + ExpectTestCase( + file, { () => + val textDocument = classpath.textDocument(file.file).get + val obtained = Semanticdbs.printTextDocument(textDocument) + obtained + } + ) + } + } +} diff --git a/tests/unit/src/test/scala/tests/ToplevelSuite.scala b/tests/unit/src/test/scala/tests/ToplevelSuite.scala new file mode 100644 index 00000000000..9179cff77e7 --- /dev/null +++ b/tests/unit/src/test/scala/tests/ToplevelSuite.scala @@ -0,0 +1,34 @@ +package tests + +import java.nio.charset.StandardCharsets +import scala.collection.mutable.ListBuffer +import scala.meta.inputs.Input +import scala.meta.internal.io.FileIO +import scala.meta.internal.mtags.Mtags + +/** Assert that Mtags.toplevels method works as expected. */ +object ToplevelSuite extends SingleFileExpectSuite("toplevels.expect") { + override def obtained(): String = { + val toplevels = ListBuffer.empty[String] + val missingSymbols = ListBuffer.empty[String] + input.sourceDirectories.filter(_.isDirectory).foreach { dir => + val ls = FileIO.listAllFilesRecursively(dir) + ls.files.foreach { relpath => + val text = + FileIO.slurp(ls.root.resolve(relpath), StandardCharsets.UTF_8) + val input = Input.VirtualFile(relpath.toString(), text) + val reluri = relpath.toURI(isDirectory = false).toString + Mtags.toplevels(input).foreach { toplevel => + if (symtab.info(toplevel).isEmpty) { + missingSymbols += toplevel + } + toplevels += s"$reluri -> $toplevel" + } + } + } + if (missingSymbols.nonEmpty) { + fail(s"unknown symbols:\n${missingSymbols.mkString("\n")}") + } + toplevels.sorted.mkString("\n") + } +}