sbt.internal.inc.Relations.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of zinc-core_2.13 Show documentation
Show all versions of zinc-core_2.13 Show documentation
Incremental compiler of Scala
The newest version!
/*
* Zinc - The incremental compiler for Scala.
* Copyright Scala Center, Lightbend, and Mark Harrah
*
* Licensed under Apache License 2.0
* SPDX-License-Identifier: Apache-2.0
*
* See the NOTICE file distributed with this work for
* additional information regarding copyright ownership.
*/
package sbt
package internal
package inc
import sbt.internal.util.Relation
import xsbti.VirtualFileRef
import xsbti.api.{ DependencyContext, ExternalDependency, InternalDependency }
import xsbti.api.DependencyContext._
import Relations.ClassDependencies
import xsbti.compile.analysis.{ Stamp => XStamp }
/**
* Provides mappings between source files, generated classes (products), and binaries.
* Dependencies that are tracked include internal: a dependency on a source in the same compilation group (project),
* external: a dependency on a source in another compilation group (tracked as the name of the class),
* library: a dependency on a class or jar file not generated by a source file in any tracked compilation group,
* inherited: a dependency that resulted from a public template inheriting,
* direct: any type of dependency, including inheritance.
*/
trait Relations {
/** All sources _with at least one product_ . */
def allSources: collection.Set[VirtualFileRef]
/** All products associated with sources. */
def allProducts: collection.Set[VirtualFileRef]
/** All files that are recorded as a library dependency of a source file.*/
def allLibraryDeps: collection.Set[VirtualFileRef]
/** All files in another compilation group (project) that are recorded as a source dependency of a source file in this group.*/
def allExternalDeps: collection.Set[String]
/**
* Names (fully qualified, at the pickler phase) of classes defined in source file `src`.
*/
def classNames(src: VirtualFileRef): Set[String]
/** Source files that generated a class with the given fully qualified `name`. This is typically a set containing a single file. */
def definesClass(name: String): Set[VirtualFileRef]
/** The classes that were generated for source file `src`. */
def products(src: VirtualFileRef): Set[VirtualFileRef]
/** The source files that generated class file `prod`. This is typically a set containing a single file. */
def produced(prod: VirtualFileRef): Set[VirtualFileRef]
/** The library dependencies for the source file `src`. */
def libraryDeps(src: VirtualFileRef): Set[VirtualFileRef]
/** The source files that depend on library file `dep`. */
def usesLibrary(dep: VirtualFileRef): Set[VirtualFileRef]
/** The library class names for the library JAR file `lib`. */
def libraryClassNames(lib: VirtualFileRef): Set[String]
/** The library files that generated a class with the given fully qualified `name`. This is typically a set containing a single file. */
def libraryDefinesClass(name: String): Set[VirtualFileRef]
/** Internal source dependencies for `src`. This includes both direct and inherited dependencies. */
def internalClassDeps(className: String): Set[String]
/** Internal source files that depend on internal source `dep`. This includes both direct and inherited dependencies. */
def usesInternalClass(className: String): Set[String]
/** External source dependencies that internal source file `src` depends on. This includes both direct and inherited dependencies. */
def externalDeps(className: String): Set[String]
/** Internal source dependencies that depend on external source file `dep`. This includes both direct and inherited dependencies. */
def usesExternal(className: String): Set[String]
/**
* Records that the file `src` generates products `products`, has internal dependencies `internalDeps`,
* has external dependencies `externalDeps` and library dependencies `libraryDeps`.
*/
def addSource(
src: VirtualFileRef,
products: Iterable[VirtualFileRef],
classes: Iterable[(String, String)],
internalDeps: Iterable[InternalDependency],
externalDeps: Iterable[ExternalDependency],
libraryDeps: Iterable[(VirtualFileRef, String, XStamp)]
): Relations = {
addProducts(src, products)
.addClasses(src, classes)
.addInternalSrcDeps(src, internalDeps)
.addExternalDeps(src, externalDeps)
.addLibraryDeps(src, libraryDeps)
}
/**
* Records all the products `prods` generated by `src`
*/
private[inc] def addProducts(
src: VirtualFileRef,
prods: Iterable[VirtualFileRef]
): Relations
/**
* Records all the classes `classes` generated by `src`
*
* a single entry in `classes` collection is `(src class name, binary class name)`
*/
private[inc] def addClasses(src: VirtualFileRef, classes: Iterable[(String, String)]): Relations
/**
* Records all the internal source dependencies `deps` of `src`
*/
private[inc] def addInternalSrcDeps(
src: VirtualFileRef,
deps: Iterable[InternalDependency]
): Relations
/**
* Records all the external dependencies `deps` of `src`
*/
private[inc] def addExternalDeps(
src: VirtualFileRef,
deps: Iterable[ExternalDependency]
): Relations
/**
* Records all the library dependencies `deps` of `src`
*/
private[inc] def addLibraryDeps(
src: VirtualFileRef,
deps: Iterable[(VirtualFileRef, String, XStamp)]
): Relations
private[inc] def addUsedNames(data: Relations.UsedNames): Relations
/** Concatenates the two relations. Acts naively, i.e., doesn't internalize external deps on added files. */
def ++(o: Relations): Relations
/** Drops all dependency mappings a->b where a is in `sources`. Acts naively, i.e., doesn't externalize internal deps on removed files. */
def --(sources: Iterable[VirtualFileRef]): Relations
/** The relation between internal sources and generated class files. */
def srcProd: Relation[VirtualFileRef, VirtualFileRef]
/** The dependency relation between internal sources and library JARs. */
def libraryDep: Relation[VirtualFileRef, VirtualFileRef]
/** The dependency relation between library JARs and class names. */
def libraryClassName: Relation[VirtualFileRef, String]
/**
* The relation between source and product class names.
*
* Only non-local classes, objects and traits are tracked by this relation.
* For classes, nested objects and traits it's 1-1 relation. For top level objects it's 1-2 relation. E.g., for
*
* object A
*
* The binaryClass will have two entries:
*
* A -> A, A -> A$
*
* This reflects Scala's compiler behavior of generating two class files per top level object declaration.
*/
def productClassName: Relation[String, String]
/** The dependency relation between internal classes.*/
def internalClassDep: Relation[String, String]
/** The dependency relation between internal and external classes.*/
def externalClassDep: Relation[String, String]
/** All the internal dependencies */
private[inc] def internalDependencies: InternalDependencies
/** All the external dependencies */
private[inc] def externalDependencies: ExternalDependencies
/**
* The class dependency relation between classes introduced by member reference.
*
* NOTE: All inheritance dependencies are included in this relation because in order to
* inherit from a member you have to refer to it. If you check documentation of `inheritance`
* you'll see that there's small oddity related to traits being the first parent of a
* class/trait that results in additional parents being introduced due to normalization.
* This relation properly accounts for that so the invariant that `memberRef` is a superset
* of `inheritance` is preserved.
*/
private[inc] def memberRef: ClassDependencies
/**
* The class dependency relation between classes introduced by inheritance.
* The dependency by inheritance is introduced when a template (class or trait) mentions
* a given type in a parent position.
*
* NOTE: Due to an oddity in how Scala's type checker works there's one unexpected dependency
* on a class being introduced. An example illustrates the best the problem. Let's consider
* the following structure:
*
* trait A extends B
* trait B extends C
* trait C extends D
* class D
*
* We are interested in dependencies by inheritance of `A`. One would expect it to be just `B`
* but the answer is `B` and `D`. The reason is because Scala's type checker performs a certain
* normalization so the first parent of a type is a class. Therefore the example above is normalized
* to the following form:
*
* trait A extends D with B
* trait B extends D with C
* trait C extends D
* class D
*
* Therefore if you inherit from a trait you'll get an additional dependency on a class that is
* resolved transitively. You should not rely on this behavior, though.
*
*/
private[inc] def inheritance: ClassDependencies
/**
* The class dependency introduced by a local class but accounted for an outer, non local class.
*
* This type of a dependency arises when a triple of classes is involved. Let's consider an
* example:
*
* // A.scala
* class A
*
* // B.scala
* class Outer {
* def foo: Unit = {
* class Foo extends A
* }
* }
*
* The `Foo` class introduced dependency on `A` by inheritance. However, only non local classes
* are tracked in dependency graph so dependency from `Foo` is mapped to `Outer` (which is non
* local class that contains `Foo`).
*
* Why don't we just express this situation as `Outer` depending on `A` by regular inheritance
* dependency? Because inheritance dependencies are invalidated transitively. It would mean that
* in case `A` is changed, all classes inheriting from `Outer` would be invalidated too. This
* suboptimal because classes inheriting from `Outer` cannot be affected by changes to `A`.
*
* Why not map it to `memberRef` relation that is not invalidated transitively? Because `memberRef`
* dependencies are subject to name hashing pruning but this is incorrect for inheritance dependencies.
* Hence we need a special relation that expresses dependencies introduced by inheritance but is not
* invalidated transitively.
*
* See https://github.com/sbt/sbt/issues/1104#issuecomment-169146039 for more details.
*/
private[inc] def localInheritance: ClassDependencies
private[inc] def macroExpansion: ClassDependencies
/** The relation between a source file and the fully qualified names of classes generated from it.*/
def classes: Relation[VirtualFileRef, String]
/**
* Relation between source files and _unqualified_ term and type names used in given source file.
*/
private[inc] def names: Relations.UsedNames
private[inc] def copy(
srcProd: Relation[VirtualFileRef, VirtualFileRef] = srcProd,
libraryDep: Relation[VirtualFileRef, VirtualFileRef] = libraryDep,
libraryClassName: Relation[VirtualFileRef, String] = libraryClassName,
internalDependencies: InternalDependencies = internalDependencies,
externalDependencies: ExternalDependencies = externalDependencies,
classes: Relation[VirtualFileRef, String] = classes,
names: Relations.UsedNames = names,
productClassName: Relation[String, String] = productClassName,
): Relations
}
object Relations {
type UsedNames = inc.UsedNames
/** Tracks internal and external source dependencies for a specific dependency type, such as direct or inherited.*/
private[inc] final class ClassDependencies(
val internal: Relation[String, String],
val external: Relation[String, String]
) {
def addInternal(className: String, dependsOn: Iterable[String]): ClassDependencies =
new ClassDependencies(internal + (className, dependsOn), external)
def addExternal(className: String, dependsOn: Iterable[String]): ClassDependencies =
new ClassDependencies(internal, external + (className, dependsOn))
/** Drops all dependency mappings from `sources`. Acts naively, i.e., doesn't externalize internal deps on removed files.*/
def --(classNames: Iterable[String]): ClassDependencies =
new ClassDependencies(internal -- classNames, external -- classNames)
def ++(o: ClassDependencies): ClassDependencies =
new ClassDependencies(internal ++ o.internal, external ++ o.external)
override def equals(other: Any) = other match {
case o: ClassDependencies => internal == o.internal && external == o.external
case _ => false
}
override def toString: String = s"ClassDependencies(internal = $internal, external = $external)"
override def hashCode = (internal, external).hashCode
}
private[sbt] def getOrEmpty[A, B, K](m: Map[K, Relation[A, B]], k: K): Relation[A, B] =
m.getOrElse(k, Relation.empty)
def empty: Relations = new MRelationsNameHashing(
srcProd = Relation.empty,
libraryDep = Relation.empty,
libraryClassName = Relation.empty,
internalDependencies = InternalDependencies.empty,
externalDependencies = ExternalDependencies.empty,
classes = Relation.empty,
names = UsedNames.fromMultiMap(Map.empty),
productClassName = Relation.empty
)
private[inc] def make(
srcProd: Relation[VirtualFileRef, VirtualFileRef],
libraryDep: Relation[VirtualFileRef, VirtualFileRef],
libraryClassName: Relation[VirtualFileRef, String],
internalDependencies: InternalDependencies,
externalDependencies: ExternalDependencies,
classes: Relation[VirtualFileRef, String],
names: Relations.UsedNames,
productClassName: Relation[String, String]
): Relations =
new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies = internalDependencies,
externalDependencies = externalDependencies,
classes,
names,
productClassName
)
private[inc] def makeClassDependencies(
internal: Relation[String, String],
external: Relation[String, String]
): ClassDependencies =
new ClassDependencies(internal, external)
}
private[inc] object DependencyCollection {
/**
* Combine `m1` and `m2` such that the result contains all the dependencies they represent.
* `m1` is expected to be smaller than `m2`.
*/
def joinMaps[T](
m1: Map[DependencyContext, Relation[String, T]],
m2: Map[DependencyContext, Relation[String, T]]
) =
m1.foldLeft(m2) {
case (tmp, (key, values)) => tmp.updated(key, tmp.getOrElse(key, Relation.empty) ++ values)
}
}
private[inc] object InternalDependencies {
/**
* Constructs an empty `InternalDependencies`
*/
def empty = InternalDependencies(Map.empty)
}
private case class InternalDependencies(
dependencies: Map[DependencyContext, Relation[String, String]]
) {
/**
* Adds `dep` to the dependencies
*/
def +(dep: InternalDependency): InternalDependencies =
InternalDependencies(
dependencies.updated(
dep.context,
dependencies
.getOrElse(dep.context, Relation.empty) + (dep.sourceClassName, dep.targetClassName)
)
)
/**
* Adds all `deps` to the dependencies
*/
def ++(deps: Iterable[InternalDependency]): InternalDependencies = deps.foldLeft(this)(_ + _)
def ++(deps: InternalDependencies): InternalDependencies =
InternalDependencies(DependencyCollection.joinMaps(dependencies, deps.dependencies))
/**
* Removes all dependencies from `sources` to another file from the dependencies
*/
def --(classes: Iterable[String]): InternalDependencies = {
InternalDependencies(
dependencies.iterator
.map { case (k, v) => k -> (v -- classes) }
.filter(_._2.size > 0)
.toMap
)
}
}
private[inc] object ExternalDependencies {
/**
* Constructs an empty `ExternalDependencies`
*/
def empty = ExternalDependencies(Map.empty)
}
private case class ExternalDependencies(
dependencies: Map[DependencyContext, Relation[String, String]]
) {
/**
* Adds `dep` to the dependencies
*/
def +(dep: ExternalDependency): ExternalDependencies =
ExternalDependencies(
dependencies.updated(
dep.context,
dependencies
.getOrElse(
dep.context,
Relation.empty
) + (dep.sourceClassName, dep.targetProductClassName)
)
)
/**
* Adds all `deps` to the dependencies
*/
def ++(deps: Iterable[ExternalDependency]): ExternalDependencies = deps.foldLeft(this)(_ + _)
def ++(deps: ExternalDependencies): ExternalDependencies =
ExternalDependencies(DependencyCollection.joinMaps(dependencies, deps.dependencies))
/**
* Removes all dependencies from `sources` to another file from the dependencies
*/
def --(classNames: Iterable[String]): ExternalDependencies =
ExternalDependencies(
dependencies.iterator
.map { case (k, v) => k -> (v -- classNames) }
.filter(_._2.size > 0)
.toMap
)
}
/**
* `srcProd` is a relation between a source file and a product: (source, product).
* Note that some source files may not have a product and will not be included in this relation.
*
* `libraryDeps` is a relation between a source file and a library dependency: (source, library dependency).
* This only includes dependencies on classes and jars that do not have a corresponding source/API to track instead.
* A class or jar with a corresponding source should only be tracked in one of the source dependency relations.
* `libraryClassName` is a relationship between a library JAR file and class names.
*
* `classes` is a relation between a source file and its generated fully-qualified class names.
*/
private class MRelationsNameHashing(
val srcProd: Relation[VirtualFileRef, VirtualFileRef],
val libraryDep: Relation[VirtualFileRef, VirtualFileRef],
val libraryClassName: Relation[VirtualFileRef, String],
val internalDependencies: InternalDependencies,
val externalDependencies: ExternalDependencies,
val classes: Relation[VirtualFileRef, String],
val names: Relations.UsedNames,
val productClassName: Relation[String, String]
) extends Relations {
def allSources: collection.Set[VirtualFileRef] = srcProd._1s
def allProducts: collection.Set[VirtualFileRef] = srcProd._2s
def allLibraryDeps: collection.Set[VirtualFileRef] = libraryDep._2s
def allExternalDeps: collection.Set[String] = externalClassDep._2s
def classNames(src: VirtualFileRef): Set[String] = classes.forward(src)
def definesClass(name: String): Set[VirtualFileRef] = classes.reverse(name)
def products(src: VirtualFileRef): Set[VirtualFileRef] = srcProd.forward(src)
def produced(prod: VirtualFileRef): Set[VirtualFileRef] = srcProd.reverse(prod)
def libraryDeps(src: VirtualFileRef): Set[VirtualFileRef] = libraryDep.forward(src)
def usesLibrary(dep: VirtualFileRef): Set[VirtualFileRef] = libraryDep.reverse(dep)
def libraryClassNames(lib: VirtualFileRef): Set[String] = libraryClassName.forward(lib)
def libraryDefinesClass(name: String): Set[VirtualFileRef] = libraryClassName.reverse(name)
def internalClassDep: Relation[String, String] = memberRef.internal
def externalClassDep: Relation[String, String] = memberRef.external
def internalClassDeps(className: String): Set[String] =
internalClassDep.forward(className)
def usesInternalClass(className: String): Set[String] =
internalClassDep.reverse(className)
def externalDeps(className: String): Set[String] = externalClassDep.forward(className)
def usesExternal(className: String): Set[String] = externalClassDep.reverse(className)
def addProducts(src: VirtualFileRef, products: Iterable[VirtualFileRef]): Relations =
new MRelationsNameHashing(
srcProd ++ products.map(p => (src, p)),
libraryDep,
libraryClassName,
internalDependencies,
externalDependencies,
classes,
names,
productClassName,
)
private[inc] def addClasses(src: VirtualFileRef, classes: Iterable[(String, String)]): Relations =
new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies,
externalDependencies,
this.classes ++ classes.map(c => (src, c._1)),
names,
productClassName = productClassName ++ classes
)
def addInternalSrcDeps(src: VirtualFileRef, deps: Iterable[InternalDependency]) =
new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies = internalDependencies ++ deps,
externalDependencies,
classes,
names,
productClassName,
)
def addExternalDeps(src: VirtualFileRef, deps: Iterable[ExternalDependency]) =
new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies,
externalDependencies = externalDependencies ++ deps,
classes,
names,
productClassName,
)
def addLibraryDeps(src: VirtualFileRef, deps: Iterable[(VirtualFileRef, String, XStamp)]) =
new MRelationsNameHashing(
srcProd,
libraryDep + (src, deps.map(_._1)),
libraryClassName ++ (deps.map(d => d._1 -> d._2)),
internalDependencies,
externalDependencies,
classes,
names,
productClassName,
)
private[inc] def addUsedNames(data: Relations.UsedNames): Relations = {
new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies,
externalDependencies,
classes,
names = if (names.isEmpty) data else names ++ data,
productClassName,
)
}
override def inheritance: ClassDependencies =
new ClassDependencies(
internalDependencies.dependencies.getOrElse(DependencyByInheritance, Relation.empty),
externalDependencies.dependencies.getOrElse(DependencyByInheritance, Relation.empty)
)
override def localInheritance: ClassDependencies =
new ClassDependencies(
internalDependencies.dependencies.getOrElse(LocalDependencyByInheritance, Relation.empty),
externalDependencies.dependencies.getOrElse(LocalDependencyByInheritance, Relation.empty)
)
override def memberRef: ClassDependencies =
new ClassDependencies(
internalDependencies.dependencies.getOrElse(DependencyByMemberRef, Relation.empty),
externalDependencies.dependencies.getOrElse(DependencyByMemberRef, Relation.empty)
)
override def macroExpansion: ClassDependencies =
new ClassDependencies(
internalDependencies.dependencies.getOrElse(DependencyByMacroExpansion, Relation.empty),
externalDependencies.dependencies.getOrElse(DependencyByMacroExpansion, Relation.empty)
)
def ++(o: Relations): Relations = {
new MRelationsNameHashing(
srcProd ++ o.srcProd,
libraryDep ++ o.libraryDep,
libraryClassName ++ o.libraryClassName,
internalDependencies = internalDependencies ++ o.internalDependencies,
externalDependencies = externalDependencies ++ o.externalDependencies,
classes ++ o.classes,
names = names ++ o.names,
productClassName = productClassName ++ o.productClassName
)
}
def --(sources: Iterable[VirtualFileRef]) = {
val classesInSources = sources.flatMap(classNames)
new MRelationsNameHashing(
srcProd -- sources,
libraryDep -- sources,
libraryClassName,
internalDependencies = internalDependencies -- classesInSources,
externalDependencies = externalDependencies -- classesInSources,
classes -- sources,
names = names -- classesInSources,
productClassName = productClassName -- classesInSources
)
}
def copy(
srcProd: Relation[VirtualFileRef, VirtualFileRef] = srcProd,
libraryDep: Relation[VirtualFileRef, VirtualFileRef] = libraryDep,
libraryClassName: Relation[VirtualFileRef, String] = libraryClassName,
internalDependencies: InternalDependencies = internalDependencies,
externalDependencies: ExternalDependencies = externalDependencies,
classes: Relation[VirtualFileRef, String] = classes,
names: Relations.UsedNames = names,
productClassName: Relation[String, String] = productClassName,
): Relations = new MRelationsNameHashing(
srcProd,
libraryDep,
libraryClassName,
internalDependencies,
externalDependencies,
classes,
names,
productClassName,
)
override def equals(other: Any) = other match {
case o: MRelationsNameHashing =>
srcProd == o.srcProd && libraryDep == o.libraryDep && memberRef == o.memberRef &&
inheritance == o.inheritance && classes == o.classes
case _ => false
}
override def hashCode =
List(srcProd, libraryDep, libraryClassName, memberRef, inheritance, classes).hashCode
/** Making large Relations a little readable. */
private val userDir = sys.props("user.dir").stripSuffix("/") + "/"
private def nocwd(s: String) = s.stripPrefix(userDir)
private def line_s(k: Any, v: Any) = s" ${nocwd(s"$k")} -> ${nocwd(s"$v")}\n"
def relation_s(r: Relation[?, ?]) = {
if (r.forwardMap.isEmpty) "Relation [ ]"
else r.all.toSeq.map(kv => line_s(kv._1, kv._2)).sorted.mkString("Relation [\n", "", "]")
}
def usedNames_s(r: Relations.UsedNames) = {
if (r.isEmpty) "UsedNames [ ]"
else {
val all = r.iterator.flatMap { case (a, bs) => bs.iterator.map(b => (a, b)) }
all.map(kv => line_s(kv._1, kv._2)).toSeq.sorted.mkString("UsedNames [\n", "", "]")
}
}
override def toString: String = {
def deps_s(m: Map[?, Relation[?, ?]]) =
m.iterator.map {
case (k, vs) => s"\n $k ${relation_s(vs)}"
}.mkString
s"""
|Relations:
| products: ${relation_s(srcProd)}
| library deps: ${relation_s(libraryDep)}
| library class names: ${relation_s(libraryClassName)}
| internalDependencies: ${deps_s(internalDependencies.dependencies)}
| externalDependencies: ${deps_s(externalDependencies.dependencies)}
| class names: ${relation_s(classes)}
| used names: ${usedNames_s(names)}
| product class names: ${relation_s(productClassName)}
""".trim.stripMargin
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy