diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/UnusedChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/UnusedChecker.kt index 0350230bc94..2aa5022b6a5 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/UnusedChecker.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/UnusedChecker.kt @@ -36,41 +36,52 @@ object UnusedChecker : FirControlFlowChecker() { } class CfaVisitor( - val data: Map, VariableStatusInfo>, + val data: Map, PathAwareVariableStatusInfo>, val reporter: DiagnosticReporter ) : ControlFlowGraphVisitorVoid() { override fun visitNode(node: CFGNode<*>) {} override fun visitVariableAssignmentNode(node: VariableAssignmentNode) { val variableSymbol = (node.fir.calleeReference as? FirResolvedNamedReference)?.resolvedSymbol ?: return - val data = data[node]?.get(variableSymbol) ?: return - if (data == VariableStatus.ONLY_WRITTEN_NEVER_READ) { - // todo: report case like "a += 1" where `a` `doesn't writes` different way (special for Idea) - val source = node.fir.lValue.source - reporter.report(source, FirErrors.ASSIGNED_VALUE_IS_NEVER_READ) + val dataPerNode = data[node] ?: return + for (label in dataPerNode.keys) { + val data = dataPerNode[label]!![variableSymbol] ?: continue + if (data == VariableStatus.ONLY_WRITTEN_NEVER_READ) { + // todo: report case like "a += 1" where `a` `doesn't writes` different way (special for Idea) + val source = node.fir.lValue.source + reporter.report(source, FirErrors.ASSIGNED_VALUE_IS_NEVER_READ) + // To avoid duplicate reports, stop investigating remaining paths once reported. + break + } } } override fun visitVariableDeclarationNode(node: VariableDeclarationNode) { val variableSymbol = node.fir.symbol if (variableSymbol.isLoopIterator) return - val data = data[node]?.get(variableSymbol) ?: return + val dataPerNode = data[node] ?: return + for (label in dataPerNode.keys) { + val data = dataPerNode[label]!![variableSymbol] ?: continue - val variableSource = variableSymbol.fir.source.takeIf { it?.elementType != KtNodeTypes.DESTRUCTURING_DECLARATION } - when { - data == VariableStatus.UNUSED -> { - if ((node.fir.initializer as? FirFunctionCall)?.isIterator != true) { - reporter.report(variableSource, FirErrors.UNUSED_VARIABLE) + val variableSource = variableSymbol.fir.source.takeIf { it?.elementType != KtNodeTypes.DESTRUCTURING_DECLARATION } + when { + data == VariableStatus.UNUSED -> { + if ((node.fir.initializer as? FirFunctionCall)?.isIterator != true) { + reporter.report(variableSource, FirErrors.UNUSED_VARIABLE) + break + } + } + data.isRedundantInit -> { + val source = variableSymbol.fir.initializer?.source + reporter.report(source, FirErrors.VARIABLE_INITIALIZER_IS_REDUNDANT) + break + } + data == VariableStatus.ONLY_WRITTEN_NEVER_READ -> { + reporter.report(variableSource, FirErrors.VARIABLE_NEVER_READ) + break + } + else -> { } - } - data.isRedundantInit -> { - val source = variableSymbol.fir.initializer?.source - reporter.report(source, FirErrors.VARIABLE_INITIALIZER_IS_REDUNDANT) - } - data == VariableStatus.ONLY_WRITTEN_NEVER_READ -> { - reporter.report(variableSource, FirErrors.VARIABLE_NEVER_READ) - } - else -> { } } } @@ -122,72 +133,102 @@ object UnusedChecker : FirControlFlowChecker() { } + class PathAwareVariableStatusInfo( + map: PersistentMap = persistentMapOf() + ) : PathAwareControlFlowInfo(map) { + companion object { + val EMPTY = PathAwareVariableStatusInfo(persistentMapOf(NormalPath to VariableStatusInfo.EMPTY)) + } + + override val constructor: (PersistentMap) -> PathAwareVariableStatusInfo = + ::PathAwareVariableStatusInfo + + override val empty: () -> PathAwareVariableStatusInfo = + ::EMPTY + } + private class ValueWritesWithoutReading( private val localProperties: Set - ) : ControlFlowGraphVisitor>() { - fun getData(graph: ControlFlowGraph): Map, VariableStatusInfo> { - return graph.collectDataForNode(TraverseDirection.Backward, VariableStatusInfo.EMPTY, this) + ) : ControlFlowGraphVisitor>>() { + fun getData(graph: ControlFlowGraph): Map, PathAwareVariableStatusInfo> { + return graph.collectPathAwareDataForNode(TraverseDirection.Backward, PathAwareVariableStatusInfo.EMPTY, this) } - override fun visitNode(node: CFGNode<*>, data: Collection): VariableStatusInfo { - if (data.isEmpty()) return VariableStatusInfo.EMPTY - return data.reduce(VariableStatusInfo::merge) + override fun visitNode( + node: CFGNode<*>, + data: Collection> + ): PathAwareVariableStatusInfo { + if (data.isEmpty()) return PathAwareVariableStatusInfo.EMPTY + return data.map { (label, info) -> info.applyLabel(node, label) } + .reduce(PathAwareVariableStatusInfo::merge) } - override fun visitVariableDeclarationNode(node: VariableDeclarationNode, data: Collection): VariableStatusInfo { + override fun visitVariableDeclarationNode( + node: VariableDeclarationNode, + data: Collection> + ): PathAwareVariableStatusInfo { val dataForNode = visitNode(node, data) if (node.fir.source?.kind is FirFakeSourceElementKind) return dataForNode val symbol = node.fir.symbol - return when (dataForNode[symbol]) { - null -> { - dataForNode.put(symbol, VariableStatus.UNUSED) - } - VariableStatus.ONLY_WRITTEN_NEVER_READ, VariableStatus.WRITTEN_AFTER_READ -> { - if (node.fir.initializer != null && dataForNode[symbol]?.isRead == true) { - val newData = dataForNode[symbol] ?: VariableStatus.UNUSED - newData.isRedundantInit = true - dataForNode.put(symbol, newData) - } else if (node.fir.initializer != null) { - dataForNode.put(symbol, VariableStatus.ONLY_WRITTEN_NEVER_READ) - } else { - dataForNode + return update(dataForNode, symbol) { prev -> + when (prev) { + null -> { + VariableStatus.UNUSED + } + VariableStatus.ONLY_WRITTEN_NEVER_READ, VariableStatus.WRITTEN_AFTER_READ -> { + if (node.fir.initializer != null && prev.isRead) { + prev.isRedundantInit = true + prev + } else if (node.fir.initializer != null) { + VariableStatus.ONLY_WRITTEN_NEVER_READ + } else { + null + } + } + VariableStatus.READ -> { + VariableStatus.READ + } + else -> { + null } - } - VariableStatus.READ -> { - dataForNode.put(symbol, VariableStatus.READ) - } - else -> { - dataForNode } } } - override fun visitVariableAssignmentNode(node: VariableAssignmentNode, data: Collection): VariableStatusInfo { + override fun visitVariableAssignmentNode( + node: VariableAssignmentNode, + data: Collection> + ): PathAwareVariableStatusInfo { val dataForNode = visitNode(node, data) val reference = node.fir.lValue as? FirResolvedNamedReference ?: return dataForNode val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return dataForNode - val toPut = when { - symbol !in localProperties -> { - null - } - dataForNode[symbol] == VariableStatus.READ -> { - VariableStatus.WRITTEN_AFTER_READ - } - dataForNode[symbol] == VariableStatus.WRITTEN_AFTER_READ -> { - VariableStatus.ONLY_WRITTEN_NEVER_READ - } - else -> { - VariableStatus.ONLY_WRITTEN_NEVER_READ.merge(dataForNode[symbol] ?: VariableStatus.UNUSED) + return update(dataForNode, symbol) update@{ prev -> + val toPut = when { + symbol !in localProperties -> { + null + } + prev == VariableStatus.READ -> { + VariableStatus.WRITTEN_AFTER_READ + } + prev == VariableStatus.WRITTEN_AFTER_READ -> { + VariableStatus.ONLY_WRITTEN_NEVER_READ + } + else -> { + VariableStatus.ONLY_WRITTEN_NEVER_READ.merge(prev ?: VariableStatus.UNUSED) + } } + + toPut ?: return@update null + + toPut.isRead = prev?.isRead ?: false + toPut } - - toPut ?: return dataForNode - - toPut.isRead = dataForNode[symbol]?.isRead ?: false - return dataForNode.put(symbol, toPut) } - override fun visitQualifiedAccessNode(node: QualifiedAccessNode, data: Collection): VariableStatusInfo { + override fun visitQualifiedAccessNode( + node: QualifiedAccessNode, + data: Collection> + ): PathAwareVariableStatusInfo { val dataForNode = visitNode(node, data) if (node.fir.source?.kind is FirFakeSourceElementKind) return dataForNode val reference = node.fir.calleeReference as? FirResolvedNamedReference ?: return dataForNode @@ -197,7 +238,27 @@ object UnusedChecker : FirControlFlowChecker() { val status = VariableStatus.READ status.isRead = true - return dataForNode.put(symbol, status) + return update(dataForNode, symbol) { status } + } + + private fun update( + info: PathAwareVariableStatusInfo, + symbol: FirPropertySymbol, + updater: (VariableStatus?) -> VariableStatus?, + ): PathAwareVariableStatusInfo { + var resultMap = persistentMapOf() + var changed = false + for (label in info.keys) { + val dataPerLabel = info[label]!! + val v = updater.invoke(dataPerLabel[symbol]) + if (v != null) { + resultMap = resultMap.put(label, dataPerLabel.put(symbol, v)) + changed = true + } else { + resultMap = resultMap.put(label, dataPerLabel) + } + } + return if (changed) PathAwareVariableStatusInfo(resultMap) else info } }