[uast] IDEA-265918 support buildString

GitOrigin-RevId: 7dbdc596d42c62cc779c6ff20a4ddbe7b1fcc2f0
This commit is contained in:
aleksandr.izmaylov
2021-05-14 19:12:33 +03:00
committed by intellij-monorepo-bot
parent cca06385ef
commit ff06a73629
8 changed files with 401 additions and 50 deletions

View File

@@ -0,0 +1,49 @@
package org.jetbrains.uast.test.kotlin.analysis
import com.intellij.psi.util.PartiallyKnownString
import com.intellij.psi.util.StringEntry
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase
import org.intellij.lang.annotations.Language
import org.jetbrains.kotlin.idea.test.KotlinLightCodeInsightFixtureTestCase
import org.jetbrains.kotlin.idea.test.KotlinWithJdkAndRuntimeLightProjectDescriptor
import org.jetbrains.uast.*
import org.jetbrains.uast.analysis.UStringEvaluator
import kotlin.test.fail as kotlinFail
abstract class AbstractKotlinUStringEvaluatorTest : KotlinLightCodeInsightFixtureTestCase() {
override fun getProjectDescriptor(): LightProjectDescriptor =
KotlinWithJdkAndRuntimeLightProjectDescriptor.INSTANCE
private val PartiallyKnownString.debugConcatenation: String
get() = buildString {
for (segment in segments) {
when (segment) {
is StringEntry.Known -> append("'").append(segment.value).append("'")
is StringEntry.Unknown -> {
segment.possibleValues
?.map { it.debugConcatenation }
?.sorted()
?.joinTo(this, "|", "{", "}") { it }
?: append("NULL")
}
}
}
}
protected fun doTest(
@Language("kotlin") source: String,
expected: String,
additionalSetup: () -> Unit = {},
configuration: () -> UStringEvaluator.Configuration = { UStringEvaluator.Configuration() },
additionalAssertions: (PartiallyKnownString) -> Unit = {}
) {
additionalSetup()
val file = myFixture.configureByText("myFile.kt", source)
val elementAtCaret = file.findElementAt(myFixture.caretOffset).getUastParentOfType<UReturnExpression>()?.returnExpression
?: kotlinFail("Cannot find UElement at caret")
val pks = UStringEvaluator().calculateValue(elementAtCaret, configuration()) ?: kotlinFail("Cannot evaluate string")
LightJavaCodeInsightFixtureTestCase.assertEquals(expected, pks.debugConcatenation)
additionalAssertions(pks)
}
}

View File

@@ -0,0 +1,14 @@
package org.jetbrains.uast.test.kotlin.analysis
class KotlinUStringEvaluatorSimpleStringTest : AbstractKotlinUStringEvaluatorTest() {
fun `test string interpolation`() = doTest(
"""
fun simpleStringInterpolation() {
val a = "aaa"
val b = "ccc"
return /*<caret>*/ "${'$'}{a}bbb${'$'}b"
}
""".trimIndent(),
"'aaa''bbb''ccc'"
)
}

View File

@@ -0,0 +1,67 @@
package org.jetbrains.uast.test.kotlin.analysis
import org.jetbrains.uast.analysis.UStringBuilderEvaluator
import org.jetbrains.uast.analysis.UStringEvaluator
class KotlinUStringEvaluatorStringBuilderTest : AbstractKotlinUStringEvaluatorTest() {
fun `test simple buildString`() = doTest(
"""
fun simpleDslStringBuilder(): String {
return /*<caret>*/ buildString {
append("a").append("b")
append("c")
this.append("e")
}
}
""".trimIndent(),
"'''a''b''c''e'",
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(UStringBuilderEvaluator)
)
}
)
fun `test buildString with this update through reference`() = doTest(
"""
fun simpleDslStringBuilderWithVariable(): String {
return /*<caret>*/ buildString {
append("a")
this.append("b")
append("c").append("d")
val sb = append("e")
sb.append("f")
append("g")
sb.append("h")
}
}
""".trimIndent(),
"'''a''b''c''d''e''f''g''h'",
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(UStringBuilderEvaluator)
)
}
)
fun `test buildString with this update through optional references`() = doTest(
"""
fun fn(param: Boolean): String {
return /*<caret>*/ buildString {
val sb1 = StringBuilder()
val sb = if (param) sb1.append("0") else append("1")
append("a")
sb1.append("b")
sb.append("c")
this.append("d")
}
}
""".trimIndent(),
"'''1''a'{''|'c'}'d'",
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(UStringBuilderEvaluator)
)
}
)
}

View File

@@ -5,24 +5,26 @@ import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.registry.Registry
import com.intellij.psi.PsiArrayType
import com.intellij.psi.PsiElement
import com.intellij.util.castSafelyTo
import org.jetbrains.uast.*
import org.jetbrains.uast.visitor.AbstractUastVisitor
import kotlin.collections.HashSet
internal class DependencyGraphBuilder private constructor(
private val currentScope: LocalScopeContext = LocalScopeContext(null),
private var currentDepth: Int,
val dependents: MutableMap<UElement, MutableSet<Dependent>> = mutableMapOf(),
val dependencies: MutableMap<UElement, MutableSet<Dependency>> = mutableMapOf(),
private val implicitReceivers: MutableMap<UCallExpression, UThisExpression> = mutableMapOf(),
val scopesStates: MutableMap<UExpression, UScopeObjectsState> = mutableMapOf()
) : AbstractUastVisitor() {
constructor() : this(currentDepth = 0)
private val elementsProcessedAsReceiver: MutableSet<UExpression> = HashSet()
private val elementsProcessedAsReceiver: MutableSet<UExpression> = mutableSetOf()
private fun createVisitor(scope: LocalScopeContext) =
DependencyGraphBuilder(scope, currentDepth, dependents, dependencies)
DependencyGraphBuilder(scope, currentDepth, dependents, dependencies, implicitReceivers, scopesStates)
inline fun checkedDepthCall(node: UElement, body: () -> Boolean): Boolean {
currentDepth++
@@ -42,6 +44,11 @@ internal class DependencyGraphBuilder private constructor(
override fun visitLambdaExpression(node: ULambdaExpression): Boolean = checkedDepthCall(node) {
ProgressManager.checkCanceled()
val child = currentScope.createChild()
for (parameter in node.parameters) {
child.declareFakeVariable(parameter, parameter.name)
child[parameter.name] = setOf(parameter)
}
val parent = (node.uastParent as? UCallExpression)
parent
?.takeIf { KotlinExtensionConstants.isExtensionFunctionToIgnore(it) }
@@ -50,11 +57,12 @@ internal class DependencyGraphBuilder private constructor(
it.valueParameters.getOrNull(0)?.name ?: KotlinExtensionConstants.DEFAULT_LAMBDA_ARGUMENT_NAME
}?.let {
parent.receiver?.let { receiver ->
child.declareFakeVariable(parent, it)
//child.declareFakeVariable(parent, it)
child[it] = setOf(receiver)
}
}
node.body.accept(createVisitor(child))
scopesStates[node] = child.toUScopeObjectsState()
return@checkedDepthCall true
}
@@ -84,10 +92,19 @@ internal class DependencyGraphBuilder private constructor(
else {
registerDependency(Dependent.CallExpression(i, node, parameter.type), Dependency.ArgumentDependency(argument, node))
}
// TODO: implicit this as receiver argument
argument.takeIf { it == receiver }?.let { elementsProcessedAsReceiver.add(it) }
}
node.getImplicitReceiver()?.let { implicitReceiver ->
registerDependency(Dependent.CommonDependent(node), Dependency.CommonDependency(implicitReceiver))
implicitReceiver.accept(this)
if (node.uastParent !is UReferenceExpression) {
currentScope.setLastPotentialUpdate(THIS_PARAMETER_NAME, node)
}
}
return@checkedDepthCall super.visitCallExpression(node)
}
@@ -104,7 +121,7 @@ internal class DependencyGraphBuilder private constructor(
registerDependency(Dependent.CommonDependent(node), Dependency.CommonDependency(node.selector))
node.receiver.accept(this)
if (node.getOutermostQualified() == node) {
(node.getQualifiedChain().first() as? USimpleNameReferenceExpression)?.identifier?.takeIf { it in currentScope }?.let {
node.getQualifiedChainWithImplicits().first().referenceOrThisIdentifier?.takeIf { it in currentScope }?.let {
currentScope.setLastPotentialUpdate(it, node)
}
}
@@ -142,11 +159,34 @@ internal class DependencyGraphBuilder private constructor(
val potentialDependenciesCandidates = currentScope.getLastPotentialUpdate(node.identifier)
if (potentialDependenciesCandidates != null) {
registerDependency(Dependent.CommonDependent(node), Dependency.PotentialSideEffectDependency(node, potentialDependenciesCandidates, referenceInfo))
registerDependency(Dependent.CommonDependent(node),
Dependency.PotentialSideEffectDependency(potentialDependenciesCandidates, referenceInfo))
}
return@checkedDepthCall super.visitSimpleNameReferenceExpression(node)
}
override fun visitThisExpression(node: UThisExpression): Boolean = checkedDepthCall(node) {
ProgressManager.checkCanceled()
val referenceInfo = DependencyOfReference.ReferenceInfo(THIS_PARAMETER_NAME, currentScope.getReferencedValues(THIS_PARAMETER_NAME))
currentScope[THIS_PARAMETER_NAME]?.let {
registerDependency(
Dependent.CommonDependent(node),
Dependency.BranchingDependency(
it,
referenceInfo
).unwrapIfSingle()
)
}
val potentialDependenciesCandidates = currentScope.getLastPotentialUpdate(THIS_PARAMETER_NAME)
if (potentialDependenciesCandidates != null) {
registerDependency(Dependent.CommonDependent(node),
Dependency.PotentialSideEffectDependency(potentialDependenciesCandidates, referenceInfo))
}
return@checkedDepthCall super.visitThisExpression(node)
}
override fun visitLocalVariable(node: ULocalVariable): Boolean = checkedDepthCall(node) {
ProgressManager.checkCanceled()
val name = node.name
@@ -318,11 +358,23 @@ internal class DependencyGraphBuilder private constructor(
private fun updatePotentialEqualReferences(name: String, initElements: Set<UElement>) {
currentScope.clearPotentialReferences(TEMP_VAR_NAME)
fun identToReferenceInfo(identifier: String): Pair<String, UReferenceExpression?>? {
return identifier.takeIf { id -> id in this.currentScope }?.let { id -> id to null } // simple reference => same references
}
val potentialEqualReferences = initElements
.mapNotNull {
when (it) {
is UQualifiedReferenceExpression -> (it.receiver as? USimpleNameReferenceExpression)?.identifier?.takeIf { id -> id in currentScope }?.let { id -> id to it }
is USimpleNameReferenceExpression -> it.identifier.takeIf { id -> id in currentScope }?.let { id -> id to null } // simple reference => same references
is UQualifiedReferenceExpression -> it.getQualifiedChainWithImplicits().firstOrNull()?.referenceOrThisIdentifier
?.takeIf { id -> id in currentScope }
?.let { id -> id to it }
is USimpleNameReferenceExpression -> identToReferenceInfo(it.identifier)
is UThisExpression -> identToReferenceInfo(THIS_PARAMETER_NAME)
is UCallExpression -> it.getImplicitReceiver()?.takeIf { THIS_PARAMETER_NAME in currentScope }
?.let { implicitThis ->
THIS_PARAMETER_NAME to UFakeQualifiedReferenceExpression(implicitThis, it, it.uastParent)
}
else -> null
}
}
@@ -337,10 +389,30 @@ internal class DependencyGraphBuilder private constructor(
private fun registerDependency(dependent: Dependent, dependency: Dependency) {
if (dependency !is Dependency.PotentialSideEffectDependency) {
for (el in dependency.elements) {
dependents.getOrPut(el) { HashSet() }.add(dependent)
dependents.getOrPut(el) { mutableSetOf() }.add(dependent)
}
}
dependencies.getOrPut(dependent.element) { HashSet() }.add(dependency)
dependencies.getOrPut(dependent.element) { mutableSetOf() }.add(dependency)
}
private fun UCallExpression.getImplicitReceiver(): UExpression? {
return if (hasImplicitReceiver(this) && THIS_PARAMETER_NAME in currentScope) {
implicitReceivers.getOrPut(this) { UFakeThisExpression(uastParent) }
}
else {
null
}
}
private fun UQualifiedReferenceExpression.getQualifiedChainWithImplicits(): List<UExpression> {
val chain = getQualifiedChain()
val firstElement = chain.firstOrNull()
return if (firstElement is UCallExpression) {
listOfNotNull(firstElement.getImplicitReceiver()) + chain
}
else {
chain
}
}
companion object {
@@ -354,9 +426,9 @@ private typealias SideEffectChangeCandidate = Dependency.PotentialSideEffectDepe
private typealias DependencyEvidence = Dependency.PotentialSideEffectDependency.DependencyEvidence
private typealias CandidatesTree = Dependency.PotentialSideEffectDependency.CandidatesTree
class LocalScopeContext(private val parent: LocalScopeContext?) {
private val definedInScopeVariables = HashSet<UElement>()
private val definedInScopeVariablesNames = HashSet<String>()
private class LocalScopeContext(private val parent: LocalScopeContext?) {
private val definedInScopeVariables = mutableSetOf<UElement>()
private val definedInScopeVariablesNames = mutableSetOf<String>()
private val lastAssignmentOf = mutableMapOf<UElement, Set<UElement>>()
private val lastDeclarationOf = mutableMapOf<String?, UElement>()
@@ -388,6 +460,7 @@ class LocalScopeContext(private val parent: LocalScopeContext?) {
fun declareFakeVariable(element: UElement, name: String) {
definedInScopeVariablesNames.add(name)
referencesModel.assignValueIfNotAssigned(name)
lastDeclarationOf[name] = element
}
@@ -415,8 +488,8 @@ class LocalScopeContext(private val parent: LocalScopeContext?) {
lastPotentialUpdatesOf[variable] = CandidatesTree.fromCandidate(
SideEffectChangeCandidate(
updateElements.first(),
DependencyEvidence(),
dependencyWitnessValues = referencesModel.getAllTargetsForReference(variable))
DependencyEvidence(),
dependencyWitnessValues = referencesModel.getAllTargetsForReference(variable))
)
}
else {
@@ -453,7 +526,7 @@ class LocalScopeContext(private val parent: LocalScopeContext?) {
fun mergeWith(others: Iterable<LocalScopeContext>) {
for (variable in variables) {
this[variable] = HashSet<UElement>().apply {
this[variable] = mutableSetOf<UElement>().apply {
for (other in others) {
other[variable]?.let { addAll(it) }
}
@@ -486,6 +559,12 @@ class LocalScopeContext(private val parent: LocalScopeContext?) {
return referencesModel.getAllTargetsForReference(identifier)
}
fun toUScopeObjectsState(): UScopeObjectsState {
val variableValueMarks = definedInScopeVariablesNames.associateWith { referencesModel.getAllTargetsForReference(it) }
val lastUpdates = definedInScopeVariablesNames.mapNotNull { getLastPotentialUpdate(it)?.let { tree -> it to tree } }.toMap()
return UScopeObjectsState(lastUpdates, variableValueMarks)
}
private class ReferencesModel(private val parent: ReferencesModel?) {
private val referencesTargets = mutableMapOf<String, MutableMap<UValueMark, DependencyEvidence>>()
private val targetsReferences = mutableMapOf<UValueMark, MutableMap<String, DependencyEvidence>>()
@@ -568,4 +647,48 @@ private fun combineEvidences(ownEvidence: DependencyEvidence, otherEvidence: Dep
private const val UAST_KT_ELVIS_NAME = "elvis"
private const val TEMP_VAR_NAME = "@$,()"
private const val TEMP_VAR_NAME = "@$,()"
private const val THIS_PARAMETER_NAME = "<this>"
private fun hasImplicitReceiver(callExpression: UCallExpression): Boolean =
callExpression.receiver == null && callExpression.receiverType != null
private val UExpression?.referenceOrThisIdentifier: String?
get() = when (this) {
is USimpleNameReferenceExpression -> identifier
is UThisExpression -> THIS_PARAMETER_NAME
else -> null
}
private class UFakeThisExpression(override val uastParent: UElement?) : UThisExpression, UFakeExpression {
override val label: String?
get() = null
override val labelIdentifier: UIdentifier?
get() = null
}
private class UFakeQualifiedReferenceExpression(
override val receiver: UExpression,
override val selector: UExpression,
override val uastParent: UElement?
) : UQualifiedReferenceExpression, UFakeExpression {
override val accessType: UastQualifiedExpressionAccessType
get() = UastQualifiedExpressionAccessType.SIMPLE
override val resolvedName: String?
get() = null
}
private interface UFakeExpression : UExpression, UResolvable {
@Suppress("OverridingDeprecatedMember")
override val psi: PsiElement?
get() = null
@JvmDefault
override val uAnnotations: List<UAnnotation>
get() = emptyList()
override fun resolve(): PsiElement? = null
}

View File

@@ -82,6 +82,11 @@ class UStringEvaluator {
if (builderEvaluator != null) {
return calculateBuilder(graph, element, configuration, builderEvaluator, null, null)
}
val dslEvaluator = configuration.getDslEvaluatorForCall(element)
if (dslEvaluator != null) {
val (builderLikeEvaluator, methodDescriptor) = dslEvaluator
return calculateDsl(graph, element, configuration, builderLikeEvaluator, methodDescriptor)
}
if (element.resolve()?.let { configuration.methodsToAnalyzePattern.accepts(it) } == true) {
return PartiallyKnownString(
StringEntry.Unknown(element.sourcePsi, element.ownTextRange,
@@ -174,14 +179,48 @@ class UStringEvaluator {
return results
}
private fun calculateDsl(
graph: UastLocalUsageDependencyGraph,
element: UCallExpression,
configuration: Configuration,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>,
dslMethodDescriptor: DslMethodDescriptor<PartiallyKnownString?>
): PartiallyKnownString? {
val lambda = dslMethodDescriptor.lambdaDescriptor.lambdaPlace.getLambda(element) ?: return null
val parameter = dslMethodDescriptor.lambdaDescriptor.getLambdaParameter(lambda) ?: return null
val (lastVariablesUpdates, variableToValueMarks) = graph.scopesObjectsStates[lambda] ?: return null
val marks = variableToValueMarks[parameter.name]
val candidates = lastVariablesUpdates[parameter.name]?.selectPotentialCandidates {
(marks == null || marks.intersect(it.dependencyWitnessValues).isNotEmpty()) &&
provePossibleDependency(it.dependencyEvidence, builderEvaluator)
} ?: return null
return candidates.mapNotNull {
calculateBuilder(graph, it.updateElement, configuration, builderEvaluator, it.dependencyWitnessValues, marks) { declaration ->
if (declaration == parameter) {
dslMethodDescriptor.lambdaDescriptor.lambdaArgumentValueProvider()
}
else {
null
}
}
}.collapse(element)
}
private fun calculateBuilder(
graph: UastLocalUsageDependencyGraph,
element: UElement,
configuration: Configuration,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>,
currentObjectsToAnalyze: Collection<UValueMark>?,
originalObjectsToAnalyze: Collection<UValueMark>?
originalObjectsToAnalyze: Collection<UValueMark>?,
declarationEvaluator: (UDeclaration) -> PartiallyKnownString? = { null }
): PartiallyKnownString? {
if (element is UDeclaration) {
val value = declarationEvaluator(element)
if (value != null) return value
}
if (element is UCallExpression) {
val methodEvaluator = builderEvaluator.methodDescriptions.entries.firstOrNull { (pattern, _) -> pattern.accepts(element.resolve()) }
if (methodEvaluator != null) {
@@ -193,7 +232,7 @@ class UStringEvaluator {
) {
is Dependency.BranchingDependency -> {
val branchResult = dependency.elements.mapNotNull {
calculateBuilder(graph, it, configuration, builderEvaluator, currentObjectsToAnalyze, originalObjectsToAnalyze)
calculateBuilder(graph, it, configuration, builderEvaluator, currentObjectsToAnalyze, originalObjectsToAnalyze, declarationEvaluator)
}.collapse(element)
methodEvaluator.value(element, branchResult, this, configuration, isStrict)
}
@@ -204,7 +243,8 @@ class UStringEvaluator {
configuration,
builderEvaluator,
currentObjectsToAnalyze,
originalObjectsToAnalyze
originalObjectsToAnalyze,
declarationEvaluator
)
methodEvaluator.value(element, result, this, configuration, isStrict)
}
@@ -237,7 +277,8 @@ class UStringEvaluator {
if (variants?.size == 1) {
variants.single()
} else {
}
else {
PartiallyKnownString(StringEntry.Unknown(
element.sourcePsi!!,
element.ownTextRange,
@@ -251,7 +292,8 @@ class UStringEvaluator {
configuration,
builderEvaluator,
currentObjectsToAnalyze,
originalObjectsToAnalyze
originalObjectsToAnalyze,
declarationEvaluator
)
is Dependency.PotentialSideEffectDependency -> if (!builderEvaluator.allowSideEffects) null
else {
@@ -263,7 +305,8 @@ class UStringEvaluator {
configuration,
builderEvaluator,
candidate.dependencyWitnessValues,
originalObjectsToAnalyze ?: dependency.referenceInfo?.possibleReferencedValues
originalObjectsToAnalyze ?: dependency.referenceInfo?.possibleReferencedValues,
declarationEvaluator
)
}
.collapse(element)
@@ -281,7 +324,7 @@ class UStringEvaluator {
visitedEvidences += evidence
val result = (evidence.evidenceElement == null || builderEvaluator.isExpressionReturnSelf(evidence.evidenceElement)) &&
(evidence.requires.isEmpty() || evidence.requires.all { provePossibleDependency(it, builderEvaluator, visitedEvidences) })
(evidence.requires.isEmpty() || evidence.requires.all { provePossibleDependency(it, builderEvaluator, visitedEvidences) })
visitedEvidences -= evidence
return result
@@ -316,6 +359,8 @@ class UStringEvaluator {
interface BuilderLikeExpressionEvaluator<T> {
val buildMethod: ElementPattern<PsiMethod>
val dslBuildMethodDescriptor: DslMethodDescriptor<T>?
val allowSideEffects: Boolean
val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, T, UStringEvaluator, Configuration, isStrict: Boolean) -> T>
@@ -323,6 +368,35 @@ class UStringEvaluator {
fun isExpressionReturnSelf(expression: UReferenceExpression): Boolean = false
}
data class DslMethodDescriptor<T>(
val methodPattern: ElementPattern<PsiMethod>,
val lambdaDescriptor: DslLambdaDescriptor<T>
) {
fun accepts(method: PsiMethod?) = methodPattern.accepts(method)
}
data class DslLambdaDescriptor<T>(
val lambdaPlace: LambdaPlace,
val lambdaArgumentIndex: Int,
val lambdaArgumentValueProvider: () -> T?
) {
internal fun getLambdaParameter(lambda: ULambdaExpression): UParameter? {
return lambda.parameters.getOrNull(lambdaArgumentIndex)
}
}
sealed class LambdaPlace {
internal abstract fun getLambda(callExpression: UCallExpression): ULambdaExpression?
object Last : LambdaPlace() {
override fun getLambda(callExpression: UCallExpression): ULambdaExpression? {
val method = callExpression.resolve() ?: return null
val lastIndex = method.parameters.lastIndex
return callExpression.getArgumentForParameter(lastIndex) as? ULambdaExpression
}
}
}
data class Configuration(
val methodCallDepth: Int = 1,
val parameterUsagesDepth: Int = 1,
@@ -339,6 +413,15 @@ class UStringEvaluator {
internal fun getBuilderEvaluatorForCall(callExpression: UCallExpression): BuilderLikeExpressionEvaluator<PartiallyKnownString?>? {
return builderEvaluators.firstOrNull { it.buildMethod.accepts(callExpression.resolve()) }
}
internal fun getDslEvaluatorForCall(
callExpression: UCallExpression
): Pair<BuilderLikeExpressionEvaluator<PartiallyKnownString?>, DslMethodDescriptor<PartiallyKnownString?>>? {
return builderEvaluators.firstOrNull { it.dslBuildMethodDescriptor?.accepts(callExpression.resolve()) == true }
?.let { evaluator ->
evaluator.dslBuildMethodDescriptor?.let { evaluator to it }
}
}
}
}

View File

@@ -24,7 +24,8 @@ import kotlin.collections.HashSet
@ApiStatus.Experimental
class UastLocalUsageDependencyGraph private constructor(
val dependents: Map<UElement, Set<Dependent>>,
val dependencies: Map<UElement, Set<Dependency>>
val dependencies: Map<UElement, Set<Dependency>>,
val scopesObjectsStates: Map<UExpression, UScopeObjectsState>
) {
companion object {
private val DEPENDENCY_GRAPH_KEY = Key.create<CachedValue<UastLocalUsageDependencyGraph>>("uast.local.dependency.graph")
@@ -62,7 +63,7 @@ class UastLocalUsageDependencyGraph private constructor(
}
}
}
return UastLocalUsageDependencyGraph(visitor.dependents, visitor.dependencies)
return UastLocalUsageDependencyGraph(visitor.dependents, visitor.dependencies, visitor.scopesStates)
}
/**
@@ -84,7 +85,8 @@ class UastLocalUsageDependencyGraph private constructor(
// TODO: handle user data holders
return UastLocalUsageDependencyGraph(
dependents = methodAndCallerMaps.dependentsMap,
dependencies = methodAndCallerMaps.dependenciesMap
dependencies = methodAndCallerMaps.dependenciesMap,
scopesObjectsStates = methodGraph.scopesObjectsStates
)
}
}
@@ -164,6 +166,11 @@ class UastLocalUsageDependencyGraph private constructor(
}
}
data class UScopeObjectsState(
val lastVariablesUpdates: Map<String, Dependency.PotentialSideEffectDependency.CandidatesTree>,
val variableToValueMarks: Map<String, Collection<UValueMark>>
)
//region Dump to PlantUML section
@ApiStatus.Internal
fun UastLocalUsageDependencyGraph.dumpVisualisation(): String {
@@ -178,8 +185,8 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
fun elementName(uElement: UElement) = "UElement_${elementToID[uElement]}"
append("@startuml").append("\n")
for ((element, id) in elementToID) {
append("object UElement_").append(id).append(" {\n")
indent().append("render=\"").append(element.asRenderString().escape()).append("\"\n")
append("object UElement_").append(id).appendLine(" {")
indent().append("render=\"").append(element.asRenderString().escape()).appendLine("\"")
element.sourcePsi?.let { psiElement ->
PsiDocumentManager.getInstance(psiElement.project).getDocument(psiElement.containingFile)?.let { document ->
val line = document.getLineNumber(psiElement.textOffset)
@@ -188,10 +195,10 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
indent().append("line=").append("\"").append(line + 1)
append(": ")
append(document.charsSequence.subSequence(begin, end).toString().escape().trim())
append("\"\n")
appendLine("\"")
}
}
append("}\n")
appendLine("}")
}
val dependencyToID = dependencies.values.flatten().mapIndexed { index, dependency -> dependency to index }.toMap()
@@ -200,11 +207,11 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
append("object ")
append(dependency.javaClass.simpleName).append("_").append(depIndex)
if (dependency is DependencyOfReference) {
append(" {\n")
indent().append("values = ").append(dependency.referenceInfo?.possibleReferencedValues).append("\n")
appendLine(" {")
indent().append("values = ").append(dependency.referenceInfo?.possibleReferencedValues).appendLine()
append("}")
}
append("\n")
appendLine()
}
val candidatesTreeToID = dependencies.values.flatten()
@@ -220,7 +227,7 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
var nodeIndexShift = 0
for ((tree, id) in candidatesTreeToID) {
append("package CandidatesTree_").append(id).append(" {\n")
append("package CandidatesTree_").append(id).appendLine(" {")
val nodeToID = tree.allNodes().withIndex().map { (index, node) -> node to index + nodeIndexShift }.toMap()
nodeIndexShift += nodeToID.size
@@ -229,12 +236,12 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
"CandidateNode_${nodeToID[node]}"
for ((node, nodeId) in nodeToID) {
indent().append("object CandidateNode_").append(nodeId).append(" {\n")
indent().indent().append("type = ").append(node.javaClass.simpleName).append("\n")
indent().append("object CandidateNode_").append(nodeId).appendLine(" {")
indent().indent().append("type = ").append(node.javaClass.simpleName).appendLine()
if (node is Dependency.PotentialSideEffectDependency.CandidatesTree.Node.CandidateNode) {
val candidate = node.candidate
indent().indent().append("witness = ").append(candidate.dependencyWitnessValues).append("\n")
indent().indent().append("evidence = ").append(candidate.dependencyEvidence.evidenceElement?.asRenderString()?.escape()).append("\n")
indent().indent().append("witness = ").append(candidate.dependencyWitnessValues).appendLine()
indent().indent().append("evidence = ").append(candidate.dependencyEvidence.evidenceElement?.asRenderString()?.escape()).appendLine()
generateSequence(candidate.dependencyEvidence.requires) { reqs -> reqs.flatMap { req -> req.requires }.takeUnless { it.isEmpty() } }
.flatten()
@@ -288,11 +295,11 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
}
}
append("@enduml").append("\n")
appendLine("@enduml")
}
private fun StringBuilder.edge(begin: String, type: String, end: String) {
append(begin).append(" ").append(type).append(" ").append(end).append("\n")
append(begin).append(" ").append(type).append(" ").append(end).appendLine()
}
private const val INDENT = " "

View File

@@ -8,34 +8,43 @@ import com.intellij.patterns.uast.callExpression
import com.intellij.psi.PsiMethod
import com.intellij.psi.util.PartiallyKnownString
import com.intellij.psi.util.StringEntry
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.UReferenceExpression
import org.jetbrains.uast.USimpleNameReferenceExpression
import org.jetbrains.uast.getQualifiedChain
import org.jetbrains.uast.*
object UStringBuilderEvaluator : UStringEvaluator.BuilderLikeExpressionEvaluator<PartiallyKnownString?> {
override val buildMethod: ElementPattern<PsiMethod>
get() = PsiJavaPatterns.psiMethod().withName("toString").definedInClass("java.lang.StringBuilder")
override val dslBuildMethodDescriptor: UStringEvaluator.DslMethodDescriptor<PartiallyKnownString?>
get() = UStringEvaluator.DslMethodDescriptor(
PsiJavaPatterns.psiMethod().withName("buildString").definedInClass("kotlin.text.StringsKt__StringBuilderKt"),
UStringEvaluator.DslLambdaDescriptor(
UStringEvaluator.LambdaPlace.Last,
0
) { PartiallyKnownString("") }
)
override val allowSideEffects: Boolean
get() = true
override fun isExpressionReturnSelf(expression: UReferenceExpression): Boolean {
val qualifiedChain = expression.getQualifiedChain()
val callPattern = callExpression().withAnyResolvedMethod(PsiJavaPatterns.psiMethod().definedInClass("java.lang.StringBuilder"))
return qualifiedChain.firstOrNull() is USimpleNameReferenceExpression && qualifiedChain.drop(1).all { callPattern.accepts(it) }
return qualifiedChain.firstOrNull().let { it is USimpleNameReferenceExpression || it is UThisExpression } &&
qualifiedChain.drop(1).all { callPattern.accepts(it) }
}
override val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, PartiallyKnownString?, UStringEvaluator, UStringEvaluator.Configuration, Boolean) -> PartiallyKnownString?>
get() = mapOf(
PsiJavaPatterns.psiMethod().withName("append").definedInClass("java.lang.StringBuilder") to { call, currentResult, stringEvaluator, config, isStrict ->
PsiJavaPatterns.psiMethod().withName("append").definedInClass(
"java.lang.StringBuilder") to { call, currentResult, stringEvaluator, config, isStrict ->
val entries = currentResult?.segments?.toMutableList()
?: mutableListOf<StringEntry>(StringEntry.Unknown(call.sourcePsi!!, TextRange(0, 1)))
val argument = call.getArgumentForParameter(0)?.let { argument -> stringEvaluator.calculateValue(argument, config) }
if (argument != null) {
if (isStrict) {
entries.addAll(argument.segments)
} else {
}
else {
entries.add(StringEntry.Unknown(
call.sourcePsi!!,
TextRange(0, call.sourcePsi!!.textLength),

View File

@@ -70,7 +70,6 @@ sealed class Dependency : UserDataHolderBase() {
}
data class PotentialSideEffectDependency(
val currentReference: USimpleNameReferenceExpression,
val candidates: CandidatesTree,
override val referenceInfo: DependencyOfReference.ReferenceInfo? = null
) : Dependency(), DependencyOfReference {