@@ -269,14 +269,21 @@ object DottyIDEPlugin extends AutoPlugin {
269
269
Command .process(" runCode" , state1)
270
270
}
271
271
272
+ private def makeId (name : String , config : String ): String = s " $name/ $config"
273
+
272
274
private def projectConfigTask (config : Configuration ): Initialize [Task [Option [ProjectConfig ]]] = Def .taskDyn {
273
275
val depClasspath = Attributed .data((dependencyClasspath in config).value)
276
+ val projectName = name.value
274
277
275
278
// Try to detect if this is a real Scala project or not. This is pretty
276
279
// fragile because sbt simply does not keep track of this information. We
277
280
// could check if at least one source file ends with ".scala" but that
278
281
// 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
+ )
280
287
281
288
if (! isScalaProject) Def .task { None }
282
289
else Def .task {
@@ -285,19 +292,39 @@ object DottyIDEPlugin extends AutoPlugin {
285
292
// step.
286
293
val _ = (compile in config).value
287
294
288
- val id = s " ${thisProject.value.id}/ ${config.name}"
295
+ val project = thisProject.value
296
+ val id = makeId(project.id, config.name)
289
297
val compilerVersion = (scalaVersion in config).value
290
298
val compilerArguments = (scalacOptions in config).value
291
299
val sourceDirectories = (unmanagedSourceDirectories in config).value ++ (managedSourceDirectories in config).value
292
300
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
+ }
293
319
294
320
Some (new ProjectConfig (
295
321
id,
296
322
compilerVersion,
297
323
compilerArguments.toArray,
298
324
sourceDirectories.toArray,
299
325
depClasspath.toArray,
300
- classDir
326
+ classDir,
327
+ dependencies.toArray
301
328
))
302
329
}
303
330
}
@@ -338,4 +365,106 @@ object DottyIDEPlugin extends AutoPlugin {
338
365
}
339
366
340
367
) ++ 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
+
341
470
}
0 commit comments