Skip to content

Commit 5df6fe3

Browse files
committed
Add dependencies field to ProjectConfig
This field will be used to restrict which projects to inspect when doing workspace-wide rename or finding all references. The code that extracts dependency information from sbt has been ported from scalacenter/bloop: https://github.com/scalacenter/bloop/blob/v1.0.0/integrations/sbt-bloop/src/main/scala/bloop/integrations/sbt/SbtBloop.scala
1 parent 2cc525a commit 5df6fe3

File tree

3 files changed

+139
-6
lines changed

3 files changed

+139
-6
lines changed

language-server/src/dotty/tools/languageserver/config/ProjectConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ public class ProjectConfig {
1111
public final File[] sourceDirectories;
1212
public final File[] dependencyClasspath;
1313
public final File classDirectory;
14+
public final String[] dependencies;
1415

1516
@JsonCreator
1617
public ProjectConfig(
@@ -19,12 +20,14 @@ public ProjectConfig(
1920
@JsonProperty("compilerArguments") String[] compilerArguments,
2021
@JsonProperty("sourceDirectories") File[] sourceDirectories,
2122
@JsonProperty("dependencyClasspath") File[] dependencyClasspath,
22-
@JsonProperty("classDirectory") File classDirectory) {
23+
@JsonProperty("classDirectory") File classDirectory,
24+
@JsonProperty("dependencies") String[] dependencies) {
2325
this.id = id;
2426
this.compilerVersion = compilerVersion;
2527
this.compilerArguments = compilerArguments;
2628
this.sourceDirectories = sourceDirectories;
2729
this.dependencyClasspath = dependencyClasspath;
28-
this.classDirectory =classDirectory;
30+
this.classDirectory = classDirectory;
31+
this.dependencies = dependencies;
2932
}
3033
}

language-server/test/dotty/tools/languageserver/util/server/TestServer.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ class TestServer(testFolder: Path, projects: List[Project]) {
5959
| "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)},
6060
| "sourceDirectories" : ${showSeq(sourceDirectory(project, wipe = false) :: Nil)},
6161
| "dependencyClasspath" : ${showSeq(dependencyClasspath(project))},
62-
| "classDirectory" : "${classDirectory(project, wipe = false).toString.replace('\\','/')}"
62+
| "classDirectory" : "${classDirectory(project, wipe = false).toString.replace('\\','/')}",
63+
| "dependencies": ${showSeq(project.dependsOn.map(_.name))}
6364
|}
6465
|""".stripMargin
6566
}

sbt-dotty/src/dotty/tools/sbtplugin/DottyIDEPlugin.scala

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,14 +269,21 @@ object DottyIDEPlugin extends AutoPlugin {
269269
Command.process("runCode", state1)
270270
}
271271

272+
private def makeId(name: String, config: String): String = s"$name/$config"
273+
272274
private def projectConfigTask(config: Configuration): Initialize[Task[Option[ProjectConfig]]] = Def.taskDyn {
273275
val depClasspath = Attributed.data((dependencyClasspath in config).value)
276+
val projectName = name.value
274277

275278
// Try to detect if this is a real Scala project or not. This is pretty
276279
// fragile because sbt simply does not keep track of this information. We
277280
// could check if at least one source file ends with ".scala" but that
278281
// doesn't work for empty projects.
279-
val isScalaProject = depClasspath.exists(_.getAbsolutePath.contains("dotty-library")) && depClasspath.exists(_.getAbsolutePath.contains("scala-library"))
282+
val isScalaProject = (
283+
// Our `dotty-library` project is a Scala project
284+
(projectName.startsWith("dotty-library") || depClasspath.exists(_.getAbsolutePath.contains("dotty-library")))
285+
&& depClasspath.exists(_.getAbsolutePath.contains("scala-library"))
286+
)
280287

281288
if (!isScalaProject) Def.task { None }
282289
else Def.task {
@@ -285,19 +292,39 @@ object DottyIDEPlugin extends AutoPlugin {
285292
// step.
286293
val _ = (compile in config).value
287294

288-
val id = s"${thisProject.value.id}/${config.name}"
295+
val project = thisProject.value
296+
val id = makeId(project.id, config.name)
289297
val compilerVersion = (scalaVersion in config).value
290298
val compilerArguments = (scalacOptions in config).value
291299
val sourceDirectories = (unmanagedSourceDirectories in config).value ++ (managedSourceDirectories in config).value
292300
val classDir = (classDirectory in config).value
301+
val extracted = Project.extract(state.value)
302+
val settings = extracted.structure.data
303+
304+
val dependencies = {
305+
val logger = streams.value.log
306+
// Project dependencies come from classpath deps and also inter-project config deps
307+
// We filter out dependencies that do not compile using Dotty
308+
val classpathProjectDependencies =
309+
project.dependencies.filter { d =>
310+
val version = scalaVersion.in(d.project).get(settings).get
311+
isDottyVersion(version)
312+
}.map(d => projectDependencyName(d, config, project, logger))
313+
val configDependencies =
314+
eligibleDepsFromConfig(config).value.map(c => makeId(project.id, c.name))
315+
316+
// The distinct here is important to make sure that there are no repeated project deps
317+
(classpathProjectDependencies ++ configDependencies).distinct.toList
318+
}
293319

294320
Some(new ProjectConfig(
295321
id,
296322
compilerVersion,
297323
compilerArguments.toArray,
298324
sourceDirectories.toArray,
299325
depClasspath.toArray,
300-
classDir
326+
classDir,
327+
dependencies.toArray
301328
))
302329
}
303330
}
@@ -338,4 +365,106 @@ object DottyIDEPlugin extends AutoPlugin {
338365
}
339366

340367
) ++ addCommandAlias("launchIDE", ";configureIDE;runCode")
368+
369+
// Ported from Bloop
370+
/**
371+
* Detect the eligible configuration dependencies from a given configuration.
372+
*
373+
* A configuration is elibile if the project defines it and `bloopGenerate`
374+
* exists for it. Otherwise, the configuration dependency is ignored.
375+
*
376+
* This is required to prevent transitive configurations like `Runtime` from
377+
* generating useless bloop configuration files and possibly incorrect project
378+
* dependencies. For example, if we didn't do this then the dependencies of
379+
* `IntegrationTest` would be `projectName-runtime` and `projectName-compile`,
380+
* whereas the following logic will return only the configuration `Compile`
381+
* so that the use site of this function can create the project dep
382+
* `projectName-compile`.
383+
*/
384+
private def eligibleDepsFromConfig(config: Configuration): Def.Initialize[Task[List[Configuration]]] = {
385+
Def.task {
386+
def depsFromConfig(configuration: Configuration): List[Configuration] = {
387+
configuration.extendsConfigs.toList match {
388+
case config :: Nil if config.extendsConfigs.isEmpty => config :: Nil
389+
case config :: Nil => config :: depsFromConfig(config)
390+
case Nil => Nil
391+
}
392+
}
393+
394+
val configs = depsFromConfig(config)
395+
val activeProjectConfigs = thisProject.value.configurations.toSet
396+
397+
val data = settingsData.value
398+
val thisProjectRef = Keys.thisProjectRef.value
399+
400+
val eligibleConfigs = activeProjectConfigs.filter { c =>
401+
val configKey = ConfigKey.configurationToKey(c)
402+
// Consider only configurations where the `compile` key is defined
403+
val eligibleKey = compile in (thisProjectRef, configKey)
404+
eligibleKey.get(data) match {
405+
case Some(t) =>
406+
// Sbt seems to return tasks for the extended configurations (looks like a big bug)
407+
t.info.get(taskDefinitionKey) match {
408+
// So we now make sure that the returned config key matches the original one
409+
case Some(taskDef) => taskDef.scope.config.toOption.toList.contains(configKey)
410+
case None => true
411+
}
412+
case None => false
413+
}
414+
}
415+
416+
configs.filter(c => eligibleConfigs.contains(c))
417+
}
418+
}
419+
420+
/**
421+
* Creates a project name from a classpath dependency and its configuration.
422+
*
423+
* This function uses internal sbt utils (`sbt.Classpaths`) to parse configuration
424+
* dependencies like sbt does and extract them. This parsing only supports compile
425+
* and test, any kind of other dependency will be assumed to be test and will be
426+
* reported to the user.
427+
*
428+
* Ref https://www.scala-sbt.org/1.x/docs/Library-Management.html#Configurations.
429+
*/
430+
private def projectDependencyName(
431+
dep: ClasspathDep[ProjectRef],
432+
configuration: Configuration,
433+
project: ResolvedProject,
434+
logger: Logger
435+
): String = {
436+
val ref = dep.project
437+
dep.configuration match {
438+
case Some(_) =>
439+
val mapping = sbt.Classpaths.mapped(
440+
dep.configuration,
441+
List("compile", "test"),
442+
List("compile", "test"),
443+
"compile",
444+
"*->compile"
445+
)
446+
447+
mapping(configuration.name) match {
448+
case Nil =>
449+
makeId(ref.project, configuration.name)
450+
case List(conf) if Compile.name == conf =>
451+
makeId(ref.project, Compile.name)
452+
case List(conf) if Test.name == conf =>
453+
makeId(ref.project, Test.name)
454+
case List(conf1, conf2) if Test.name == conf1 && Compile.name == conf2 =>
455+
makeId(ref.project, Test.name)
456+
case List(conf1, conf2) if Compile.name == conf1 && Test.name == conf2 =>
457+
makeId(ref.project, Test.name)
458+
case unknown =>
459+
val msg =
460+
s"Unsupported dependency '${project.id}' -> '${ref.project}:${unknown.mkString(", ")}' is understood as '${ref.project}:test'."
461+
logger.warn(msg)
462+
makeId(ref.project, Test.name)
463+
}
464+
case None =>
465+
// If no configuration, default is `Compile` dependency (see scripted tests `cross-compile-test-configuration`)
466+
makeId(ref.project, Compile.name)
467+
}
468+
}
469+
341470
}

0 commit comments

Comments
 (0)