@@ -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