-
Notifications
You must be signed in to change notification settings - Fork 398
/
ExternalSourceRootResolution.scala
784 lines (666 loc) · 34.4 KB
/
ExternalSourceRootResolution.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
package org.jetbrains.sbt
package project
import com.intellij.openapi.externalSystem.model.ExternalSystemException
import com.intellij.openapi.externalSystem.model.project.{ExternalSystemSourceType, ModuleData}
import com.intellij.openapi.util.io.FileUtilRt
import com.intellij.openapi.roots.DependencyScope
import org.jetbrains.plugins.scala.extensions.RichFile
import org.jetbrains.sbt.project.SourceSetType.SourceSetType
import org.jetbrains.sbt.project.data.{NestedModuleNode, _}
import org.jetbrains.sbt.project.sources.SharedSourcesModuleType
import org.jetbrains.sbt.structure.{Dependencies, ProjectData, ProjectDependencyData}
import org.jetbrains.sbt.{structure => sbtStructure}
import java.io.File
import java.net.URI
import scala.reflect.ClassTag
trait ExternalSourceRootResolution { self: SbtProjectResolver =>
type ModuleDataNodeType = Node[_<:ModuleData]
protected sealed abstract class ModuleSourceSet(val parent: ModuleDataNodeType)
protected case class PrentModuleSourceSet(override val parent: ModuleDataNodeType) extends ModuleSourceSet(parent)
protected case class CompleteModuleSourceSet(override val parent: ModuleDataNodeType, main: SbtSourceSetModuleNode, test: SbtSourceSetModuleNode) extends ModuleSourceSet(parent)
protected def addSharedSourceModules(
projectToSourceSet: Map[sbtStructure.ProjectData, ModuleSourceSet],
libraryNodes: Seq[LibraryNode],
moduleFilesDirectory: File,
separateProdTestSources: Boolean,
buildProjectsGroups: Seq[BuildProjectsGroup]
): Unit = {
val projects = projectToSourceSet.keys.toSeq
val sharedRoots = sharedAndExternalRootsIn(projects)
val grouped = groupSharedRoots(sharedRoots)
val createSourceModule =
// note: we know that if separateProdTestSources are enabled, projectToSourceSet values will be of type CompleteModuleSourceSet
// and if not, values will be of type PrentModuleSourceSet
if (separateProdTestSources) {
createSourceModuleNode(_, castMapValues[CompleteModuleSourceSet](projectToSourceSet), _, _, _)
} else {
createSourceModuleNodeLegacy(_, castMapValues[PrentModuleSourceSet](projectToSourceSet), _, _, _)
}
grouped.map(createSourceModule(_, libraryNodes, moduleFilesDirectory, buildProjectsGroups))
}
protected def addModuleDependencies(
projectDependencies: Seq[ProjectDependencyData],
allModules: Seq[ModuleDataNodeType],
moduleNode: ModuleDataNodeType
): Unit = {
projectDependencies.foreach { dependencyId =>
val dependency = allModules
.find(_.getId == ModuleNode.combinedId(dependencyId.project, dependencyId.buildURI))
.getOrElse(throw new ExternalSystemException("Cannot find project dependency: " + dependencyId.project))
val scope = scopeFor(dependencyId.configurations.distinct)
addModuleDependencyNode(moduleNode, dependency, scope, exported = false)
}
}
private def castMapValues[R <: ModuleSourceSet : ClassTag](map: Map[sbtStructure.ProjectData, ModuleSourceSet]): Map[sbtStructure.ProjectData, R] =
map.collect { case (key, value: R) => key -> value }
private def createSourceModuleNodeLegacy(
rootGroup: RootGroup,
projectToModuleNode: Map[sbtStructure.ProjectData, PrentModuleSourceSet],
libraryNodes: Seq[LibraryNode],
moduleFilesDirectory: File,
buildProjectsGroups: Seq[BuildProjectsGroup]
): ModuleDataNodeType = {
val projects = rootGroup.projects
val sourceModuleNode = {
val ownerProjectsIds = projects.map(projectToModuleNode).map(_.parent.getId)
val (moduleNode, contentRootNode) = createSourceModule(rootGroup, moduleFilesDirectory, ownerProjectsIds)
//todo: get jdk from a corresponding jvm module ?
moduleNode.add(ModuleSdkNode.inheritFromProject)
val representativeProject = representativeProjectIn(projects)
moduleNode.add(createScalaSdkData(representativeProject.scala))
val representativeProjectDependencies = representativeProject.dependencies
// create unmanaged dependencies, we need to know how many of them there are, they need to be ordered before
// the managed dependencies SCL-21852
val unmanagedLibraryDependencies = representativeProjectDependencies.jars
val unmanagedDependencies = createUnmanagedDependencies(unmanagedLibraryDependencies.forProduction)(moduleNode)
//add library dependencies of the representative project
val libraryDependencies = representativeProjectDependencies.modules
moduleNode.addAll(createLibraryDependencies(libraryDependencies.forProduction)(moduleNode, libraryNodes.map(_.data), offset = unmanagedDependencies.size + 1, separateModulesForProdTest = false))
//add unmanaged jars/libraries dependencies of the representative project
moduleNode.addAll(unmanagedDependencies)
//add project dependencies of the representative project
val allSourceModules = projectToModuleNode.values.toSeq.map(_.parent)
addModuleDependencies(representativeProjectDependencies.projects.forProduction, allSourceModules, moduleNode)
//add some managed sources of the representative project
//(see description of `getManagedSourceRootsFromRepresentativeProjectToIncludeAsBaseModelSourceRoots` method for the details)
val representativeProjectManagedSources = getManagedSourceRootsFromRepresentativeProjectToIncludeAsBaseModelSourceRoots(rootGroup, representativeProject)
representativeProjectManagedSources.foreach { root =>
val esSourceType = calculateEsSourceType(root)
contentRootNode.storePath(esSourceType, root.directory.path)
}
projectToModuleNode.get(representativeProject).foreach { case PrentModuleSourceSet(reprProjectModule) =>
//put source module to the same module group
extendModuleInternalNameWithGroupName(reprProjectModule, Some(moduleNode))
//find rootNode for reprProjectModule, because shared sources module should be put in the same root
val rootNode = findRootNodeForProjectData(representativeProject, buildProjectsGroups, projectToModuleNode)
rootNode.foreach(_.add(moduleNode))
}
moduleNode
}
val dependentModulesThatRequireSharedSourcesModule = getModulesRequiringSharedModuleTransitivelyLegacy(projectToModuleNode, projects)
//add shared sources module as a dependency to platform modules
val sharedSourceRootProjects = projects.map(projectToModuleNode).map { case PrentModuleSourceSet(module) =>
(module, DependencyScope.COMPILE)
}
val allModulesThatRequireSharedSourcesModule = sharedSourceRootProjects ++ dependentModulesThatRequireSharedSourcesModule
allModulesThatRequireSharedSourcesModule.foreach { case (ownerModule, dependencyScope) =>
addModuleDependencyNode(ownerModule, sourceModuleNode, dependencyScope)
}
sourceModuleNode
}
/**
* Collects IDs of shared sources owners modules. It is needed to create [[org.jetbrains.sbt.project.SharedSourcesOwnersData]].
* For more information see [[org.jetbrains.sbt.project.SharedSourcesOwnersData]] ScalaDoc.
*
* @return a tuple containing two sequences of strings.
* The first sequence in the tuple represents the IDs for the main shared sources module,
* while the second sequence represents the IDs for the test shared sources modules.
*/
private def collectIdsOfSharedSourcesOwners(
owners: Seq[ProjectData],
projectToSourceSet: Map[sbtStructure.ProjectData, CompleteModuleSourceSet]
): (Seq[String], Seq[String]) = {
val ownersModuleSourceSets = owners.map(projectToSourceSet)
ownersModuleSourceSets.foldLeft((Seq.empty[String], Seq.empty[String])) {
case ((mainIds, testIds), CompleteModuleSourceSet(_, main, test)) => (mainIds :+ main.id, testIds :+ test.id)
}
}
private def createSourceModuleNode(
rootGroup: RootGroup,
projectToSourceSet: Map[sbtStructure.ProjectData, CompleteModuleSourceSet],
libraryNodes: Seq[LibraryNode],
moduleFilesDirectory: File,
buildProjectsGroups: Seq[BuildProjectsGroup]
): ModuleDataNodeType = {
val projects = rootGroup.projects
val (parentModule, sharedSourcesMainModule, sharedSourcesTestModule) = {
val representativeProject = representativeProjectIn(projects)
val (mainOwnerProjectsIds, testOwnerProjectsIds) = collectIdsOfSharedSourcesOwners(projects, projectToSourceSet)
// add some managed sources of the representative project
// (see description of #getManagedSourceRootsFromRepresentativeProjectToIncludeAsBaseModelSourceRoots method for the details)
val representativeProjectManagedSources = getManagedSourceRootsFromRepresentativeProjectToIncludeAsBaseModelSourceRoots(rootGroup, representativeProject).toSeq
val rootsToSourceType = (rootGroup.roots ++ representativeProjectManagedSources).map(root => (root, calculateEsSourceType(root)))
val allSourceModules = collectSourceModules(projectToSourceSet)
val sharedSourcesMainModule = createSharedSourceSetModule(
rootGroup,
moduleFilesDirectory,
representativeProject,
libraryNodes,
SourceSetType.MAIN,
mainOwnerProjectsIds,
rootsToSourceType,
allSourceModules
)
val sharedSourcesTestModule = createSharedSourceSetModule(
rootGroup,
moduleFilesDirectory,
representativeProject,
libraryNodes,
SourceSetType.TEST,
testOwnerProjectsIds,
rootsToSourceType,
allSourceModules
)
val parentModule = createParentSharedSourcesModule(rootGroup, moduleFilesDirectory)
projectToSourceSet.get(representativeProject).foreach { case CompleteModuleSourceSet(reprProjectModule, _, _) =>
// put source module to the same module group
extendModuleInternalNameWithGroupName(reprProjectModule, Some(parentModule), sharedSourcesMainModule, sharedSourcesTestModule)
// find rootNode for reprProjectModule, because shared sources module should be put in the same root
val rootNode = findRootNodeForProjectData(representativeProject, buildProjectsGroups, projectToSourceSet)
rootNode.foreach(_.add(parentModule))
}
(parentModule, sharedSourcesMainModule, sharedSourcesTestModule)
}
val modulesRequiringSharedModules = getModulesRequiringSharedModulesTransitively(projectToSourceSet, projects)
val modulesToSharedModuleWithScope = modulesRequiringSharedModules.map { case (module, dependency) =>
val isTestProject = dependency.project.endsWith("test")
val sharedSourcesModule = if (isTestProject) sharedSourcesTestModule else sharedSourcesMainModule
(module, sharedSourcesModule, scopeFor(dependency.configurations))
}
// collect shared sources owner modules and shared sources modules to create dependencies
val sharedSourcesOwnersToSharedModuleWithScope = projects.map(projectToSourceSet).flatMap { case CompleteModuleSourceSet(_, mainModule, testModule) =>
Seq(
(mainModule, sharedSourcesMainModule, DependencyScope.COMPILE), // shared sources main module in the platform main module
(testModule, sharedSourcesMainModule, DependencyScope.COMPILE), // shared sources main module in the platform test module
(testModule, sharedSourcesTestModule, DependencyScope.COMPILE) // shared sources test module in the platform test module
)
}
val allModuleDependencies = modulesToSharedModuleWithScope ++ sharedSourcesOwnersToSharedModuleWithScope
allModuleDependencies.collect { case (ownerModule, Some(sharedSourcesModule), scope) =>
addModuleDependencyNode(ownerModule, sharedSourcesModule, scope)
}
Seq(sharedSourcesMainModule, sharedSourcesTestModule).collect { case Some(module) =>
parentModule.add(module)
}
addSourceSetModulesDependencies(parentModule, sharedSourcesMainModule, sharedSourcesTestModule)
parentModule
}
private def addSourceSetModulesDependencies(parentModule: ModuleDataNodeType, sourceModules: Option[SbtSourceSetModuleNode]*): Unit =
sourceModules.flatten.foreach { sourceModuleNode =>
addModuleDependencyNode(parentModule, sourceModuleNode, DependencyScope.COMPILE, exported = false)
}
protected def collectSourceModules(projectToSourceSet: Map[sbtStructure.ProjectData, ModuleSourceSet]): Seq[ModuleDataNodeType] =
projectToSourceSet.values.flatMap {
case PrentModuleSourceSet(parent) => Seq(parent)
case CompleteModuleSourceSet(_, main, test) => Seq(main, test)
}.toSeq
protected def addModuleDependencyNode(ownerModule: ModuleDataNodeType, module: ModuleDataNodeType, dependencyScope: DependencyScope, exported: Boolean = true): Unit = {
val node = new ModuleDependencyNode(ownerModule, module)
node.setScope(dependencyScope)
module match {
case sourceSetNode: SbtSourceSetModuleNode if sourceSetNode.sourceSetType == SourceSetType.TEST =>
node.data.setProductionOnTestDependency(true)
case _ =>
}
node.setExported(exported)
ownerModule.add(node)
}
private def extendModuleInternalNameWithGroupName(
reprProjectModule: ModuleDataNodeType,
moduleNodes: Option[ModuleDataNodeType]*
): Unit = {
val reprProjectModulePrefix = reprProjectModule.getInternalName.stripSuffix(reprProjectModule.getModuleName)
// note: if reprProjectModulePrefix is blank, then it's a root module in the project,
// and simply the internal module should be used instead
val group =
if (reprProjectModulePrefix.isBlank) reprProjectModule.getInternalName
else reprProjectModulePrefix
moduleNodes.collect { case Some(moduleNode) =>
val moduleNameWithGroupName = prependModuleNameWithGroupName(moduleNode.getInternalName, Option(group))
moduleNode.setInternalName(moduleNameWithGroupName)
}
}
private def findRootNodeForProjectData(
representativeProject: ProjectData,
buildProjectsGroups: Seq[BuildProjectsGroup],
projectToModuleNode: Map[sbtStructure.ProjectData, ModuleSourceSet]
): Option[ModuleDataNodeType] = {
val rootProjectDataOpt = buildProjectsGroups
.find(group => (group.projects :+ group.rootProject).contains(representativeProject))
.map(_.rootProject)
rootProjectDataOpt.flatMap(projectToModuleNode.get).map(_.parent)
}
/**
* If project transitive dependencies feature is on, it is required to put shared sources module not only in its owners' modules,
* but in all modules that depend on the owners' modules.
*/
private def getModulesRequiringSharedModuleTransitivelyLegacy(
projectToModuleNode: Map[sbtStructure.ProjectData, PrentModuleSourceSet],
sharedSourcesProjects: Seq[ProjectData]
): Seq[(ModuleDataNodeType, DependencyScope)] = {
projectToModuleNode
.filterNot { case (project, _) => sharedSourcesProjects.contains(project) }
.flatMap { case (project, PrentModuleSourceSet(moduleNode)) =>
val sharedSourcesDependencies = getProjectDependenciesOverlappingWithSharedProjects(project, sharedSourcesProjects)
if (sharedSourcesDependencies.nonEmpty) {
Some((moduleNode, scopeFor(sharedSourcesDependencies.flatMap(_.configurations))))
} else None
}.toSeq
}
private def getProjectDependenciesOverlappingWithSharedProjects(project: ProjectData, sharedSourcesProjects: Seq[ProjectData]): Seq[ProjectDependencyData] = {
def isSharedSourcesDependency(dependency: ProjectDependencyData): Boolean =
sharedSourcesProjects.exists { sharedSourcesProjectData =>
val isTheSameSbtBuild = Option(sharedSourcesProjectData.buildURI) == dependency.buildURI
isTheSameSbtBuild && sharedSourcesProjectData.id == dependency.project
}
val dependencies = project.dependencies.projects.forProduction
dependencies.filter(isSharedSourcesDependency)
}
/**
* If project transitive dependencies feature is on, it is required to put shared sources module not only in its owners' modules,
* but in all modules that depend on the owners' modules.
*/
private def getModulesRequiringSharedModulesTransitively(
projectToModuleNode: Map[ProjectData, CompleteModuleSourceSet],
sharedSourcesProjects: Seq[ProjectData]
): Seq[(SbtSourceSetModuleNode, ProjectDependencyData)] = {
val sharedSourcesProjectIdMap = sharedSourcesProjects
.groupBy(_.buildURI)
.map { case (k, v) => Option(k) -> v }
//note: it is a small hack, but ProjectDependencyData already has a suffix of the type, but ProjectData hasn't
def dropSourceTypeSuffix(projectDependencyData: ProjectDependencyData) =
projectDependencyData.project.dropRight(5)
def filterOnlyRequiredDependencies(dependencies: Seq[ProjectDependencyData]): Seq[ProjectDependencyData] =
dependencies
.filter { projectDependencyData =>
val sharedSourcesProjects = sharedSourcesProjectIdMap.getOrElse(projectDependencyData.buildURI, Seq.empty)
val projectName = dropSourceTypeSuffix(projectDependencyData)
sharedSourcesProjects.map(_.id).contains(projectName)
}
val moduleToDependencies = projectToModuleNode
.filterNot { case (project, _) => sharedSourcesProjects.contains(project) }
.flatMap { case (project, CompleteModuleSourceSet(_, main, test)) =>
Seq((main, project.dependencies.projects.forProduction), (test, project.dependencies.projects.forTest))
}
moduleToDependencies
.view.mapValues(filterOnlyRequiredDependencies).toSeq
.flatMap { case (module, deps) => deps.map((module, _)) }
}
/**
* Select a representative project (preferable a JVM one) among projects that share sources.
* It's module / project dependencies will be copied to shared sources' module. It seems enough to highlight files in the shared source module.
* Please note that we mix source modules into other modules on compilation,
* so source module dependencies are not relevant for compilation, only for highlighting.
*
* Also see [[org.jetbrains.plugins.scala.project.ModuleExt.findRepresentativeModuleForSharedSourceModule]]
*/
private def representativeProjectIn(projects: Seq[ProjectData]): ProjectData = {
val isNonJvmTitle = (title: String) => {
val titleLower = title.toLowerCase()
titleLower.endsWith("js") || titleLower.endsWith("native")
}
val isNonJvmProject = (project: ProjectData) =>
isNonJvmTitle(project.id) || isNonJvmTitle(project.name)
//We sort projects by name to have a more deterministic way of how representative projects are picked in cross-build projects
//If we don't do that, different projects might have dependencies on representative projects with different scala version
//NOTE: we assume that all subprojects have same prefix and are only different in the suffix
val projectsSorted = projects.sortBy(_.id)
val (nonJvmProjects, jvmProjects) = projectsSorted.partition(isNonJvmProject)
if (jvmProjects.nonEmpty)
jvmProjects.head
else
nonJvmProjects.head
}
private def createSourceModule(
group: RootGroup,
moduleFilesDirectory: File,
ownerProjectsIds: Seq[String]
): (ModuleDataNodeType, ContentRootNode) = {
val groupBase = group.base
val moduleNode = createModuleNode(
SharedSourcesModuleType.instance.getId,
group.name,
group.name,
moduleFilesDirectory.path,
groupBase.canonicalPath,
shouldCreateNestedModule = true
)
moduleNode.add(new SbtDisplayModuleNameNode(group.name))
moduleNode.add(new SharedSourcesOwnersNode(SharedSourcesOwnersData(ownerProjectsIds)))
val contentRootNode = new ContentRootNode(groupBase.path)
group.roots.foreach { root =>
val esSourceType = calculateEsSourceType(root)
contentRootNode.storePath(esSourceType, root.directory.path)
}
moduleNode.add(contentRootNode)
setupOutputDirectories(moduleNode, contentRootNode)
(moduleNode, contentRootNode)
}
private def createParentSharedSourcesModule(group: RootGroup, moduleFilesDirectory: File): ModuleDataNodeType = {
val moduleNode = new NestedModuleNode(
SharedSourcesModuleType.instance.getId,
group.name,
group.name,
moduleFilesDirectory.path,
group.base.canonicalPath
)
val contentRootNode = new ContentRootNode(group.base.path)
moduleNode.add(new SbtDisplayModuleNameNode(group.name))
contentRootNode.storePath(ExternalSystemSourceType.EXCLUDED, getOrCreateTargetDir(group.base.path, "target").getAbsolutePath)
moduleNode.add(contentRootNode)
moduleNode.add(ModuleSdkNode.inheritFromProject)
moduleNode
}
private def createSharedSourceSetModule(
group: RootGroup,
moduleFilesDirectory: File,
representativeProject: ProjectData,
libraryNodes: Seq[LibraryNode],
sourceSetName: SourceSetType,
ownerProjectsIds: Seq[String],
rootsToSourceType: Seq[(Root, ExternalSystemSourceType)],
allSourceModules: Seq[ModuleDataNodeType]
): Option[SbtSourceSetModuleNode] = {
val groupPath = group.base.path
val internalModuleName = s"${group.name}.$sourceSetName"
val moduleNode = new SbtSourceSetModuleNode(
SharedSourcesModuleType.instance.getId,
internalModuleName,
sourceSetName,
moduleFilesDirectory.path,
group.base.canonicalPath
)
moduleNode.setInternalName(internalModuleName)
moduleNode.add(new SbtDisplayModuleNameNode(internalModuleName))
moduleNode.add(new SharedSourcesOwnersNode(SharedSourcesOwnersData(ownerProjectsIds)))
def isApplicableSource(sourceType: ExternalSystemSourceType): Boolean =
if (sourceSetName == SourceSetType.TEST) sourceType.isTest
else !sourceType.isTest
// it is not needed to care about excluded because it is not possible to have excluded type see #calculateEsSourceType
val roots = rootsToSourceType
.filter { case (_, sourceType) => isApplicableSource(sourceType) }
.map { case (root, sourceType) => (root.directory.path, sourceType) }
if (roots.nonEmpty) {
// it is correct to hardcode a root path to src/main or src/test, because the current logic with shared sources
// allows the creation of shared source only in those directories. See #basePathFromKnownHardcodedDefaultPaths
val contentRootNodes = createContentRootNodes(roots, Seq(s"$groupPath/src/$sourceSetName"))
moduleNode.addAll(contentRootNodes)
} else {
// when roots are empty, we shouldn't create a shared sources module
return None
}
moduleNode.setInheritProjectCompileOutputPath(false)
val (relPath, externalSystemSourceType) =
if (sourceSetName == SourceSetType.TEST) ("target/test-classes", ExternalSystemSourceType.TEST)
else ("target/classes", ExternalSystemSourceType.SOURCE)
setupOutputDirectoryBasedOnRelPath(moduleNode, groupPath, externalSystemSourceType, relPath)
val scalaSdk = createScalaSdkData(representativeProject.scala)
moduleNode.add(ModuleSdkNode.inheritFromProject)
moduleNode.add(scalaSdk)
val representativeProjectDependencies = representativeProject.dependencies
def getScopedDependencies[T](deps: Dependencies[T]): Seq[T] =
if (sourceSetName == SourceSetType.TEST) deps.forTest
else deps.forProduction
// create unmanaged dependencies, we need to know how many of them there are, they need to be ordered before
// the managed dependencies SCL-21852
val unmanagedLibraryDependencies = getScopedDependencies(representativeProjectDependencies.jars)
val unmanagedDependencies = createUnmanagedDependencies(unmanagedLibraryDependencies)(moduleNode)
//add library dependencies of the representative project
val librariesNodeData = libraryNodes.map(_.data)
val libraryDependencies = getScopedDependencies(representativeProjectDependencies.modules)
moduleNode.addAll(createLibraryDependencies(libraryDependencies)(moduleNode, librariesNodeData, offset = unmanagedDependencies.size + 1, separateModulesForProdTest = true))
//add unmanaged jars/libraries dependencies of the representative project
moduleNode.addAll(unmanagedDependencies)
// add project dependencies of the representative project
val moduleDependencies = getScopedDependencies(representativeProjectDependencies.projects)
addModuleDependencies(moduleDependencies, allSourceModules, moduleNode)
Some(moduleNode)
}
protected def createContentRootNodes(
roots: Seq[(String, ExternalSystemSourceType)],
rootPaths: Seq[String]
): Seq[ContentRootNode] = {
val contentRootNodes = rootPaths.distinct.map(path => new ContentRootNode(path))
roots.foldLeft(contentRootNodes) { case (nodes, (root, sourceType)) =>
val rootFile = new File(root)
val suitableContentRootNode = nodes.find { node =>
val dataRootFile = new File(node.data.getRootPath)
rootFile.isUnder(dataRootFile, strict = false)
}
suitableContentRootNode match {
case Some(contentRootNode) =>
contentRootNode.storePath(sourceType, root)
nodes
case None =>
val node = new ContentRootNode(root)
node.storePath(sourceType, root)
nodes :+ node
}
}
}
/**
* The primary use case for this logic is to handle SBT projects with `projectmatrix` sbt plugin.<br>
* You can inspect `sbt-projectmatrix-with-source-generators` test project as an example.
*
* Details:<br>
* In sbt build with `projectmatrix` sbt plugin, for a single project multiple subprojects are generated
* For example if we define single project {{{
* val downstream = (projectMatrix in file("downstream"))
* .settings(commonSettings(false) *)
* .jvmPlatform(scalaVersions = Seq("2.12.17", "2.13.10"))
* .jsPlatform(scalaVersions = Seq("2.12.17", "2.13.10"))
* }}}
* 4 extra subprojects will be generated (2 JVM projects with 2 scala versions and 2 JS projects with 2 scala version)
*
* But generated sources for such projects will be located outside their base directory (or "contentRoot" in terms of IDEA)
* Instead, they will be located in the content root of the original project, but in a special folders, like: {{{
* target/jvm-2.12/src_managed/main
* target/jvm-2.13/src_managed/main
* target/js-2.12/src_managed/main
* target/js-2.13/src_managed/main
* }}}
* So they will not be registered as source roots for IntelliJ Module (source roots must be located under the content root).
* That's why we need to explicitly add source dependency from the representative project, by analogy with it's module/library/jars dependencies
*
* In case some logic is not clear, try to comment it out and run project structure/highlighting tests
*/
private def getManagedSourceRootsFromRepresentativeProjectToIncludeAsBaseModelSourceRoots(
rootGroup: RootGroup,
representativeProject: ProjectData
): Set[Root] = {
val rootGroupBase = rootGroup.base
val representativeProjectBase = representativeProject.base
val sourceRootsFromRepresentative: Seq[Root] = sourceRootsIn(representativeProject)
sourceRootsFromRepresentative
.filter(_.managed)
.toSet
//ensure that source roots are not already listed in root group roots to avoid duplicates
.diff(rootGroup.roots.toSet)
//ensure that source roots are in the content root of base module
.filter(_.directory.isUnder(rootGroupBase))
//get those source roots which are outside representative project content root
.filterNot(_.directory.isUnder(representativeProjectBase))
}
//target directory are expected by jps compiler:
//if they are missing all sources are marked dirty and there is no incremental compilation
private def setupOutputDirectories(moduleNode: ModuleDataNodeType, contentRootNode: ContentRootNode): Unit = {
val contentRootData = contentRootNode.data
val contentRoot = contentRootData.getRootPath
contentRootData.storePath(ExternalSystemSourceType.EXCLUDED, getOrCreateTargetDir(contentRoot, "target").getAbsolutePath)
moduleNode.setInheritProjectCompileOutputPath(false)
Seq((ExternalSystemSourceType.SOURCE, "target/classes"), (ExternalSystemSourceType.TEST, "target/test-classes")).foreach { case (sourceType, relPath) =>
setupOutputDirectoryBasedOnRelPath(moduleNode, contentRoot, sourceType, relPath)
}
}
private def setupOutputDirectoryBasedOnRelPath(
moduleNode: ModuleDataNodeType,
basePath: String,
sourceType: ExternalSystemSourceType,
relPath: String
): Unit =
moduleNode.setCompileOutputPath(sourceType, getOrCreateTargetDir(basePath, relPath).getAbsolutePath)
private def getOrCreateTargetDir(root: String, relPath: String): File = {
val file = new File(root, relPath)
if (!file.exists()) {
FileUtilRt.createDirectory(file)
}
file
}
private def calculateEsSourceType(root: Root): ExternalSystemSourceType =
ExternalSystemSourceType.from(
root.scope == Root.Scope.Test,
root.managed,
root.kind == Root.Kind.Resources,
false
)
private def sharedAndExternalRootsIn(projects: Seq[sbtStructure.ProjectData]): Seq[SharedRoot] = {
val projectRoots = projects.flatMap(project => sourceRootsIn(project).map(ProjectRoot(project,_)))
// TODO return the message about omitted directories
val internalSourceDirectories = projectRoots.filter(_.isInternal).map(_.root.directory)
projectRoots
.filter(it => it.isExternal && !internalSourceDirectories.contains(it.root.directory))
.groupBy(_.root)
.view.mapValues(_.map(_.project).toSet).toMap
.map(p => SharedRoot(p._1, p._2.toSeq))
.toSeq
}
private def groupSharedRoots(roots: Seq[SharedRoot]): Seq[RootGroup] = {
val nameProvider = new SharedSourceRootNameProvider()
// TODO consider base/projects correspondence
val rootsGroupedByBase = roots.groupBy(_.root.basePathFromKnownHardcodedDefaultPaths)
rootsGroupedByBase.toList.collect {
//NOTE: ignore roots with empty base to avoid dangling "shared-sources" module
case (Some(base), sharedRoots) =>
val name = nameProvider.nameFor(base)
val projects = sharedRoots.flatMap(_.projects).distinct
RootGroup(name, sharedRoots.map(_.root), projects)
}
}
private def sourceRootsIn(project: sbtStructure.ProjectData): Seq[Root] = {
val relevantScopes = Set("compile", "test", "it")
val relevantConfigurations = project.configurations.filter(it => relevantScopes.contains(it.id))
relevantConfigurations.flatMap { configuration =>
def createRoot(kind: Root.Kind)(directory: sbtStructure.DirectoryData): Root = {
val scope = if (configuration.id == "compile") Root.Scope.Compile else Root.Scope.Test
Root(scope, kind, directory.file.canonicalFile, directory.managed)
}
val sourceRoots = configuration.sources.map(createRoot(Root.Kind.Sources))
val resourceRoots = configuration.resources.map(createRoot(Root.Kind.Resources))
sourceRoots ++ resourceRoots
}
}
/**
* This class is designed to group projects from single SBT build.
* Note, SBT single sbt build can consists from multiple other builds using `ProjectRef`
*
* @param buildUri can point to a directory ot a github repository
*/
protected case class BuildProjectsGroup(
buildUri: URI,
rootProject: ProjectData,
projects: Seq[ProjectData],
rootProjectModuleNameUnique: String,
)
private case class RootGroup(name: String, roots: Seq[Root], projects: Seq[sbtStructure.ProjectData]) {
lazy val base: File = commonBase(roots)
private def commonBase(roots: Seq[Root]): File = {
import scala.jdk.CollectionConverters._
val paths = roots.map { root =>
root.basePathFromKnownHardcodedDefaultPaths.getOrElse(root.directory)
.getCanonicalFile.toPath.normalize
}
paths.foldLeft(paths.head) { case (common, it) =>
common.iterator().asScala.zip(it.iterator().asScala)
.takeWhile { case (c,p) => c==p}
.map(_._1)
.foldLeft(paths.head.getRoot) { case (base,child) => base.resolve(child)}
}.toFile
}
}
private case class SharedRoot(root: Root, projects: Seq[sbtStructure.ProjectData])
private case class ProjectRoot(project: sbtStructure.ProjectData, root: Root) {
def isInternal: Boolean = !isExternal
def isExternal: Boolean = root.directory.isOutsideOf(project.base)
}
private case class Root(
scope: Root.Scope,
kind: Root.Kind,
directory: File,
managed: Boolean
) {
lazy val basePathFromKnownHardcodedDefaultPaths: Option[File] = Root.DefaultPaths.collectFirst {
//Example directory: /c/example-project/downstream/src/test/java (check if it parent ends with `src/test`)
case paths if directory.parent.exists(_.endsWith(paths: _*)) => directory << (paths.length + 1)
}
}
private object Root {
private val DefaultPaths = Seq(
Seq("src", "main"),
Seq("src", "test"),
)
sealed trait Scope
object Scope {
case object Compile extends Scope
case object Test extends Scope
}
sealed trait Kind
object Kind {
case object Sources extends Kind
case object Resources extends Kind
}
}
private class SharedSourceRootNameProvider {
private var usedNames = Set.empty[String]
private var counter = 1
def nameFor(base: File): String = {
val namedDirectory = if (base.getName == "shared") base.parent.getOrElse(base) else base
val prefix = s"${namedDirectory.getName}-sources"
val result = if (usedNames.contains(prefix)) {
counter += 1
s"$prefix-$counter"
} else {
prefix
}
usedNames += result
result
}
}
protected def prependModuleNameWithGroupName(moduleName: String, group: Option[String]): String = {
val moduleNameWithGroupPrefix = group
.filterNot(_.isBlank)
// the group name might ended with a dot, when it is from org/jetbrains/sbt/project/ExternalSourceRootResolution.scala:111
// and can be without a dot, when it is from org.jetbrains.sbt.project.SbtProjectResolver#createModuleWithAllRequiredData
.map(groupName => if (groupName.endsWith(".")) groupName else s"$groupName.")
.map(_ + moduleName)
moduleNameWithGroupPrefix.getOrElse(moduleName)
}
protected def createModuleNode(
typeId: String,
projectId: String,
moduleName: String,
moduleFileDirectoryPath: String,
externalConfigPath: String,
shouldCreateNestedModule: Boolean
): ModuleDataNodeType = {
if (shouldCreateNestedModule) {
new NestedModuleNode(typeId, projectId, moduleName, moduleFileDirectoryPath, externalConfigPath)
} else {
new ModuleNode(typeId, projectId, moduleName, moduleFileDirectoryPath, externalConfigPath)
}
}
}
object SourceSetType extends Enumeration {
type SourceSetType = Value
final val MAIN = Value("main")
final val TEST = Value("test")
}