FIR checker: make property init analyzer path-sensitive

In particular, exception throwing path after finally block is
distinguished via path label.

KT-42350 Fixed
This commit is contained in:
Jinseong Jeon
2020-10-09 00:12:44 -07:00
committed by Dmitriy Novozhilov
parent 6fc3f7e776
commit ed188204b4
8 changed files with 199 additions and 37 deletions
@@ -14,7 +14,7 @@ abstract class AbstractFirPropertyInitializationChecker {
abstract fun analyze( abstract fun analyze(
graph: ControlFlowGraph, graph: ControlFlowGraph,
reporter: DiagnosticReporter, reporter: DiagnosticReporter,
data: Map<CFGNode<*>, PropertyInitializationInfo>, data: Map<CFGNode<*>, PathAwarePropertyInitializationInfo>,
properties: Set<FirPropertySymbol> properties: Set<FirPropertySymbol>
) )
} }
@@ -11,7 +11,7 @@ import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange
import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* import org.jetbrains.kotlin.fir.resolve.dfa.cfg.*
import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol
import java.lang.IllegalStateException
class PropertyInitializationInfo( class PropertyInitializationInfo(
map: PersistentMap<FirPropertySymbol, EventOccurrencesRange> = persistentMapOf() map: PersistentMap<FirPropertySymbol, EventOccurrencesRange> = persistentMapOf()
@@ -52,17 +52,97 @@ class LocalPropertyCollector private constructor() : ControlFlowGraphVisitorVoid
} }
} }
class PathAwarePropertyInitializationInfo(
map: PersistentMap<EdgeLabel, PropertyInitializationInfo> = persistentMapOf()
) : ControlFlowInfo<PathAwarePropertyInitializationInfo, EdgeLabel, PropertyInitializationInfo>(map) {
companion object {
val EMPTY = PathAwarePropertyInitializationInfo(persistentMapOf(NormalPath to PropertyInitializationInfo.EMPTY))
}
override val constructor: (PersistentMap<EdgeLabel, PropertyInitializationInfo>) -> PathAwarePropertyInitializationInfo =
::PathAwarePropertyInitializationInfo
val infoAtNormalPath: PropertyInitializationInfo
get() = map[NormalPath] ?: PropertyInitializationInfo.EMPTY
val hasNormalPath: Boolean
get() = map.containsKey(NormalPath)
fun applyLabel(node: CFGNode<*>, label: EdgeLabel): PathAwarePropertyInitializationInfo {
if (label.isNormal) {
// Special case: when we exit the try expression, null label means a normal path.
// Filter out any info bound to non-null label
// One day, if we allow multiple edges between nodes with different labels, e.g., labeling all paths in try/catch/finally,
// instead of this kind of special handling, proxy enter/exit nodes per label are preferred.
if (node is TryExpressionExitNode) {
return if (hasNormalPath) {
constructor(persistentMapOf(NormalPath to infoAtNormalPath))
} else {
/* This means no info for normal path. */
EMPTY
}
}
// In general, null label means no additional path info, hence return `this` as-is.
return this
}
val hasAbnormalLabels = map.keys.any { !it.isNormal }
return if (hasAbnormalLabels) {
// { |-> ... l1 |-> I1, l2 |-> I2, ... }
// | l1 // path exit: if the given info has non-null labels, this acts like a filtering
// { |-> I1 } // NB: remove the path info
if (map.keys.contains(label)) {
constructor(persistentMapOf(NormalPath to map[label]!!))
} else {
/* This means no info for the specific label. */
EMPTY
}
} else {
// { |-> ... } // empty path info
// | l1 // path entry
// { l1 -> ... } // now, every info bound to the label
constructor(persistentMapOf(label to infoAtNormalPath))
}
}
fun merge(other: PathAwarePropertyInitializationInfo): PathAwarePropertyInitializationInfo {
var resultMap = persistentMapOf<EdgeLabel, PropertyInitializationInfo>()
for (label in keys.union(other.keys)) {
// disjoint merging to preserve paths. i.e., merge the property initialization info if and only if both have the key.
// merge({ |-> I1 }, { |-> I2, l1 |-> I3 }
// == { |-> merge(I1, I2), l1 |-> I3 }
val i1 = this[label]
val i2 = other[label]
resultMap = when {
i1 != null && i2 != null ->
resultMap.put(label, i1.merge(i2))
i1 != null ->
resultMap.put(label, i1)
i2 != null ->
resultMap.put(label, i2)
else ->
throw IllegalStateException()
}
}
return constructor(resultMap)
}
}
class PropertyInitializationInfoCollector(private val localProperties: Set<FirPropertySymbol>) : class PropertyInitializationInfoCollector(private val localProperties: Set<FirPropertySymbol>) :
ControlFlowGraphVisitor<PropertyInitializationInfo, Collection<PropertyInitializationInfo>>() { ControlFlowGraphVisitor<PathAwarePropertyInitializationInfo, Collection<Pair<EdgeLabel, PathAwarePropertyInitializationInfo>>>() {
override fun visitNode(node: CFGNode<*>, data: Collection<PropertyInitializationInfo>): PropertyInitializationInfo { override fun visitNode(
if (data.isEmpty()) return PropertyInitializationInfo.EMPTY node: CFGNode<*>,
return data.reduce(PropertyInitializationInfo::merge) data: Collection<Pair<EdgeLabel, PathAwarePropertyInitializationInfo>>
): PathAwarePropertyInitializationInfo {
if (data.isEmpty()) return PathAwarePropertyInitializationInfo.EMPTY
return data.map { (label, info) -> info.applyLabel(node, label) }
.reduce(PathAwarePropertyInitializationInfo::merge)
} }
override fun visitVariableAssignmentNode( override fun visitVariableAssignmentNode(
node: VariableAssignmentNode, node: VariableAssignmentNode,
data: Collection<PropertyInitializationInfo> data: Collection<Pair<EdgeLabel, PathAwarePropertyInitializationInfo>>
): PropertyInitializationInfo { ): PathAwarePropertyInitializationInfo {
val dataForNode = visitNode(node, data) val dataForNode = visitNode(node, data)
val reference = node.fir.lValue as? FirResolvedNamedReference ?: return dataForNode val reference = node.fir.lValue as? FirResolvedNamedReference ?: return dataForNode
val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return dataForNode val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return dataForNode
@@ -75,8 +155,8 @@ class PropertyInitializationInfoCollector(private val localProperties: Set<FirPr
override fun visitVariableDeclarationNode( override fun visitVariableDeclarationNode(
node: VariableDeclarationNode, node: VariableDeclarationNode,
data: Collection<PropertyInitializationInfo> data: Collection<Pair<EdgeLabel, PathAwarePropertyInitializationInfo>>
): PropertyInitializationInfo { ): PathAwarePropertyInitializationInfo {
val dataForNode = visitNode(node, data) val dataForNode = visitNode(node, data)
return if (node.fir.initializer == null && node.fir.delegate == null) { return if (node.fir.initializer == null && node.fir.delegate == null) {
dataForNode dataForNode
@@ -86,18 +166,27 @@ class PropertyInitializationInfoCollector(private val localProperties: Set<FirPr
} }
fun getData(graph: ControlFlowGraph) = fun getData(graph: ControlFlowGraph) =
graph.collectDataForNode( graph.collectPathAwareDataForNode(
TraverseDirection.Forward, TraverseDirection.Forward,
PropertyInitializationInfo.EMPTY, PathAwarePropertyInitializationInfo.EMPTY,
this this
) )
private fun processVariableWithAssignment( private fun processVariableWithAssignment(
dataForNode: PropertyInitializationInfo, dataForNode: PathAwarePropertyInitializationInfo,
symbol: FirPropertySymbol symbol: FirPropertySymbol
): PropertyInitializationInfo { ): PathAwarePropertyInitializationInfo {
val existingKind = dataForNode[symbol] ?: EventOccurrencesRange.ZERO assert(dataForNode.keys.isNotEmpty())
val kind = existingKind + EventOccurrencesRange.EXACTLY_ONCE var resultMap = persistentMapOf<EdgeLabel, PropertyInitializationInfo>()
return dataForNode.put(symbol, kind) // before: { |-> { p1 |-> PI1 }, l1 |-> { p2 |-> PI2 }
for (label in dataForNode.keys) {
val dataPerLabel = dataForNode[label]!!
val existingKind = dataPerLabel[symbol] ?: EventOccurrencesRange.ZERO
val kind = existingKind + EventOccurrencesRange.EXACTLY_ONCE
resultMap = resultMap.put(label, dataPerLabel.put(symbol, kind))
}
// after (if symbol is p1):
// { |-> { p1 |-> PI1 + 1 }, l1 |-> { p1 |-> [1, 1], p2 |-> PI2 }
return PathAwarePropertyInitializationInfo(resultMap)
} }
} }
@@ -7,6 +7,8 @@ package org.jetbrains.kotlin.fir.analysis.cfa
import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* import org.jetbrains.kotlin.fir.resolve.dfa.cfg.*
// ------------------------------ Graph Traversal ------------------------------
enum class TraverseDirection { enum class TraverseDirection {
Forward, Backward Forward, Backward
} }
@@ -29,6 +31,10 @@ fun ControlFlowGraph.traverse(
traverse(direction, visitor, null) traverse(direction, visitor, null)
} }
// --------------------- Path-insensitive data collection ----------------------
// TODO: Deprecate and make all existing checkers path-sensitive
fun <I : ControlFlowInfo<I, K, V>, K : Any, V : Any> ControlFlowGraph.collectDataForNode( fun <I : ControlFlowInfo<I, K, V>, K : Any, V : Any> ControlFlowGraph.collectDataForNode(
direction: TraverseDirection, direction: TraverseDirection,
initialInfo: I, initialInfo: I,
@@ -75,3 +81,62 @@ private fun <I : ControlFlowInfo<I, K, V>, K : Any, V : Any> ControlFlowGraph.co
} }
} }
} }
// ---------------------- Path-sensitive data collection -----------------------
// TODO: Once migration is done, get rid of "PathAware" from util names
fun <I : ControlFlowInfo<I, K, V>, K : Any, V : Any> ControlFlowGraph.collectPathAwareDataForNode(
direction: TraverseDirection,
initialInfo: I,
visitor: ControlFlowGraphVisitor<I, Collection<Pair<EdgeLabel, I>>>
): Map<CFGNode<*>, I> {
val nodeMap = LinkedHashMap<CFGNode<*>, I>()
val startNode = getEnterNode(direction)
nodeMap[startNode] = initialInfo
val changed = mutableMapOf<CFGNode<*>, Boolean>()
do {
collectPathAwareDataForNodeInternal(direction, initialInfo, visitor, nodeMap, changed)
} while (changed.any { it.value })
return nodeMap
}
private fun <I : ControlFlowInfo<I, K, V>, K : Any, V : Any> ControlFlowGraph.collectPathAwareDataForNodeInternal(
direction: TraverseDirection,
initialInfo: I,
visitor: ControlFlowGraphVisitor<I, Collection<Pair<EdgeLabel, I>>>,
nodeMap: MutableMap<CFGNode<*>, I>,
changed: MutableMap<CFGNode<*>, Boolean>
) {
val nodes = getNodesInOrder(direction)
for (node in nodes) {
if (direction == TraverseDirection.Backward && node is CFGNodeWithCfgOwner<*>) {
node.subGraphs.forEach { it.collectPathAwareDataForNodeInternal(direction, initialInfo, visitor, nodeMap, changed) }
}
val previousNodes = when (direction) {
TraverseDirection.Forward -> node.previousCfgNodes
TraverseDirection.Backward -> node.followingCfgNodes
}
// One noticeable different against the path-unaware version is, here, we pair the control-flow info with the label.
val previousData =
previousNodes.mapNotNull {
val k = when (direction) {
TraverseDirection.Forward -> node.incomingEdges[it]?.label ?: NormalPath
TraverseDirection.Backward -> node.outgoingEdges[it]?.label ?: NormalPath
}
val v = nodeMap[it] ?: return@mapNotNull null
k to v
}
val data = nodeMap[node]
val newData = node.accept(visitor, previousData)
val hasChanged = newData != data
changed[node] = hasChanged
if (hasChanged) {
nodeMap[node] = newData
}
if (direction == TraverseDirection.Forward && node is CFGNodeWithCfgOwner<*>) {
node.subGraphs.forEach { it.collectPathAwareDataForNodeInternal(direction, initialInfo, visitor, nodeMap, changed) }
}
}
}
@@ -22,7 +22,7 @@ object FirPropertyInitializationAnalyzer : AbstractFirPropertyInitializationChec
override fun analyze( override fun analyze(
graph: ControlFlowGraph, graph: ControlFlowGraph,
reporter: DiagnosticReporter, reporter: DiagnosticReporter,
data: Map<CFGNode<*>, PropertyInitializationInfo>, data: Map<CFGNode<*>, PathAwarePropertyInitializationInfo>,
properties: Set<FirPropertySymbol> properties: Set<FirPropertySymbol>
) { ) {
val localData = data.filter { val localData = data.filter {
@@ -37,7 +37,7 @@ object FirPropertyInitializationAnalyzer : AbstractFirPropertyInitializationChec
} }
private class UninitializedPropertyReporter( private class UninitializedPropertyReporter(
val data: Map<CFGNode<*>, PropertyInitializationInfo>, val data: Map<CFGNode<*>, PathAwarePropertyInitializationInfo>,
val localProperties: Set<FirPropertySymbol>, val localProperties: Set<FirPropertySymbol>,
val reporter: DiagnosticReporter val reporter: DiagnosticReporter
) : ControlFlowGraphVisitorVoid() { ) : ControlFlowGraphVisitorVoid() {
@@ -48,12 +48,24 @@ object FirPropertyInitializationAnalyzer : AbstractFirPropertyInitializationChec
val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return
if (symbol !in localProperties) return if (symbol !in localProperties) return
if (symbol.fir.isLateInit) return if (symbol.fir.isLateInit) return
val kind = data.getValue(node)[symbol] ?: EventOccurrencesRange.ZERO val pathAwareInfo = data.getValue(node)
if (!kind.isDefinitelyVisited()) { for (label in pathAwareInfo.keys) {
node.fir.source?.let { if (investigate(pathAwareInfo[label]!!, symbol, node)) {
reporter.report(FirErrors.UNINITIALIZED_VARIABLE.on(it, symbol)) // To avoid duplicate reports, stop investigating remaining paths if the property is not initialized at any path.
break
} }
} }
} }
private fun investigate(info: PropertyInitializationInfo, symbol: FirPropertySymbol, node: QualifiedAccessNode): Boolean {
val kind = info[symbol] ?: EventOccurrencesRange.ZERO
if (!kind.isDefinitelyVisited()) {
node.fir.source?.let {
reporter.report(FirErrors.UNINITIALIZED_VARIABLE.on(it, symbol))
return true
}
}
return false
}
} }
} }
@@ -5,16 +5,12 @@
package org.jetbrains.kotlin.fir.analysis.checkers.extended package org.jetbrains.kotlin.fir.analysis.checkers.extended
import com.intellij.lang.LighterASTNode import com.intellij.lang.LighterASTNode
import com.intellij.openapi.util.Ref import com.intellij.openapi.util.Ref
import org.jetbrains.kotlin.KtNodeTypes import org.jetbrains.kotlin.KtNodeTypes
import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange
import org.jetbrains.kotlin.fir.* import org.jetbrains.kotlin.fir.*
import org.jetbrains.kotlin.fir.analysis.cfa.AbstractFirPropertyInitializationChecker import org.jetbrains.kotlin.fir.analysis.cfa.*
import org.jetbrains.kotlin.fir.analysis.cfa.PropertyInitializationInfo
import org.jetbrains.kotlin.fir.analysis.cfa.TraverseDirection
import org.jetbrains.kotlin.fir.analysis.cfa.traverse
import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors
import org.jetbrains.kotlin.fir.analysis.getChild import org.jetbrains.kotlin.fir.analysis.getChild
@@ -23,12 +19,11 @@ import org.jetbrains.kotlin.fir.resolve.dfa.cfg.*
import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol
import org.jetbrains.kotlin.lexer.KtTokens import org.jetbrains.kotlin.lexer.KtTokens
object CanBeValChecker : AbstractFirPropertyInitializationChecker() { object CanBeValChecker : AbstractFirPropertyInitializationChecker() {
override fun analyze( override fun analyze(
graph: ControlFlowGraph, graph: ControlFlowGraph,
reporter: DiagnosticReporter, reporter: DiagnosticReporter,
data: Map<CFGNode<*>, PropertyInitializationInfo>, data: Map<CFGNode<*>, PathAwarePropertyInitializationInfo>,
properties: Set<FirPropertySymbol> properties: Set<FirPropertySymbol>
) { ) {
val unprocessedProperties = mutableSetOf<FirPropertySymbol>() val unprocessedProperties = mutableSetOf<FirPropertySymbol>()
@@ -75,7 +70,7 @@ object CanBeValChecker : AbstractFirPropertyInitializationChecker() {
value in canBeValOccurrenceRanges && symbol.fir.isVar value in canBeValOccurrenceRanges && symbol.fir.isVar
private class UninitializedPropertyReporter( private class UninitializedPropertyReporter(
val data: Map<CFGNode<*>, PropertyInitializationInfo>, val data: Map<CFGNode<*>, PathAwarePropertyInitializationInfo>,
val localProperties: Set<FirPropertySymbol>, val localProperties: Set<FirPropertySymbol>,
val unprocessedProperties: MutableSet<FirPropertySymbol>, val unprocessedProperties: MutableSet<FirPropertySymbol>,
val propertiesCharacteristics: MutableMap<FirPropertySymbol, EventOccurrencesRange> val propertiesCharacteristics: MutableMap<FirPropertySymbol, EventOccurrencesRange>
@@ -89,7 +84,8 @@ object CanBeValChecker : AbstractFirPropertyInitializationChecker() {
unprocessedProperties.remove(symbol) unprocessedProperties.remove(symbol)
val currentCharacteristic = propertiesCharacteristics.getOrDefault(symbol, EventOccurrencesRange.ZERO) val currentCharacteristic = propertiesCharacteristics.getOrDefault(symbol, EventOccurrencesRange.ZERO)
propertiesCharacteristics[symbol] = currentCharacteristic.or(data.getValue(node)[symbol] ?: EventOccurrencesRange.ZERO) val info = data.getValue(node)
propertiesCharacteristics[symbol] = currentCharacteristic.or(info.infoAtNormalPath[symbol] ?: EventOccurrencesRange.ZERO)
} }
override fun visitVariableDeclarationNode(node: VariableDeclarationNode) { override fun visitVariableDeclarationNode(node: VariableDeclarationNode) {
@@ -27,7 +27,7 @@ fun assignedInTryAndCatch() {
a = 41 a = 41
} finally { } finally {
} }
<!UNINITIALIZED_VARIABLE!>a<!>.hashCode() a.hashCode()
} }
fun sideEffectBeforeAssignedInTryAndCatch(s: Any) { fun sideEffectBeforeAssignedInTryAndCatch(s: Any) {
@@ -40,7 +40,7 @@ fun sideEffectBeforeAssignedInTryAndCatch(s: Any) {
a = 41 a = 41
} finally { } finally {
} }
<!UNINITIALIZED_VARIABLE!>a<!>.hashCode() a.hashCode()
} }
fun assignedAtAll() { fun assignedAtAll() {
@@ -4,7 +4,7 @@ fun assignedInTry() {
a = 42 a = 42
} finally { } finally {
} }
<!UNINITIALIZED_VARIABLE!>a<!>.hashCode() a.hashCode()
} }
fun sideEffectBeforeAssignmentInTry(s: Any) { fun sideEffectBeforeAssignmentInTry(s: Any) {
@@ -14,7 +14,7 @@ fun sideEffectBeforeAssignmentInTry(s: Any) {
a = 42 a = 42
} finally { } finally {
} }
<!UNINITIALIZED_VARIABLE!>a<!>.hashCode() a.hashCode()
} }
fun assignedInTryAndFinally() { fun assignedInTryAndFinally() {
@@ -95,7 +95,7 @@ fun case_6() {
println(<!UNINITIALIZED_VARIABLE!>value_2<!>.inc()) println(<!UNINITIALIZED_VARIABLE!>value_2<!>.inc())
} }
<!UNINITIALIZED_VARIABLE!>value_2<!>++ value_2++
} }
// TESTCASE NUMBER: 7 // TESTCASE NUMBER: 7