FIR DFA: add smartcast invalidation for init block assignments
This commit is contained in:
+5
-8
@@ -202,18 +202,15 @@ abstract class FirDataFlowAnalyzer(
|
||||
// ----------------------------------- Classes -----------------------------------
|
||||
|
||||
fun enterClass(klass: FirClass, buildGraph: Boolean) {
|
||||
val (outerNode, enterNode) = graphBuilder.enterClass(klass, buildGraph) ?: return
|
||||
val (outerNode, enterNode) = graphBuilder.enterClass(klass, buildGraph)
|
||||
outerNode?.mergeIncomingFlow()
|
||||
enterNode.mergeIncomingFlow()
|
||||
enterNode?.mergeIncomingFlow()
|
||||
context.variableAssignmentAnalyzer.enterClass(klass)
|
||||
}
|
||||
|
||||
fun exitClass(): ControlFlowGraph? {
|
||||
// TODO: support capturing of mutable properties
|
||||
// var x: String?
|
||||
// x = ""
|
||||
// class C { init { x = maybeNull() } }
|
||||
// // C will be initialized at first use - x is no longer safe to smart cast
|
||||
val (node, graph) = graphBuilder.exitClass() ?: return null
|
||||
context.variableAssignmentAnalyzer.exitClass()
|
||||
val (node, graph) = graphBuilder.exitClass()
|
||||
if (node != null) {
|
||||
node.mergeIncomingFlow()
|
||||
} else {
|
||||
|
||||
+86
-63
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.fir.expressions.*
|
||||
import org.jetbrains.kotlin.fir.references.FirNamedReference
|
||||
import org.jetbrains.kotlin.fir.references.FirReference
|
||||
import org.jetbrains.kotlin.fir.references.toResolvedPropertySymbol
|
||||
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
|
||||
import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol
|
||||
import org.jetbrains.kotlin.fir.visitors.FirVisitor
|
||||
import org.jetbrains.kotlin.name.Name
|
||||
@@ -24,10 +25,10 @@ import org.jetbrains.kotlin.name.Name
|
||||
* queries after the traversal is done.
|
||||
**/
|
||||
internal class FirLocalVariableAssignmentAnalyzer {
|
||||
private var rootFunction: FirFunction? = null
|
||||
private var assignedLocalVariablesByFunction: Map<FirFunctionSymbol<*>, FunctionFork>? = null
|
||||
private var rootFunction: FirFunctionSymbol<*>? = null
|
||||
private var assignedLocalVariablesByDeclaration: Map<FirBasedSymbol<*>, Fork>? = null
|
||||
|
||||
private val functionScopes: Stack<Pair<FunctionFork?, MutableSet<FirProperty>>> = stackOf()
|
||||
private val scopes: Stack<Pair<Fork?, MutableSet<FirProperty>>> = stackOf()
|
||||
|
||||
// Example of control-flow-postponed lambdas: callBoth({ a.x }, { a = null })
|
||||
// Lambdas are called in an unknown order, so control flow edges to both of them go from before the call.
|
||||
@@ -40,24 +41,24 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
// from the result of `run` to the result of `genericFunction` so smart casts should be prohibited in `a.x`.
|
||||
//
|
||||
// This mirrors `ControlFlowGraphBuilder.postponedLambdaExits`.
|
||||
private val postponedLambdas: Stack<MutableMap<FunctionFork, Boolean /* data-flow only */>> = stackOf()
|
||||
private val postponedLambdas: Stack<MutableMap<Fork, Boolean /* data-flow only */>> = stackOf()
|
||||
|
||||
fun reset() {
|
||||
rootFunction = null
|
||||
assignedLocalVariablesByFunction = null
|
||||
assignedLocalVariablesByDeclaration = null
|
||||
postponedLambdas.reset()
|
||||
functionScopes.reset()
|
||||
scopes.reset()
|
||||
}
|
||||
|
||||
/** Checks whether the given access is an unstable access to a local variable at this moment. */
|
||||
@OptIn(DfaInternals::class)
|
||||
fun isAccessToUnstableLocalVariable(fir: FirExpression): Boolean {
|
||||
if (assignedLocalVariablesByFunction == null) return false
|
||||
if (assignedLocalVariablesByDeclaration == null) return false
|
||||
|
||||
val realFir = fir.unwrapElement() as? FirQualifiedAccessExpression ?: return false
|
||||
val property = realFir.calleeReference.toResolvedPropertySymbol()?.fir ?: return false
|
||||
// Have data => have a root function => functionScopes is not empty.
|
||||
return property in functionScopes.top().second || postponedLambdas.all().any { lambdas ->
|
||||
// Have data => have a root function => `scopes` is not empty.
|
||||
return property in scopes.top().second || postponedLambdas.all().any { lambdas ->
|
||||
// Control-flow-postponed lambdas' assignments should be in `functionScopes.top()`.
|
||||
// The reason we can't check them here is that one of the entries may be the lambda
|
||||
// that is currently being analyzed, and assignments in it are, in fact, totally fine.
|
||||
@@ -65,22 +66,54 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInfoForLocalFunction(symbol: FirFunctionSymbol<*>): FunctionFork? {
|
||||
private fun getInfoForDeclaration(symbol: FirBasedSymbol<*>): Fork? {
|
||||
val root = rootFunction ?: return null
|
||||
if (root.symbol == symbol) return null
|
||||
val cachedMap = assignedLocalVariablesByFunction ?: run {
|
||||
if (root == symbol) return null
|
||||
val cachedMap = assignedLocalVariablesByDeclaration ?: run {
|
||||
val data = MiniCfgBuilder.MiniCfgData()
|
||||
MiniCfgBuilder().visitElement(root, data)
|
||||
data.functionForks.also { assignedLocalVariablesByFunction = it }
|
||||
MiniCfgBuilder().visitElement(root.fir, data)
|
||||
data.forks.also { assignedLocalVariablesByDeclaration = it }
|
||||
}
|
||||
return cachedMap[symbol]
|
||||
}
|
||||
|
||||
fun enterFunction(function: FirFunction) {
|
||||
val prohibitSmartCasts = functionScopes.topOrNull()?.second?.toMutableSet()
|
||||
?: mutableSetOf<FirProperty>().also { rootFunction = function }
|
||||
private fun enterScope(symbol: FirBasedSymbol<*>, evaluatedInPlace: Boolean): Pair<Fork?, MutableSet<FirProperty>> {
|
||||
val currentInfo = getInfoForDeclaration(symbol)
|
||||
val prohibitInThisScope = scopes.top().second.toMutableSet()
|
||||
scopes.push(currentInfo to prohibitInThisScope)
|
||||
if (!evaluatedInPlace) {
|
||||
for ((outerInfo, prohibitInOuterScope) in scopes.all()) {
|
||||
// The callable may be stored and then called later
|
||||
// => any access of the variables it touches is no longer smartcastable ever,
|
||||
// including within the callable itself (can recurse).
|
||||
//
|
||||
// TODO: this incorrectly affects separate branches that are visited after this one:
|
||||
// if (p is Something) {
|
||||
// if (condition)) {
|
||||
// foo { p = whatever }
|
||||
// p.memberOfSomething // Bad
|
||||
// } else {
|
||||
// p.memberOfSomething // Marked as an error, but actually OK
|
||||
// }
|
||||
// p.memberOfSomething // Bad
|
||||
// }
|
||||
// FE1.0 has the same behavior.
|
||||
currentInfo?.assignedInside?.let(prohibitInOuterScope::addAll)
|
||||
// => any write to a variable outside the callable invalidates smart casts inside it
|
||||
outerInfo?.assignedLater?.let(prohibitInThisScope::addAll)
|
||||
}
|
||||
}
|
||||
return scopes.top()
|
||||
}
|
||||
|
||||
val info = getInfoForLocalFunction(function.symbol)
|
||||
fun enterFunction(function: FirFunction) {
|
||||
if (rootFunction == null) {
|
||||
rootFunction = function.symbol
|
||||
scopes.push(null to mutableSetOf())
|
||||
return
|
||||
}
|
||||
val (info, prohibitSmartCasts) =
|
||||
enterScope(function.symbol, function is FirAnonymousFunction && function.invocationKind.isInPlace)
|
||||
for (concurrentLambdas in postponedLambdas.all()) {
|
||||
for ((otherLambda, dataFlowOnly) in concurrentLambdas) {
|
||||
if (!dataFlowOnly && otherLambda != info) {
|
||||
@@ -88,51 +121,35 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (function !is FirAnonymousFunction || !function.invocationKind.isInPlace) {
|
||||
// The function may be stored and then called later =>
|
||||
for ((outerInfo, prohibitInOuterScope) in functionScopes.all()) {
|
||||
if (info != null) {
|
||||
// => any access of the variables it touches is no longer smartcastable ever.
|
||||
//
|
||||
// TODO: this incorrectly affects separate branches that are visited after this one:
|
||||
// if (p is Something) {
|
||||
// if (condition)) {
|
||||
// foo { p = whatever }
|
||||
// p.memberOfSomething // Bad
|
||||
// } else {
|
||||
// p.memberOfSomething // Marked as an error, but actually OK
|
||||
// }
|
||||
// p.memberOfSomething // Bad
|
||||
// }
|
||||
// FE1.0 has the same behavior.
|
||||
prohibitInOuterScope.addAll(info.assignedInside)
|
||||
}
|
||||
if (outerInfo != null) {
|
||||
// => any write to a variable outside the function invalidates smart casts inside it
|
||||
prohibitSmartCasts += outerInfo.assignedLater
|
||||
}
|
||||
}
|
||||
if (info != null) {
|
||||
prohibitSmartCasts.addAll(info.assignedLater)
|
||||
// => it may be called twice in parallel, so it can't even trust its own assignments
|
||||
prohibitSmartCasts.addAll(info.assignedInside)
|
||||
}
|
||||
}
|
||||
functionScopes.push(info to prohibitSmartCasts)
|
||||
}
|
||||
|
||||
fun exitFunction() {
|
||||
functionScopes.pop()
|
||||
if (functionScopes.isEmpty) {
|
||||
scopes.pop()
|
||||
if (scopes.isEmpty) {
|
||||
rootFunction = null
|
||||
assignedLocalVariablesByFunction = null
|
||||
assignedLocalVariablesByDeclaration = null
|
||||
}
|
||||
}
|
||||
|
||||
fun enterClass(klass: FirClass) {
|
||||
if (rootFunction == null) return
|
||||
val (info, prohibitSmartCasts) = enterScope(klass.symbol, klass is FirAnonymousObject)
|
||||
if (klass is FirAnonymousObject && info != null) {
|
||||
// Assignments in initializers and methods invalidate smart casts in other members.
|
||||
// TODO: initializers shouldn't invalidate smart casts in themselves.
|
||||
prohibitSmartCasts.addAll(info.assignedInside)
|
||||
}
|
||||
}
|
||||
|
||||
fun exitClass() {
|
||||
if (rootFunction == null) return
|
||||
scopes.pop()
|
||||
}
|
||||
|
||||
fun enterFunctionCall(lambdaArgs: Collection<FirAnonymousFunction>) {
|
||||
// If not inside a function at all, then there is no concept of a local and nothing to track.
|
||||
if (rootFunction == null) return
|
||||
postponedLambdas.push(lambdaArgs.mapNotNull { getInfoForLocalFunction(it.symbol) }.associateWithTo(mutableMapOf()) { false })
|
||||
postponedLambdas.push(lambdaArgs.mapNotNull { getInfoForDeclaration(it.symbol) }.associateWithTo(mutableMapOf()) { false })
|
||||
}
|
||||
|
||||
fun exitFunctionCall(callCompleted: Boolean) {
|
||||
@@ -235,7 +252,7 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
* so that shadowed names are handled correctly. This works because local variables at any scope have higher priority
|
||||
* than members on implicit receivers, even if the implicit receiver is introduced by a later scope.
|
||||
*/
|
||||
class FunctionFork(
|
||||
class Fork(
|
||||
val assignedLater: Set<FirProperty>,
|
||||
val assignedInside: Set<FirProperty>,
|
||||
)
|
||||
@@ -255,7 +272,7 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
element.acceptChildren(this, data)
|
||||
}
|
||||
|
||||
private fun visitRepeatableElement(element: FirElement, data: MiniCfgData): Set<FirProperty> {
|
||||
private fun visitElementWithLexicalScope(element: FirElement, data: MiniCfgData): Set<FirProperty> {
|
||||
// Detach the flow so that variables declared inside the structure do not leak into the outside.
|
||||
val flow = MiniFlow.start()
|
||||
val freeVariables = data.variableDeclarations.flatMapTo(mutableSetOf()) { it.values }
|
||||
@@ -265,19 +282,25 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
}
|
||||
|
||||
override fun visitAnonymousFunction(anonymousFunction: FirAnonymousFunction, data: MiniCfgData) =
|
||||
visitFunction(anonymousFunction, data)
|
||||
visitLocalDeclaration(anonymousFunction, data)
|
||||
|
||||
override fun visitSimpleFunction(simpleFunction: FirSimpleFunction, data: MiniCfgData) =
|
||||
visitFunction(simpleFunction, data)
|
||||
visitLocalDeclaration(simpleFunction, data)
|
||||
|
||||
override fun visitFunction(function: FirFunction, data: MiniCfgData) {
|
||||
override fun visitRegularClass(regularClass: FirRegularClass, data: MiniCfgData) =
|
||||
visitLocalDeclaration(regularClass, data)
|
||||
|
||||
override fun visitAnonymousObject(anonymousObject: FirAnonymousObject, data: MiniCfgData) =
|
||||
visitLocalDeclaration(anonymousObject, data)
|
||||
|
||||
private fun visitLocalDeclaration(declaration: FirDeclaration, data: MiniCfgData) {
|
||||
val flow = data.flow
|
||||
val assignedInside = visitRepeatableElement(function, data)
|
||||
val assignedInside = visitElementWithLexicalScope(declaration, data)
|
||||
// Now that the inner variables have been discarded, the rest can be propagated to prevent smartcasts
|
||||
// in lambdas declared before this one.
|
||||
// in declarations that came before this one.
|
||||
flow.recordAssignments(assignedInside)
|
||||
data.flow = flow.fork()
|
||||
data.functionForks[function.symbol] = FunctionFork(data.flow.assignedLater, assignedInside)
|
||||
data.forks[declaration.symbol] = Fork(data.flow.assignedLater, assignedInside)
|
||||
}
|
||||
|
||||
override fun visitWhenExpression(whenExpression: FirWhenExpression, data: MiniCfgData) {
|
||||
@@ -308,7 +331,7 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
|
||||
override fun visitLoop(loop: FirLoop, data: MiniCfgData) {
|
||||
val entry = data.flow
|
||||
val assignedInside = visitRepeatableElement(loop, data)
|
||||
val assignedInside = visitElementWithLexicalScope(loop, data)
|
||||
// All forks in the loop should have the same set of variables assigned later, equal to the set
|
||||
// at the start of the loop.
|
||||
data.flow.recordAssignments(assignedInside)
|
||||
@@ -383,7 +406,7 @@ internal class FirLocalVariableAssignmentAnalyzer {
|
||||
class MiniCfgData {
|
||||
var flow: MiniFlow = MiniFlow.start()
|
||||
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf()))
|
||||
val functionForks: MutableMap<FirFunctionSymbol<*>, FunctionFork> = mutableMapOf()
|
||||
val forks: MutableMap<FirBasedSymbol<*>, Fork> = mutableMapOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -371,10 +371,10 @@ class ControlFlowGraphBuilder {
|
||||
addEdge(source, enterNode, preferredKind = kind)
|
||||
}
|
||||
|
||||
fun enterClass(klass: FirClass, buildGraph: Boolean): Pair<CFGNode<*>?, ClassEnterNode>? {
|
||||
fun enterClass(klass: FirClass, buildGraph: Boolean): Pair<CFGNode<*>?, ClassEnterNode?> {
|
||||
if (!buildGraph || klass !is FirControlFlowGraphOwner) {
|
||||
graphs.push(ControlFlowGraph(null, "<discarded class graph>", ControlFlowGraph.Kind.Class))
|
||||
return null
|
||||
return null to null
|
||||
}
|
||||
|
||||
val localClassEnterNode = when {
|
||||
@@ -418,11 +418,11 @@ class ControlFlowGraphBuilder {
|
||||
return localClassEnterNode to enterNode
|
||||
}
|
||||
|
||||
fun exitClass(): Pair<ClassExitNode?, ControlFlowGraph>? {
|
||||
fun exitClass(): Pair<ClassExitNode?, ControlFlowGraph?> {
|
||||
assert(currentGraph.kind == ControlFlowGraph.Kind.Class)
|
||||
if (currentGraph.declaration == null) {
|
||||
graphs.pop()
|
||||
return null
|
||||
return null to null
|
||||
}
|
||||
|
||||
// Members of a class can be visited in any order, so data flow between them is unordered,
|
||||
@@ -435,7 +435,7 @@ class ControlFlowGraphBuilder {
|
||||
// this graph to be built twice (or more). Not sure what this means. Nothing good, probably.
|
||||
// In any case, attempting to add more edges to subgraphs will be fatal.
|
||||
graphs.pop()
|
||||
return null
|
||||
return null to null
|
||||
}
|
||||
|
||||
val calledInPlace = mutableListOf<ControlFlowGraph>()
|
||||
|
||||
+15
-15
@@ -8,11 +8,11 @@ fun objectInit() {
|
||||
x.length // ok
|
||||
y.length // ok
|
||||
val o = object {
|
||||
init { x.length } // ?
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // ?
|
||||
init { x = null }
|
||||
init { x.length } // bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
|
||||
init { y.length } // ok
|
||||
fun foo() = x.length // bad
|
||||
fun foo() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
}
|
||||
y = null
|
||||
x<!UNSAFE_CALL!>.<!>length // bad
|
||||
@@ -24,7 +24,7 @@ fun objectMethod() {
|
||||
x = ""
|
||||
x.length // ok
|
||||
val o = object {
|
||||
init { x.length } // sort of bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // sort of bad
|
||||
fun foo() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
fun bar() { x = null }
|
||||
fun baz() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
@@ -45,24 +45,24 @@ fun classInit() {
|
||||
y.length // ok
|
||||
val ctor = run {
|
||||
class C {
|
||||
init { x.length } // ?
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // ?
|
||||
init { x = null }
|
||||
init { x.length } // bad
|
||||
init { y.length } // bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>y<!>.length } // bad
|
||||
fun foo() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
}
|
||||
x.length // bad
|
||||
if (<!SENSELESS_COMPARISON!>x != null<!>) {
|
||||
<!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
if (x != null) {
|
||||
y = null
|
||||
C() // read y & assign x here
|
||||
x.length // bad
|
||||
<!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
}
|
||||
::C
|
||||
}
|
||||
x.length // bad
|
||||
if (<!SENSELESS_COMPARISON!>x != null<!>) {
|
||||
<!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
if (x != null) {
|
||||
ctor() // read y & assign x here
|
||||
x.length // bad
|
||||
<!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,8 @@ fun classMethod() {
|
||||
y.length // ok
|
||||
val ctor = run {
|
||||
class C {
|
||||
init { x.length } // sort of bad
|
||||
init { y.length } // bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>x<!>.length } // sort of bad
|
||||
init { <!SMARTCAST_IMPOSSIBLE!>y<!>.length } // bad
|
||||
fun foo() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
fun bar() { x = null }
|
||||
fun baz() = <!SMARTCAST_IMPOSSIBLE!>x<!>.length // bad
|
||||
|
||||
Reference in New Issue
Block a user