[uast] IDEA-265918 handle conditional assignments

GitOrigin-RevId: 8299beb4088552902e473682f49a59820c907499
This commit is contained in:
aleksandr.izmaylov
2021-05-11 18:46:43 +03:00
committed by intellij-monorepo-bot
parent a130a3e6a7
commit f8c5165105
7 changed files with 305 additions and 93 deletions

View File

@@ -17,7 +17,7 @@ internal class DependencyGraphBuilder private constructor(
val dependencies: MutableMap<UElement, MutableSet<Dependency>> = mutableMapOf(),
) : AbstractUastVisitor() {
constructor(): this(currentDepth = 0)
constructor() : this(currentDepth = 0)
private val elementsProcessedAsReceiver: MutableSet<UExpression> = HashSet()
@@ -128,13 +128,21 @@ internal class DependencyGraphBuilder private constructor(
if (node.uastParent is UReferenceExpression && (node.uastParent as? UQualifiedReferenceExpression)?.receiver != node)
return@checkedDepthCall true
val referenceInfo = DependencyOfReference.ReferenceInfo(node.identifier, currentScope.getReferencedValues(node.identifier))
currentScope[node.identifier]?.let {
registerDependency(Dependent.CommonDependent(node), Dependency.BranchingDependency(it).unwrapIfSingle())
registerDependency(
Dependent.CommonDependent(node),
Dependency.BranchingDependency(
it,
referenceInfo
).unwrapIfSingle()
)
}
val potentialDependenciesCandidates = currentScope.getLastPotentialUpdate(node.identifier)
if (potentialDependenciesCandidates != null) {
registerDependency(Dependent.CommonDependent(node), Dependency.PotentialSideEffectDependency(potentialDependenciesCandidates))
registerDependency(Dependent.CommonDependent(node), Dependency.PotentialSideEffectDependency(node, potentialDependenciesCandidates, referenceInfo))
}
return@checkedDepthCall super.visitSimpleNameReferenceExpression(node)
}
@@ -309,7 +317,7 @@ internal class DependencyGraphBuilder private constructor(
override fun visitField(node: UField): Boolean = true
private fun updatePotentialEqualReferences(name: String, initElements: Set<UElement>) {
currentScope.clearPotentialReferences(name)
currentScope.clearPotentialReferences(TEMP_VAR_NAME)
val potentialEqualReferences = initElements
.mapNotNull {
when (it) {
@@ -319,9 +327,11 @@ internal class DependencyGraphBuilder private constructor(
}
}
for ((potentialEqualReference, evidence) in potentialEqualReferences) {
currentScope.setPotentialEquality(name, potentialEqualReference,
DependencyEvidence(potentialEqualReferences.size == 1, evidence, potentialEqualReference))
currentScope.setPotentialEquality(TEMP_VAR_NAME, potentialEqualReference, DependencyEvidence(evidence))
}
currentScope.clearPotentialReferences(name)
currentScope.setPotentialEquality(name, TEMP_VAR_NAME, DependencyEvidence())
currentScope.clearPotentialReferences(TEMP_VAR_NAME)
}
private fun registerDependency(dependent: Dependent, dependency: Dependency) {
@@ -344,7 +354,7 @@ private typealias SideEffectChangeCandidate = Dependency.PotentialSideEffectDepe
private typealias DependencyEvidence = Dependency.PotentialSideEffectDependency.DependencyEvidence
private typealias CandidatesTree = Dependency.PotentialSideEffectDependency.CandidatesTree
private class LocalScopeContext(private val parent: LocalScopeContext?) {
class LocalScopeContext(private val parent: LocalScopeContext?) {
private val definedInScopeVariables = HashSet<UElement>()
private val definedInScopeVariablesNames = HashSet<String>()
@@ -371,6 +381,7 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
definedInScopeVariables.add(variable)
variable.name?.let {
definedInScopeVariablesNames.add(it)
referencesModel.assignValueIfNotAssigned(it)
lastDeclarationOf[it] = variable
}
}
@@ -387,9 +398,13 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
fun createChild() = LocalScopeContext(this)
fun setLastPotentialUpdate(variable: String, updateElement: UElement) {
lastPotentialUpdatesOf[variable] = CandidatesTree.fromCandidate(SideEffectChangeCandidate(updateElement, DependencyEvidence(true)))
for ((reference, evidence) in getAllPotentialEqualReferences(variable)) {
val newCandidate = SideEffectChangeCandidate(updateElement, evidence)
lastPotentialUpdatesOf[variable] = CandidatesTree.fromCandidate(
SideEffectChangeCandidate(updateElement, DependencyEvidence(),
dependencyWitnessValues = referencesModel.getAllTargetsForReference(variable))
)
for ((reference, evidenceAndWitness) in referencesModel.getAllPossiblyEqualReferences(variable)) {
val (evidence, witness) = evidenceAndWitness
val newCandidate = SideEffectChangeCandidate(updateElement, evidence, witness)
val candidatesForReference = lastPotentialUpdatesOf[reference]
lastPotentialUpdatesOf[reference] = candidatesForReference?.addToBegin(newCandidate) ?: CandidatesTree.fromCandidate(newCandidate)
}
@@ -398,12 +413,17 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
fun setLastPotentialUpdateAsAssignment(variable: String, updateElements: Collection<UElement>) {
if (updateElements.size == 1) {
lastPotentialUpdatesOf[variable] = CandidatesTree.fromCandidate(
SideEffectChangeCandidate(updateElements.first(), DependencyEvidence(true))
SideEffectChangeCandidate(
updateElements.first(),
DependencyEvidence(),
dependencyWitnessValues = referencesModel.getAllTargetsForReference(variable))
)
}
else {
lastPotentialUpdatesOf[variable] = CandidatesTree.fromCandidates(
updateElements.mapTo(mutableSetOf()) { SideEffectChangeCandidate(it, DependencyEvidence(true)) }
updateElements.mapTo(mutableSetOf()) {
SideEffectChangeCandidate(it, DependencyEvidence(), dependencyWitnessValues = referencesModel.getAllTargetsForReference(variable))
}
)
}
}
@@ -419,10 +439,6 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
referencesModel.clearReference(reference)
}
fun getAllPotentialEqualReferences(reference: String): Map<String, DependencyEvidence> {
return referencesModel.getAllPossiblyEqualReferences(reference)
}
val variables: Iterable<UElement>
get() {
return generateSequence(this) { it.parent }
@@ -451,7 +467,7 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
}
}
}.takeUnless { it.isEmpty() }?.let { candidates ->
lastPotentialUpdatesOf[variableName] = CandidatesTree.merge(candidates)
lastPotentialUpdatesOf[variableName] = CandidatesTree.merge(candidates)
}
}
}
@@ -466,35 +482,50 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
}
}
fun getReferencedValues(identifier: String): Collection<UValueMark> {
return referencesModel.getAllTargetsForReference(identifier)
}
private class ReferencesModel(private val parent: ReferencesModel?) {
private class Target
private val referencesTargets = mutableMapOf<String, MutableMap<UValueMark, DependencyEvidence>>()
private val targetsReferences = mutableMapOf<UValueMark, MutableMap<String, DependencyEvidence>>()
private val referencesTargets = mutableMapOf<String, MutableMap<Target, DependencyEvidence>>()
private val targetsReferences = mutableMapOf<Target, MutableSet<String>>()
private fun getAllReferences(referencedValue: UValueMark): Map<String, DependencyEvidence> =
parent?.getAllReferences(referencedValue).orEmpty() + targetsReferences[referencedValue].orEmpty()
private fun getAllReferences(target: Target): Set<String> =
parent?.getAllReferences(target).orEmpty() + targetsReferences[target].orEmpty()
private fun getAllTargets(reference: String): Map<Target, DependencyEvidence> =
private fun getAllTargets(reference: String): Map<UValueMark, DependencyEvidence> =
listOfNotNull(referencesTargets[reference], parent?.getAllTargets(reference)).fold(emptyMap()) { result, current ->
(result.keys + current.keys).associateWith { (result[it] ?: current[it])!! }
}
fun assignValueIfNotAssigned(reference: String) {
if (getAllTargets(reference).isNotEmpty()) return
val evidence = DependencyEvidence()
val newTarget = UValueMark()
referencesTargets[reference] = mutableMapOf(newTarget to evidence)
targetsReferences[newTarget] = mutableMapOf(reference to evidence)
}
fun setPossibleEquality(assigneeReference: String, targetReference: String, evidence: DependencyEvidence) {
val targets = getAllTargets(targetReference).toMutableMap()
if (targets.isEmpty()) {
val newTarget = Target()
referencesTargets[targetReference] = mutableMapOf(newTarget to DependencyEvidence(true)) // equal by default
val newTarget = UValueMark()
val targetEvidence = DependencyEvidence()
referencesTargets[targetReference] = mutableMapOf(newTarget to targetEvidence) // equal by default
referencesTargets.getOrPut(assigneeReference) { mutableMapOf() }[newTarget] = evidence
targetsReferences[newTarget] = mutableSetOf(assigneeReference, targetReference)
targetsReferences[newTarget] = mutableMapOf(
assigneeReference to evidence,
targetReference to targetEvidence
)
return
}
referencesTargets.getOrPut(assigneeReference) { mutableMapOf() }
.putAll(targets.mapValues { (_, evidenceForTarget) -> evidence.copy(requires = evidenceForTarget) })
.putAll(targets.mapValues { (_, evidenceForTarget) -> evidence.copy(requires = listOf(evidenceForTarget)) })
for (target in targets.keys) {
targetsReferences.getOrPut(target) { mutableSetOf() }.add(assigneeReference)
targetsReferences.getOrPut(target) { mutableMapOf() }[assigneeReference] = evidence
}
}
@@ -512,14 +543,29 @@ private class LocalScopeContext(private val parent: LocalScopeContext?) {
}
}
fun getAllPossiblyEqualReferences(reference: String): Map<String, DependencyEvidence> =
getAllTargets(reference)
.map { (target, evidence) -> getAllReferences(target).map { it to evidence } }
fun getAllPossiblyEqualReferences(reference: String): Map<String, Pair<DependencyEvidence, Collection<UValueMark>>> {
val allTargets = getAllTargets(reference)
return allTargets
.map { (target, evidence) ->
getAllReferences(target).map { (currentReference, referenceEvidence) ->
currentReference to (combineEvidences(evidence, referenceEvidence) to allTargets.keys)
}
}
.flatten()
.filter { it.first != reference }
.toMap()
}
fun getAllTargetsForReference(reference: String): Collection<UValueMark> {
return getAllTargets(reference).keys
}
}
}
private const val UAST_KT_ELVIS_NAME = "elvis"
private fun combineEvidences(ownEvidence: DependencyEvidence, otherEvidence: DependencyEvidence): DependencyEvidence =
ownEvidence.copy(requires = ownEvidence.requires + otherEvidence)
private const val UAST_KT_ELVIS_NAME = "elvis"
private const val TEMP_VAR_NAME = "@$,()"

