FIR: fix some errors in local variable assignment analyzer
* wrong method was called from FirDataFlowAnalyzer.exitFunctionCall; * map from function to affected properties should be keyed by symbol, not FirFunction, as the latter may change; * arguments of `return` and assignment statements should be visited, as they may contain lambdas.
This commit is contained in:
+1
-1
@@ -1056,7 +1056,7 @@ abstract class FirDataFlowAnalyzer<FLOW : Flow>(
|
|||||||
fun exitFunctionCall(functionCall: FirFunctionCall, callCompleted: Boolean) {
|
fun exitFunctionCall(functionCall: FirFunctionCall, callCompleted: Boolean) {
|
||||||
val lambdaArgs = functionCall.arguments.mapNotNull { (it as? FirAnonymousFunctionExpression)?.anonymousFunction }
|
val lambdaArgs = functionCall.arguments.mapNotNull { (it as? FirAnonymousFunctionExpression)?.anonymousFunction }
|
||||||
if (lambdaArgs.size > 1) {
|
if (lambdaArgs.size > 1) {
|
||||||
getOrCreateLocalVariableAssignmentAnalyzer(lambdaArgs.first())?.enterFunctionCallWithMultipleLambdaArgs(lambdaArgs)
|
getOrCreateLocalVariableAssignmentAnalyzer(lambdaArgs.first())?.exitFunctionCallWithMultipleLambdaArgs()
|
||||||
}
|
}
|
||||||
if (ignoreFunctionCalls) {
|
if (ignoreFunctionCalls) {
|
||||||
graphBuilder.exitIgnoredCall(functionCall)
|
graphBuilder.exitIgnoredCall(functionCall)
|
||||||
|
|||||||
+35
-28
@@ -12,6 +12,7 @@ import org.jetbrains.kotlin.fir.declarations.utils.referredPropertySymbol
|
|||||||
import org.jetbrains.kotlin.fir.expressions.*
|
import org.jetbrains.kotlin.fir.expressions.*
|
||||||
import org.jetbrains.kotlin.fir.references.FirNamedReference
|
import org.jetbrains.kotlin.fir.references.FirNamedReference
|
||||||
import org.jetbrains.kotlin.fir.resolve.dfa.FirLocalVariableAssignmentAnalyzer.Companion.MiniFlow.Companion.join
|
import org.jetbrains.kotlin.fir.resolve.dfa.FirLocalVariableAssignmentAnalyzer.Companion.MiniFlow.Companion.join
|
||||||
|
import org.jetbrains.kotlin.fir.symbols.impl.FirFunctionSymbol
|
||||||
import org.jetbrains.kotlin.fir.visitors.FirVisitor
|
import org.jetbrains.kotlin.fir.visitors.FirVisitor
|
||||||
import org.jetbrains.kotlin.name.Name
|
import org.jetbrains.kotlin.name.Name
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import org.jetbrains.kotlin.name.Name
|
|||||||
* queries after the traversal is done.
|
* queries after the traversal is done.
|
||||||
**/
|
**/
|
||||||
internal class FirLocalVariableAssignmentAnalyzer(
|
internal class FirLocalVariableAssignmentAnalyzer(
|
||||||
private val assignedLocalVariablesByFunction: Map<FirFunction, AssignedLocalVariables>
|
private val assignedLocalVariablesByFunction: Map<FirFunctionSymbol<*>, AssignedLocalVariables>
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Stack storing concurrent lambda arguments for the current visited anonymous function. For example
|
* Stack storing concurrent lambda arguments for the current visited anonymous function. For example
|
||||||
@@ -70,7 +71,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
// always null and hence there is no need to check it. In addition, since multiple lambda can be passed, we accumulate the
|
// always null and hence there is no need to check it. In addition, since multiple lambda can be passed, we accumulate the
|
||||||
// effects by appending to `ephemeralConcurrentlyAssignedLocalVariables`. After the function call is resolved,
|
// effects by appending to `ephemeralConcurrentlyAssignedLocalVariables`. After the function call is resolved,
|
||||||
// `exitAnonymousFunction` will be invoked at some point to properly set up the `persistentConcurrentlyAssignedLocalVariables`.
|
// `exitAnonymousFunction` will be invoked at some point to properly set up the `persistentConcurrentlyAssignedLocalVariables`.
|
||||||
assignedLocalVariablesByFunction[anonymousFunction]?.insideLocalFunction?.let {
|
assignedLocalVariablesByFunction[anonymousFunction.symbol]?.insideLocalFunction?.let {
|
||||||
ephemeralConcurrentlyAssignedLocalVariables.addAll(it)
|
ephemeralConcurrentlyAssignedLocalVariables.addAll(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,30 +82,32 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun enterLocalFunction(function: FirFunction) {
|
fun enterLocalFunction(function: FirFunction) {
|
||||||
val eventOccurrencesRange: EventOccurrencesRange? = (function as? FirAnonymousFunction)?.invocationKind
|
|
||||||
// carry on concurrently modified variables from the current scope
|
|
||||||
val concurrentlyAssignedLocalVariables = concurrentlyAssignedLocalVariablesStack.last().toMutableSet()
|
val concurrentlyAssignedLocalVariables = concurrentlyAssignedLocalVariablesStack.last().toMutableSet()
|
||||||
concurrentlyAssignedLocalVariablesStack.add(concurrentlyAssignedLocalVariables)
|
concurrentlyAssignedLocalVariablesStack.add(concurrentlyAssignedLocalVariables)
|
||||||
|
|
||||||
val concurrentLambdasInCurrentCall = concurrentLambdaArgsStack.lastOrNull()
|
val concurrentLambdasInCurrentCall = concurrentLambdaArgsStack.lastOrNull()
|
||||||
if (concurrentLambdasInCurrentCall != null && function in concurrentLambdasInCurrentCall) {
|
if (concurrentLambdasInCurrentCall != null && function in concurrentLambdasInCurrentCall) {
|
||||||
concurrentLambdasInCurrentCall.filter { it != function }.forEach { otherLambda ->
|
for (otherLambda in concurrentLambdasInCurrentCall) {
|
||||||
assignedLocalVariablesByFunction[otherLambda]?.insideLocalFunction?.let {
|
// As mentioned in the comment above, we don't know whether other lambda arguments passed to the same call will be
|
||||||
concurrentlyAssignedLocalVariables += it
|
// called before or after this lambda, so their assignments might have executed. Unless they're not called at all.
|
||||||
|
if (otherLambda != function && otherLambda.invocationKind != EventOccurrencesRange.ZERO) {
|
||||||
|
assignedLocalVariablesByFunction[otherLambda.symbol]?.insideLocalFunction?.let {
|
||||||
|
concurrentlyAssignedLocalVariables += it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (eventOccurrencesRange) {
|
|
||||||
|
when ((function as? FirAnonymousFunction)?.invocationKind) {
|
||||||
EventOccurrencesRange.AT_LEAST_ONCE,
|
EventOccurrencesRange.AT_LEAST_ONCE,
|
||||||
EventOccurrencesRange.MORE_THAN_ONCE -> assignedLocalVariablesByFunction[function]?.insideLocalFunction?.let {
|
EventOccurrencesRange.MORE_THAN_ONCE ->
|
||||||
concurrentlyAssignedLocalVariables += it
|
// The function may be called repeatedly so the assignments may have already executed before we enter it again.
|
||||||
}
|
assignedLocalVariablesByFunction[function.symbol]?.insideLocalFunction?.let { concurrentlyAssignedLocalVariables += it }
|
||||||
// Add both inside and outside since this local function may be invoked multiple times concurrently.
|
EventOccurrencesRange.UNKNOWN, null ->
|
||||||
EventOccurrencesRange.UNKNOWN, null -> assignedLocalVariablesByFunction[function]?.all?.let {
|
// The function may not only be called repeatedly, but also stored and called later, so assignments done outside
|
||||||
concurrentlyAssignedLocalVariables += it
|
// its scope after the definition might also have executed.
|
||||||
}
|
assignedLocalVariablesByFunction[function.symbol]?.all?.let { concurrentlyAssignedLocalVariables += it }
|
||||||
else -> {
|
else -> {} // The function is called at most once so its assignments have not executed yet.
|
||||||
// no additional stuff to do for other cases
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,12 +115,13 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
val eventOccurrencesRange: EventOccurrencesRange? = (function as? FirAnonymousFunction)?.invocationKind
|
val eventOccurrencesRange: EventOccurrencesRange? = (function as? FirAnonymousFunction)?.invocationKind
|
||||||
concurrentlyAssignedLocalVariablesStack.removeLast()
|
concurrentlyAssignedLocalVariablesStack.removeLast()
|
||||||
when (eventOccurrencesRange) {
|
when (eventOccurrencesRange) {
|
||||||
EventOccurrencesRange.UNKNOWN, null -> assignedLocalVariablesByFunction[function]?.insideLocalFunction?.let {
|
EventOccurrencesRange.UNKNOWN, null ->
|
||||||
concurrentlyAssignedLocalVariablesStack.last() += it
|
// The function may be stored and then called later, so any access to the variables it touches
|
||||||
}
|
// is no longer smartcastable ever.
|
||||||
else -> {
|
assignedLocalVariablesByFunction[function.symbol]?.insideLocalFunction?.let {
|
||||||
// no additional stuff to do for other cases
|
concurrentlyAssignedLocalVariablesStack.last() += it
|
||||||
}
|
}
|
||||||
|
else -> {} // The function is only called inline; this is handled by CFG construction by visiting the function body.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +221,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
/**
|
/**
|
||||||
* Computes a mini CFG and returns the map tracking assigned local variables at each potentially concurrent local/lambda function.
|
* Computes a mini CFG and returns the map tracking assigned local variables at each potentially concurrent local/lambda function.
|
||||||
*/
|
*/
|
||||||
private fun computeAssignedLocalVariables(firFunction: FirFunction): Map<FirFunction, AssignedLocalVariables> {
|
private fun computeAssignedLocalVariables(firFunction: FirFunction): Map<FirFunctionSymbol<*>, AssignedLocalVariables> {
|
||||||
val startFlow = MiniFlow.start()
|
val startFlow = MiniFlow.start()
|
||||||
val data = MiniCfgBuilder.MiniCfgData(startFlow)
|
val data = MiniCfgBuilder.MiniCfgData(startFlow)
|
||||||
MiniCfgBuilder().visitElement(firFunction, data)
|
MiniCfgBuilder().visitElement(firFunction, data)
|
||||||
@@ -265,7 +269,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMap { it.values })
|
functionFork.assignedLocalVariables.retainAll(data.variableDeclarations.flatMap { it.values })
|
||||||
// Create another fork for the normal execution
|
// Create another fork for the normal execution
|
||||||
val normalExecution = currentFlow.fork()
|
val normalExecution = currentFlow.fork()
|
||||||
data.localFunctionToAssignedLocalVariables[function] =
|
data.localFunctionToAssignedLocalVariables[function.symbol] =
|
||||||
AssignedLocalVariables(normalExecution.assignedLocalVariables, functionFork.assignedLocalVariables)
|
AssignedLocalVariables(normalExecution.assignedLocalVariables, functionFork.assignedLocalVariables)
|
||||||
data.flow = normalExecution
|
data.flow = normalExecution
|
||||||
}
|
}
|
||||||
@@ -292,11 +296,11 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun visitReturnExpression(returnExpression: FirReturnExpression, data: MiniCfgData) {
|
override fun visitReturnExpression(returnExpression: FirReturnExpression, data: MiniCfgData) {
|
||||||
|
super.visitReturnExpression(returnExpression, data)
|
||||||
// TODO: consider to also handle `throw`, which would require keeping track of all `try`, `catch` and `finally` constructs.
|
// TODO: consider to also handle `throw`, which would require keeping track of all `try`, `catch` and `finally` constructs.
|
||||||
data.flow = null
|
data.flow = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
|
||||||
override fun visitFunctionCall(functionCall: FirFunctionCall, data: MiniCfgData) {
|
override fun visitFunctionCall(functionCall: FirFunctionCall, data: MiniCfgData) {
|
||||||
val visitor = this
|
val visitor = this
|
||||||
with(functionCall) {
|
with(functionCall) {
|
||||||
@@ -316,18 +320,21 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun visitProperty(property: FirProperty, data: MiniCfgData) {
|
override fun visitProperty(property: FirProperty, data: MiniCfgData) {
|
||||||
|
super.visitProperty(property, data)
|
||||||
if (property.isLocal) {
|
if (property.isLocal) {
|
||||||
data.variableDeclarations.last()[property.name] = property
|
data.variableDeclarations.last()[property.name] = property
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitVariableAssignment(variableAssignment: FirVariableAssignment, data: MiniCfgData) {
|
override fun visitVariableAssignment(variableAssignment: FirVariableAssignment, data: MiniCfgData) {
|
||||||
|
super.visitVariableAssignment(variableAssignment, data)
|
||||||
val flow = data.flow ?: return
|
val flow = data.flow ?: return
|
||||||
val name = (variableAssignment.lValue as? FirNamedReference)?.name ?: return
|
val name = (variableAssignment.lValue as? FirNamedReference)?.name ?: return
|
||||||
flow.recordAssignment(name, data)
|
flow.recordAssignment(name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun visitAssignmentOperatorStatement(assignmentOperatorStatement: FirAssignmentOperatorStatement, data: MiniCfgData) {
|
override fun visitAssignmentOperatorStatement(assignmentOperatorStatement: FirAssignmentOperatorStatement, data: MiniCfgData) {
|
||||||
|
super.visitAssignmentOperatorStatement(assignmentOperatorStatement, data)
|
||||||
val flow = data.flow ?: return
|
val flow = data.flow ?: return
|
||||||
val lhs = assignmentOperatorStatement.leftArgument as? FirQualifiedAccessExpression ?: return
|
val lhs = assignmentOperatorStatement.leftArgument as? FirQualifiedAccessExpression ?: return
|
||||||
if (lhs.explicitReceiver != null) return
|
if (lhs.explicitReceiver != null) return
|
||||||
@@ -350,7 +357,7 @@ internal class FirLocalVariableAssignmentAnalyzer(
|
|||||||
|
|
||||||
class MiniCfgData(var flow: MiniFlow?) {
|
class MiniCfgData(var flow: MiniFlow?) {
|
||||||
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf()))
|
val variableDeclarations: ArrayDeque<MutableMap<Name, FirProperty>> = ArrayDeque(listOf(mutableMapOf()))
|
||||||
val localFunctionToAssignedLocalVariables: MutableMap<FirFunction, AssignedLocalVariables> = mutableMapOf()
|
val localFunctionToAssignedLocalVariables: MutableMap<FirFunctionSymbol<*>, AssignedLocalVariables> = mutableMapOf()
|
||||||
fun resolveLocalVariable(name: Name): FirProperty? {
|
fun resolveLocalVariable(name: Name): FirProperty? {
|
||||||
return variableDeclarations.asReversed().firstNotNullOfOrNull { it[name] }
|
return variableDeclarations.asReversed().firstNotNullOfOrNull { it[name] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
// Issue: KT-30826
|
|
||||||
|
|
||||||
interface I1
|
|
||||||
interface I2 {
|
|
||||||
fun foo() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class A : I1, I2
|
|
||||||
|
|
||||||
fun foo(x: I1?) {
|
|
||||||
var y = x
|
|
||||||
y as I2
|
|
||||||
val bar = {
|
|
||||||
y.foo() // NPE in NI, smartcast impossible in OI
|
|
||||||
}
|
|
||||||
y = null
|
|
||||||
bar()
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// FIR_IDENTICAL
|
||||||
// Issue: KT-30826
|
// Issue: KT-30826
|
||||||
|
|
||||||
interface I1
|
interface I1
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
// !LANGUAGE: +NewInference
|
|
||||||
// !DIAGNOSTICS: -UNUSED_EXPRESSION
|
|
||||||
// SKIP_TXT
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TESTCASE NUMBER: 1
|
|
||||||
* ISSUES: KT-30826
|
|
||||||
*/
|
|
||||||
fun case_1(x: Interface1?) {
|
|
||||||
var y = x
|
|
||||||
y as Interface2
|
|
||||||
val foo = {
|
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("Interface1? & Interface1 & Interface2")!>y<!>.itest2()
|
|
||||||
}
|
|
||||||
y = null
|
|
||||||
foo()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TESTCASE NUMBER: 2
|
|
||||||
* ISSUES: KT-30826
|
|
||||||
*/
|
|
||||||
fun case_2(x: Interface1?) {
|
|
||||||
var y = x
|
|
||||||
y as Interface2
|
|
||||||
val foo = fun () {
|
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("Interface1? & Interface1 & Interface2")!>y<!>.itest2()
|
|
||||||
}
|
|
||||||
y = null
|
|
||||||
foo()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TESTCASE NUMBER: 3
|
|
||||||
* ISSUES: KT-30826
|
|
||||||
*/
|
|
||||||
fun case_3(x: Interface1?) {
|
|
||||||
var y = x
|
|
||||||
y as Interface2
|
|
||||||
fun foo() {
|
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("Interface1?"), SMARTCAST_IMPOSSIBLE!>y<!>.itest2()
|
|
||||||
}
|
|
||||||
y = null
|
|
||||||
foo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTCASE NUMBER: 4
|
|
||||||
fun case_4(x: Interface1?) {
|
|
||||||
var y = x
|
|
||||||
y as Interface2
|
|
||||||
y = null
|
|
||||||
fun foo() {
|
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("Interface1?")!>y<!>.<!UNRESOLVED_REFERENCE!>itest2<!>()
|
|
||||||
}
|
|
||||||
y = x
|
|
||||||
foo()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TESTCASE NUMBER: 5
|
|
||||||
fun case_5(x: Interface1?) {
|
|
||||||
var y = x
|
|
||||||
y as Interface2
|
|
||||||
y = null
|
|
||||||
fun foo() {
|
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("Interface1?")!>y<!>.<!UNRESOLVED_REFERENCE!>itest2<!>()
|
|
||||||
}
|
|
||||||
y = x
|
|
||||||
foo()
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// FIR_IDENTICAL
|
||||||
// !LANGUAGE: +NewInference
|
// !LANGUAGE: +NewInference
|
||||||
// !DIAGNOSTICS: -UNUSED_EXPRESSION
|
// !DIAGNOSTICS: -UNUSED_EXPRESSION
|
||||||
// SKIP_TXT
|
// SKIP_TXT
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ fun case_2() {
|
|||||||
var x: Int? = 10
|
var x: Int? = 10
|
||||||
var y = { x = null }
|
var y = { x = null }
|
||||||
if (x != null) {
|
if (x != null) {
|
||||||
val z = case_2(<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int? & kotlin.Int")!>x<!>)
|
val z = case_2(<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>x<!>)
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.String")!>z<!>
|
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ fun case_4(x: Int) = ""
|
|||||||
fun case_4(x: Int?) = 10
|
fun case_4(x: Int?) = 10
|
||||||
fun case_4(y: Case4) {
|
fun case_4(y: Case4) {
|
||||||
if (y.x != null) {
|
if (y.x != null) {
|
||||||
val z = case_4(y.x)
|
val z = case_4(<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>y.x<!>)
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ fun case_5(x: Int) = ""
|
|||||||
fun case_5(x: Int?) = 10
|
fun case_5(x: Int?) = 10
|
||||||
fun case_5(y: Case4) {
|
fun case_5(y: Case4) {
|
||||||
if (y.x != null) {
|
if (y.x != null) {
|
||||||
val z = case_5(y.x)
|
val z = case_5(<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>y.x<!>)
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ fun case_6(x: Int) = ""
|
|||||||
fun case_6(x: Int?) = 10
|
fun case_6(x: Int?) = 10
|
||||||
fun case_6(y: Case4) {
|
fun case_6(y: Case4) {
|
||||||
if (y.x != null) {
|
if (y.x != null) {
|
||||||
val z = case_6(y.x)
|
val z = case_6(<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int?")!>y.x<!>)
|
||||||
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
<!DEBUG_INFO_EXPRESSION_TYPE("kotlin.Int")!>z<!>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user