All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.incremental.classpathDiff.Impact.kt Maven / Gradle / Ivy

There is a newer version: 2.1.0-RC
Show newest version
/*
 * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
 * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
 */

package org.jetbrains.kotlin.incremental.classpathDiff

import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.resolve.jvm.JvmClassName
import java.util.*

/**
 * A common interface for all types of impact among classes. For example, if class B extends class A, then class A impacts class B, because
 * if class A has changed, a source file that references class B will need to be recompiled (even though class B has not changed).
 */
internal sealed interface Impact {

    /** Provides an [ImpactedSymbolsResolver] to compute the set of [ProgramSymbol]s impacted by a given set of [ProgramSymbol]s. */
    fun getResolver(allClasses: Iterable): ImpactedSymbolsResolver

    /**
     * Provides an [ImpactingClassesResolver] to compute the set of classes impacting a given set of classes (the reverse of [getResolver]).
     */
    fun getReverseResolver(allClasses: Iterable): ImpactingClassesResolver
}

/**
 * Computes the set of [ProgramSymbol]s that are *directly* impacted by a given set of [ProgramSymbol]s.
 *
 * The returned set is *inclusive* (it contains the given set + the directly impacted ones).
 *
 * This is typically used when computing classpath changes: If class A has changed, and it impacts class B, then a source file that
 * references class B will need to be recompiled (even though class B has not changed).
 */
internal interface ImpactedSymbolsResolver {
    fun getImpactedClasses(classId: ClassId): Set
    fun getImpactedClassMembers(classMembers: ClassMembers): Set
}

/**
 * Computes the set of classes *directly* impacting a given set of classes.
 *
 * The returned set is *inclusive* (it contains the given set + the directly impacting ones).
 *
 * This is typically used when shrinking classpath snapshots: If class A impacts class B, and class B is referenced by a source file, then
 * class A will need to be retained in the shrunk classpath snapshot because the classpath changes computation will need to see class A
 * (see [ImpactedSymbolsResolver]).
 */
internal interface ImpactingClassesResolver {
    fun getImpactingClasses(classId: ClassId): Set
}

/**
 * A composite [Impact] containing all possible concrete impacts. Currently, the types of impact include:
 *   1. [SupertypesInheritorsImpact]
 *   2. [ConstantsInCompanionObjectsImpact]
 */
internal object AllImpacts : Impact {

    private val allImpacts = listOf(SupertypesInheritorsImpact, ConstantsInCompanionObjectsImpact)

    override fun getResolver(allClasses: Iterable): ImpactedSymbolsResolver {
        val resolvers = allImpacts.map { it.getResolver(allClasses) }
        return object : ImpactedSymbolsResolver {
            override fun getImpactedClasses(classId: ClassId): Set {
                return resolvers.flatMapTo(mutableSetOf()) { it.getImpactedClasses(classId) }
            }

            override fun getImpactedClassMembers(classMembers: ClassMembers): Set {
                return resolvers.flatMapTo(mutableSetOf()) { it.getImpactedClassMembers(classMembers) }
            }
        }
    }

    override fun getReverseResolver(allClasses: Iterable): ImpactingClassesResolver {
        val reverseResolvers = allImpacts.map { it.getReverseResolver(allClasses) }
        return object : ImpactingClassesResolver {
            override fun getImpactingClasses(classId: ClassId): Set {
                return reverseResolvers.flatMapTo(mutableSetOf()) { it.getImpactingClasses(classId) }
            }
        }
    }
}

/**
 * Describes the impact between supertypes and inheritors: If a superclass/interface has changed, its subclasses/sub-interfaces will be
 * impacted.
 */
private object SupertypesInheritorsImpact : Impact {

    override fun getResolver(allClasses: Iterable): ImpactedSymbolsResolver {
        val classIdToSubclasses: Map> = getClassIdToSubclassesMap(allClasses)
        return object : ImpactedSymbolsResolver {
            override fun getImpactedClasses(classId: ClassId): Set {
                return classIdToSubclasses[classId] ?: emptySet()
            }

            override fun getImpactedClassMembers(classMembers: ClassMembers): Set {
                return classIdToSubclasses[classMembers.classId]?.let { subclasses ->
                    subclasses.mapTo(mutableSetOf()) { subclass ->
                        ClassMembers(subclass, classMembers.memberNames)
                    }
                } ?: emptySet()
            }
        }
    }

    override fun getReverseResolver(allClasses: Iterable): ImpactingClassesResolver {
        val classIdToSupertypesMap: Map> = getClassIdToSupertypesMap(allClasses)
        return object : ImpactingClassesResolver {
            override fun getImpactingClasses(classId: ClassId): Set {
                return classIdToSupertypesMap[classId] ?: emptySet()
            }
        }
    }

    private fun getClassIdToSubclassesMap(allClasses: Iterable): Map> {
        val classIdToSubclasses = mutableMapOf>()
        getClassIdToSupertypesMap(allClasses).forEach { (classId, supertypes) ->
            supertypes.forEach { supertype ->
                classIdToSubclasses.getOrPut(supertype) { mutableSetOf() }.add(classId)
            }
        }
        return classIdToSubclasses
    }