View File

@@ -16,6 +16,7 @@ import com.intellij.util.Plow
import com.intellij.util.containers.addIfNotNull
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.uast.*
import org.jetbrains.uast.analysis.Dependency.PotentialSideEffectDependency.*
import org.jetbrains.uast.visitor.AbstractUastVisitor
@ApiStatus.Experimental
@@ -79,7 +80,7 @@ class UStringEvaluator {
}
val builderEvaluator = configuration.getBuilderEvaluatorForCall(element)
if (builderEvaluator != null) {
return calculateBuilder(graph, element, configuration, builderEvaluator)
return calculateBuilder(graph, element, configuration, builderEvaluator, null, null)
}
if (element.resolve()?.let { configuration.methodsToAnalyzePattern.accepts(it) } == true) {
return PartiallyKnownString(
@@ -177,48 +178,93 @@ class UStringEvaluator {
graph: UastLocalUsageDependencyGraph,
element: UElement,
configuration: Configuration,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>,
currentObjectsToAnalyze: Collection<UValueMark>?,
originalObjectsToAnalyze: Collection<UValueMark>?
): PartiallyKnownString? {
if (element is UCallExpression) {
val methodEvaluator = builderEvaluator.methodDescriptions.entries.firstOrNull { (pattern, _) -> pattern.accepts(element.resolve()) }
if (methodEvaluator != null) {
val dependencies = graph.dependencies[element].orEmpty()
return when (val dependency = dependencies.firstOrNull { it !is Dependency.PotentialSideEffectDependency && it !is Dependency.ArgumentDependency }) {
val isStrict = currentObjectsToAnalyze == originalObjectsToAnalyze
return when (
val dependency = dependencies
.firstOrNull { it !is Dependency.PotentialSideEffectDependency && it !is Dependency.ArgumentDependency }
) {
is Dependency.BranchingDependency -> {
val branchResult = dependency.elements.mapNotNull { calculateBuilder(graph, it, configuration, builderEvaluator) }.collapse(
element)
methodEvaluator.value(element, branchResult, this, configuration)
val branchResult = dependency.elements.mapNotNull {
calculateBuilder(graph, it, configuration, builderEvaluator, currentObjectsToAnalyze, originalObjectsToAnalyze)
}.collapse(element)
methodEvaluator.value(element, branchResult, this, configuration, isStrict)
}
is Dependency.CommonDependency -> {
val result = calculateBuilder(graph, dependency.element, configuration, builderEvaluator)
methodEvaluator.value(element, result, this, configuration)
val result = calculateBuilder(
graph,
dependency.element,
configuration,
builderEvaluator,
currentObjectsToAnalyze,
originalObjectsToAnalyze
)
methodEvaluator.value(element, result, this, configuration, isStrict)
}
is Dependency.PotentialSideEffectDependency -> null // there should not be anything
else -> methodEvaluator.value(element, null, this, configuration)
else -> methodEvaluator.value(element, null, this, configuration, isStrict)
}
}
}
val dependencies = graph.dependencies[element].orEmpty()
val dependency = (dependencies.firstOrNull { it is Dependency.PotentialSideEffectDependency }.takeIf { builderEvaluator.allowSideEffects }
?: dependencies.firstOrNull { it !is Dependency.PotentialSideEffectDependency && it !is Dependency.ArgumentDependency })
val (dependency, candidates) = selectDependency(dependencies, builderEvaluator) {
(originalObjectsToAnalyze == null ||
it.dependencyWitnessValues.intersect(originalObjectsToAnalyze).isNotEmpty()) &&
provePossibleDependency(it.dependencyEvidence, builderEvaluator)
}
if (
dependency is DependencyOfReference &&
originalObjectsToAnalyze != null &&
dependency.referenceInfo?.possibleReferencedValues?.intersect(originalObjectsToAnalyze)?.isEmpty() == true
) {
return null
}
return when (dependency) {
is Dependency.BranchingDependency -> {
PartiallyKnownString(StringEntry.Unknown(
element.sourcePsi!!, element.ownTextRange,
dependency.elements.mapNotNull {
calculateBuilder(graph, it, configuration, builderEvaluator)
}.takeUnless { it.isEmpty() }
))
val variants = dependency.elements.mapNotNull {
calculateBuilder(graph, it, configuration, builderEvaluator, currentObjectsToAnalyze, originalObjectsToAnalyze)
}.takeUnless { it.isEmpty() }
if (variants?.size == 1) {
variants.single()
} else {
PartiallyKnownString(StringEntry.Unknown(
element.sourcePsi!!,
element.ownTextRange,
variants
))
}
}
is Dependency.CommonDependency -> calculateBuilder(graph, dependency.element, configuration, builderEvaluator)
is Dependency.CommonDependency -> calculateBuilder(
graph,
dependency.element,
configuration,
builderEvaluator,
currentObjectsToAnalyze,
originalObjectsToAnalyze
)
is Dependency.PotentialSideEffectDependency -> if (!builderEvaluator.allowSideEffects) null
else {
dependency.candidates
.selectPotentialCandidates { provePossibleDependency(it, builderEvaluator) }
candidates
.mapNotNull { candidate ->
calculateBuilder(graph, candidate.updateElement, configuration, builderEvaluator)
calculateBuilder(
graph,
candidate.updateElement,
configuration,
builderEvaluator,
candidate.dependencyWitnessValues,
originalObjectsToAnalyze ?: dependency.referenceInfo?.possibleReferencedValues
)
}
.collapse(element)
}
@@ -227,13 +273,38 @@ class UStringEvaluator {
}
private fun provePossibleDependency(
evidence: Dependency.PotentialSideEffectDependency.DependencyEvidence,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>
evidence: DependencyEvidence,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>,
visitedEvidences: MutableSet<DependencyEvidence> = mutableSetOf()
): Boolean {
return (evidence.evidenceElement == null || builderEvaluator.isExpressionReturnSelf(evidence.evidenceElement)) &&
(evidence.requires == null || provePossibleDependency(evidence.requires, builderEvaluator))
if (evidence in visitedEvidences) return false // Cyclic evidence
visitedEvidences += evidence
val result = (evidence.evidenceElement == null || builderEvaluator.isExpressionReturnSelf(evidence.evidenceElement)) &&
(evidence.requires.isEmpty() || evidence.requires.all { provePossibleDependency(it, builderEvaluator, visitedEvidences) })
visitedEvidences -= evidence
return result
}
private fun selectDependency(
dependencies: Collection<Dependency>,
builderEvaluator: BuilderLikeExpressionEvaluator<PartiallyKnownString?>,
candidateChecker: (SideEffectChangeCandidate) -> Boolean
): Pair<Dependency?, Collection<SideEffectChangeCandidate>> =
dependencies
.firstOrNull { it is Dependency.PotentialSideEffectDependency }
.takeIf { builderEvaluator.allowSideEffects }
?.let { dependency ->
(dependency as Dependency.PotentialSideEffectDependency).candidates
.selectPotentialCandidates(candidateChecker)
.takeUnless { it.isEmpty() }
?.let { candidates ->
dependency to candidates
}
}
?: (dependencies.firstOrNull { it !is Dependency.PotentialSideEffectDependency && it !is Dependency.ArgumentDependency } to emptyList())
fun interface DeclarationValueProvider {
fun provideValue(element: UDeclaration): PartiallyKnownString?
}
@@ -247,7 +318,7 @@ class UStringEvaluator {
val allowSideEffects: Boolean
val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, T, UStringEvaluator, Configuration) -> T>
val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, T, UStringEvaluator, Configuration, isStrict: Boolean) -> T>
fun isExpressionReturnSelf(expression: UReferenceExpression): Boolean = false
}
@@ -289,10 +360,11 @@ private fun List<PartiallyKnownString>.collapse(element: UElement): PartiallyKno
}
if (segments.size != maxIndex + 1) {
segments.add(
StringEntry.Unknown(element.sourcePsi!!, element.ownTextRange,
map { PartiallyKnownString(it.segments.subList(segments.size, it.segments.size)) })
)
segments.add(StringEntry.Unknown(
element.sourcePsi!!,
element.ownTextRange,
map { PartiallyKnownString(it.segments.subList(segments.size, it.segments.size)) }
))
}
PartiallyKnownString(segments)

View File

@@ -199,6 +199,11 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
for ((dependency, depIndex) in dependencyToID) {
append("object ")
append(dependency.javaClass.simpleName).append("_").append(depIndex)
if (dependency is DependencyOfReference) {
append(" {\n")
indent().append("values = ").append(dependency.referenceInfo?.possibleReferencedValues).append("\n")
append("}")
}
append("\n")
}
@@ -228,8 +233,15 @@ private fun dumpDependencies(dependencies: Map<UElement, Set<Dependency>>): Stri
indent().indent().append("type = ").append(node.javaClass.simpleName).append("\n")
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("strict = ").append(candidate.dependencyEvidence.strict).append("\n")
generateSequence(candidate.dependencyEvidence.requires) { reqs -> reqs.flatMap { req -> req.requires }.takeUnless { it.isEmpty() } }
.flatten()
.map { it.evidenceElement?.asRenderString()?.escape() }
.toSet()
.takeUnless { it.isEmpty() }
?.joinTo(this, prefix = "$INDENT${INDENT}require = ", postfix = "\n", separator = " && ")
}
indent().append("}\n")
if (node is Dependency.PotentialSideEffectDependency.CandidatesTree.Node.CandidateNode) {
@@ -283,8 +295,10 @@ private fun StringBuilder.edge(begin: String, type: String, end: String) {
append(begin).append(" ").append(type).append(" ").append(end).append("\n")
}
private const val INDENT = " "
private fun StringBuilder.indent(): StringBuilder {
return append(" ")
return append(INDENT)
}
private fun String.escape() =

View File

@@ -26,21 +26,27 @@ object UStringBuilderEvaluator : UStringEvaluator.BuilderLikeExpressionEvaluator
return qualifiedChain.firstOrNull() is USimpleNameReferenceExpression && qualifiedChain.drop(1).all { callPattern.accepts(it) }
}
override val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, PartiallyKnownString?, UStringEvaluator, UStringEvaluator.Configuration) -> PartiallyKnownString?>
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 ->
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)
}
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) {
entries.addAll(argument.segments)
if (isStrict) {
entries.addAll(argument.segments)
} else {
entries.add(StringEntry.Unknown(
call.sourcePsi!!,
TextRange(0, call.sourcePsi!!.textLength),
possibleValues = listOf(PartiallyKnownString(argument.segments), PartiallyKnownString(""))
))
}
}
PartiallyKnownString(entries)
},
PsiJavaPatterns.psiMethod().definedInClass("java.lang.StringBuilder").constructor(true) to { call, _, stringEvaluator, config ->
PsiJavaPatterns.psiMethod().definedInClass("java.lang.StringBuilder").constructor(true) to { call, _, stringEvaluator, config, _ ->
call.getArgumentForParameter(0)?.let { argument ->
stringEvaluator.calculateValue(argument, config)
}

View File

@@ -26,10 +26,14 @@ sealed class Dependent : UserDataHolderBase() {
}
}
sealed class Dependency : UserDataHolderBase() {
abstract val elements: Set<UElement>
data class CommonDependency(val element: UElement) : Dependency() {
data class CommonDependency(
val element: UElement,
override val referenceInfo: DependencyOfReference.ReferenceInfo? = null
) : Dependency(), DependencyOfReference {
override val elements = setOf(element)
}
@@ -37,10 +41,13 @@ sealed class Dependency : UserDataHolderBase() {
override val elements = setOf(element)
}
data class BranchingDependency(override val elements: Set<UElement>) : Dependency() {
data class BranchingDependency(
override val elements: Set<UElement>,
override val referenceInfo: DependencyOfReference.ReferenceInfo? = null
) : Dependency(), DependencyOfReference {
fun unwrapIfSingle(): Dependency =
if (elements.size == 1) {
CommonDependency(elements.single())
CommonDependency(elements.single(), referenceInfo)
}
else {
this
@@ -62,17 +69,20 @@ sealed class Dependency : UserDataHolderBase() {
get() = dependencyFromConnectedGraph.elements
}
data class PotentialSideEffectDependency(val candidates: CandidatesTree) : Dependency() {
data class PotentialSideEffectDependency(
val currentReference: USimpleNameReferenceExpression,
val candidates: CandidatesTree,
override val referenceInfo: DependencyOfReference.ReferenceInfo? = null
) : Dependency(), DependencyOfReference {
data class SideEffectChangeCandidate(
val updateElement: UElement,
val dependencyEvidence: DependencyEvidence
val dependencyEvidence: DependencyEvidence,
val dependencyWitnessValues: Collection<UValueMark> = emptyList(),
)
data class DependencyEvidence(
val strict: Boolean,
val evidenceElement: UReferenceExpression? = null,
val dependencyWitnessIdentifier: String? = null,
val requires: DependencyEvidence? = null
val requires: Collection<DependencyEvidence> = emptyList()
)
/**
@@ -91,8 +101,8 @@ sealed class Dependency : UserDataHolderBase() {
/**
* Select first proven candidate on each path to leaf.
*/
fun selectPotentialCandidates(evidenceChecker: (DependencyEvidence) -> Boolean): Collection<SideEffectChangeCandidate> {
return selectFromBranches(root, evidenceChecker)
fun selectPotentialCandidates(candidateChecker: (SideEffectChangeCandidate) -> Boolean): Collection<SideEffectChangeCandidate> {
return selectFromBranches(root, candidateChecker)
}
fun addToBegin(candidate: SideEffectChangeCandidate): CandidatesTree {
@@ -109,8 +119,9 @@ sealed class Dependency : UserDataHolderBase() {
}
companion object {
private fun selectFromBranches(node: Node, evidenceChecker: (DependencyEvidence) -> Boolean): Collection<SideEffectChangeCandidate> {
return if (node is Node.CandidateNode && evidenceChecker(node.candidate.dependencyEvidence)) {
private fun selectFromBranches(node: Node,
evidenceChecker: (SideEffectChangeCandidate) -> Boolean): Collection<SideEffectChangeCandidate> {
return if (node is Node.CandidateNode && evidenceChecker(node.candidate)) {
listOf(node.candidate)
}
else {
@@ -139,4 +150,16 @@ sealed class Dependency : UserDataHolderBase() {
fun and(other: Dependency): Dependency {
return BranchingDependency(elements + other.elements)
}
}
interface DependencyOfReference {
val referenceInfo: ReferenceInfo?
data class ReferenceInfo(val identifier: String, val possibleReferencedValues: Collection<UValueMark>)
}
class UValueMark {
override fun toString(): String {
return "UValueMark@${System.identityHashCode(this)}"
}
}

View File

@@ -8,7 +8,7 @@ import org.jetbrains.uast.analysis.UStringEvaluator
import org.jetbrains.uast.test.java.AbstractJavaUastLightTest
import org.jetbrains.uast.toUElement
open class AbstractStringEvaluatorTest : AbstractJavaUastLightTest() {
abstract class AbstractStringEvaluatorTest : AbstractJavaUastLightTest() {
protected val PartiallyKnownString.debugConcatenation: String
get() = buildString {
for (segment in segments) {

View File

@@ -101,8 +101,7 @@ class UStringEvaluatorWithSideEffectsTest : AbstractStringEvaluatorTest() {
)
}
@Suppress("unused")
fun `ignore test StringBuilder change through possible reference`() {
fun `test StringBuilder change through possible reference`() {
doTest(
"""
class MyFile {
@@ -112,12 +111,14 @@ class UStringEvaluatorWithSideEffectsTest : AbstractStringEvaluatorTest() {
StringBuilder sb2 = param ? sb : sb1; // ignore sb1 because sb1 != sb
s1.append("-");
sb1.append("-");
StringBuilder sb3 = sb2;
sb3.append("c"); // add because of equality (strict = true)
sb.append("\\m/");
sb1.append("d"); // ignore (strict = false, witness incorrect)
sb2.append("e"); // add "c" as optional (strict = false, witness correct)
@@ -128,7 +129,7 @@ class UStringEvaluatorWithSideEffectsTest : AbstractStringEvaluatorTest() {
}
}
""".trimIndent(),
"""'a'{''|'c'}""",
"""'a'{''|'c'}'\m/'{''|'e'}""",
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(UStringBuilderEvaluator)
@@ -391,10 +392,60 @@ class UStringEvaluatorWithSideEffectsTest : AbstractStringEvaluatorTest() {
}
)
fun `test StringBuilder reassignment to new value`() = doTest(
"""
class MyFile {
String falseEvidenceInIf() {
StringBuilder sb = new StringBuilder("a");
sb.toString();
sb = new StringBuilder("b");
sb.append("c");
return sb.toSt<caret>ring();
}
}
""".trimIndent(),
"'b''c'",
retrieveElement = UElement?::getUCallExpression,
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(CloneAwareStringBuilderEvaluator())
)
}
)
fun `test strange one line change`() = doTest(
"""
class MyFile {
String a() {
StringBuilder sb = new StringBuilder("0");
sb.append("aaa")
.append("bbb")
.clone()
.append("ccc")
.clone()
.append("ddd");
sb.append("d");
return sb.toSt<caret>ring();
}
}
""".trimIndent(),
"'0''aaa''bbb''d'",
retrieveElement = UElement?::getUCallExpression,
configuration = {
UStringEvaluator.Configuration(
builderEvaluators = listOf(CloneAwareStringBuilderEvaluator())
)
}
)
private class CloneAwareStringBuilderEvaluator : UStringEvaluator.BuilderLikeExpressionEvaluator<PartiallyKnownString?> by UStringBuilderEvaluator {
override val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, PartiallyKnownString?, UStringEvaluator, UStringEvaluator.Configuration) -> PartiallyKnownString?>
override val methodDescriptions: Map<ElementPattern<PsiMethod>, (UCallExpression, PartiallyKnownString?, UStringEvaluator, UStringEvaluator.Configuration, Boolean) -> PartiallyKnownString?>
get() = UStringBuilderEvaluator.methodDescriptions + mapOf(
PsiJavaPatterns.psiMethod().withName("clone") to { _, partiallyKnownString, _, _ ->
PsiJavaPatterns.psiMethod().withName("clone") to { _, partiallyKnownString, _, _, _ ->
partiallyKnownString
}
)