mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-15 20:26:04 +07:00
[uast] IDEA-265918 support buildString
GitOrigin-RevId: 7dbdc596d42c62cc779c6ff20a4ddbe7b1fcc2f0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
cca06385ef
commit
ff06a73629
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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'"
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = " "
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user