    private fun getClassIdToSupertypesMap(allClasses: Iterable): Map> {
        val classNameToClassId = allClasses.associate { JvmClassName.byClassId(it.classId) to it.classId }
        return allClasses.mapNotNull { clazz ->
            // Find supertypes that are within `allClasses`, we don't care about those outside `allClasses` (e.g., `java/lang/Object`)
            val supertypes = when (clazz) {
                is RegularKotlinClassSnapshot -> clazz.supertypes.mapNotNullTo(mutableSetOf()) { classNameToClassId[it] }
                is PackageFacadeKotlinClassSnapshot, is MultifileClassKotlinClassSnapshot -> {
                    // These classes may have supertypes (e.g., kotlin/collections/ArraysKt (MULTIFILE_CLASS) extends
                    // kotlin/collections/ArraysKt___ArraysKt (MULTIFILE_CLASS_PART)), but we don't have to use that info during impact
                    // analysis because those inheritors and supertypes should have the same package names, and in package facades only the
                    // package names and member names matter.
                    emptySet()
                }
                is JavaClassSnapshot -> clazz.supertypes.mapNotNullTo(mutableSetOf()) { classNameToClassId[it] }
            }
            if (supertypes.isNotEmpty()) {
                clazz.classId to supertypes
            } else null
        }.toMap()
    }
}

/**
 * Describes the impact between a class and its companion object when the companion object defines some constants.
 *
 * Consider the following source file:
 *    class A {
 *       companion object {
 *          const val CONSTANT = 1
 *       }
 *    }
 *
 * This source file will compile into 2 .class files:
 *   - `A.Companion.class`'s Kotlin metadata describes the name and type of `CONSTANT` but not its value. Its Java bytecode does not define
 *     the constant.
 *   - `A.class`'s Kotlin metadata does not contain `CONSTANT`. However, its Java bytecode defines the constant as follows:
 *         public static final int CONSTANT = 1;
 *
 * Therefore, if the value of the constant has changed in the source file, we will only see a change in the Java bytecode of `A.class`, not
 * in `A.Companion.class` or in the Kotlin metadata of either class.
 *
 * Hence, we will need to detect that `A.CONSTANT` impacts `A.Companion.CONSTANT` because if a source file references
 * `A.Companion.CONSTANT`, it will need to be recompiled when `A.CONSTANT`'s value in `A.class` has changed (even though `A.Companion.class`
 * has not changed).
 *
 * Note: This corner case only applies to *constants' values* defined in *companion objects* (it does not apply to constants' names and
 * types, or top-level constants, or constants in non-companion objects, or inline functions).
 */
private object ConstantsInCompanionObjectsImpact : Impact {

    override fun getResolver(allClasses: Iterable): ImpactedSymbolsResolver {
        val companionObjectToConstants: Map> = allClasses.mapNotNull { clazz ->
            (clazz as? RegularKotlinClassSnapshot)?.constantsInCompanionObject?.let { constants ->
                // We only care about companion objects that define some constants
                if (constants.isNotEmpty()) {
                    clazz.classId to constants
                } else null
            }
        }.toMap()
        val classToCompanionObject: Map = companionObjectToConstants.keys.associateBy { companionObject ->
            // companionObject.parentClassId should be present in `allClasses` as this is a companion object
            companionObject.parentClassId!!
        }

        return object : ImpactedSymbolsResolver {
            override fun getImpactedClasses(classId: ClassId): Set {
                return setOfNotNull(classToCompanionObject[classId])
            }

            override fun getImpactedClassMembers(classMembers: ClassMembers): Set {
                return classToCompanionObject[classMembers.classId]?.let { companionObject ->
                    val constantsInCompanionObject = companionObjectToConstants[companionObject]!!
                    val impactedConstants = classMembers.memberNames.intersect(constantsInCompanionObject.toSet())
                    setOf(ClassMembers(companionObject, impactedConstants))
                } ?: emptySet()
            }
        }
    }

    override fun getReverseResolver(allClasses: Iterable): ImpactingClassesResolver {
        val companionObjects: Set = allClasses.mapNotNullTo(mutableSetOf()) { clazz ->
            (clazz as? RegularKotlinClassSnapshot)?.constantsInCompanionObject?.let { constants ->
                // We only care about companion objects that define some constants
                if (constants.isNotEmpty()) clazz.classId else null
            }
        }

        return object : ImpactingClassesResolver {
            override fun getImpactingClasses(classId: ClassId): Set {
                return if (classId in companionObjects) {
                    // classId.parentClassId should be present in `allClasses` as this is a companion object
                    setOf(classId.parentClassId!!)
                } else emptySet()
            }
        }
    }
}

internal object BreadthFirstSearch {

    /**
     * Finds the set of nodes that are *transitively* reachable from the given set of nodes.
     *
     * The returned set is *inclusive* (it contains the given set + the directly/transitively reachable ones).
     */
    fun  findReachableNodes(nodes: Iterable, edgesProvider: (T) -> Iterable): Set {
        val visitedAndToVisitNodes = nodes.toMutableSet()
        val nodesToVisit = ArrayDeque(nodes.toSet())

        while (nodesToVisit.isNotEmpty()) {
            val nodeToVisit = nodesToVisit.removeFirst()
            val nextNodesToVisit = edgesProvider.invoke(nodeToVisit) - visitedAndToVisitNodes
            visitedAndToVisitNodes.addAll(nextNodesToVisit)
            nodesToVisit.addAll(nextNodesToVisit)
        }
        return visitedAndToVisitNodes
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy