From d7b3a86f5ec08c68be5e24427826346378388f77 Mon Sep 17 00:00:00 2001 From: vldf Date: Tue, 21 Jul 2020 16:24:21 +0300 Subject: [PATCH] [FIR] Add "can be val" extended checker --- .../RedundantExplicitTypeChecker.kt | 2 +- .../RedundantVisibilityModifierChecker.kt | 2 +- .../VariableAssignmentChecker.kt | 187 ++++++++++++++ .../VariableAssignmentChecker.txt | 240 ++++++++++++++++++ .../multipleOperators.kt | 2 +- .../multipleOperatorsRightSideRepeat.kt | 2 +- .../ExtendedFirDiagnosticsTestGenerated.java | 5 + .../kotlin/fir/analysis/CheckersComponent.kt | 5 + ...bstractFirPropertyInitializationChecker.kt | 104 ++++++++ .../analysis/cfa/FirControlFlowAnalyzer.kt | 8 +- .../cfa/FirPropertyInitializationAnalyzer.kt | 94 +------ .../checkers/cfa/FirControlFlowChecker.kt | 13 + .../declaration/DeclarationCheckers.kt | 3 + .../declaration/DefaultDeclarationCheckers.kt | 7 + .../ExtendedDeclarationCheckers.kt | 6 +- .../extended/VariableAssignmentChecker.kt | 110 ++++++++ .../ControlFlowAnalysisDiagnosticComponent.kt | 2 +- .../fir/analysis/diagnostics/FirErrors.kt | 3 +- .../description/EventOccurrencesRange.kt | 18 +- 19 files changed, 709 insertions(+), 104 deletions(-) create mode 100644 compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.kt create mode 100644 compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.txt create mode 100644 compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/AbstractFirPropertyInitializationChecker.kt create mode 100644 compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/cfa/FirControlFlowChecker.kt create mode 100644 compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/VariableAssignmentChecker.kt diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantExplicitTypeChecker.kt b/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantExplicitTypeChecker.kt index f17ed45d2a5..11495ecde4f 100644 --- a/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantExplicitTypeChecker.kt +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantExplicitTypeChecker.kt @@ -52,7 +52,7 @@ fun foo() { val piFloat: Float = 3.14f val piDouble: Double = 3.14 val charZ: Char = 'z' - var alpha: Int = 0 + var alpha: Int = 0 } fun test(boolean: Boolean) { diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantVisibilityModifierChecker.kt b/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantVisibilityModifierChecker.kt index add1dfeb269..473c692097d 100644 --- a/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantVisibilityModifierChecker.kt +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/RedundantVisibilityModifierChecker.kt @@ -4,7 +4,7 @@ fun f() { internal var foo = 0 } LocalClass().foo = 1 - public var baz = 0 + public var baz = 0 } internal inline fun internal() { diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.kt b/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.kt new file mode 100644 index 00000000000..f924c4a4db0 --- /dev/null +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.kt @@ -0,0 +1,187 @@ +// WITH_RUNTIME + +import kotlin.reflect.KProperty +import kotlin.properties.Delegates + +fun testDelegator() { + var x: Boolean by LocalFreezableVar(true) + var y by LocalFreezableVar("") +} + +class LocalFreezableVar(private var value: T) { + operator fun getValue(thisRef: Nothing?, property: KProperty<*>): T = value + + operator fun setValue(thisRef: Nothing?, property: KProperty<*>, value: T) { + this.value = value + } +} + + +class C +operator fun C.plus(a: Any): C = this +operator fun C.plusAssign(a: Any) {} + +fun testOperatorAssignment() { + val c = C() + c += "" + var c1 = C() + c1 += "" + + var a = 1 + a += 12 + a -= 10 +} + + +fun destructuringDeclaration() { + var (v1, v2) = getPair() + print(v1) + + var (v3, v4) = getPair() + print(v3) + v4 = "" + + var (v5, v6) = getPair() + v5 = 1 + + var (v7, v8) = getPair() + v7 = 2 + v8 = "42" + + val (a, b, c) = Triple(1, 1, 1) + + var (x, y, z) = Triple(1, 1, 1) +} + +fun stackOverflowBug() { + var a: Int + a = 1 + for (i in 1..10) + print(i) +} + +fun smth(flag: Boolean) { + var a = 1 + + if (flag) { + while (a > 0) { + a-- + } + } +} + +fun withAnnotation(p: List) { + @Suppress("UNCHECKED_CAST") + var v = p as List + print(v) +} + +fun withReadonlyDeligate() { + val s: String by lazy { "Hello!" } + s.hashCode() +} + +fun getPair(): Pair = Pair(1, "1") + +fun listReceiver(p: List) {} + +fun withInitializer() { + var v1 = 1 + var v2 = 2 + var v3 = 3 + v1 = 1 + v2++ + print(v3) +} + +fun test() { + var a = 0 + while (a>0) { + a++ + } +} + +fun foo() { + var a: Int + val bool = true + val b: String + + if (bool) a = 4 else a = 42 + + bool = false +} + +fun cycles() { + var a = 10 + while (a > 0) { + a-- + } + + val b: Int + while (a < 10) { + a++ + b = a + } +} + +fun assignedTwice(p: Int) { + var v: Int + v = 0 + if (p > 0) v = 1 +} + +fun main(args: Array) { + var a: String? + + if (args.size == 1) { + a = args[0] + } + else { + a = args.toString() + } + + if (a != null && a.equals("cde")) return +} + +fun run(f: () -> Unit) = f() + +fun lambda() { + var a: Int + a = 10 + + run { + a = 20 + } +} + +fun lambdaInitialization() { + var a: Int + + run { + a = 20 + } +} + +fun notAssignedWhenNotUsed(p: Int) { + var v: Int + if (p > 0) { + v = 1 + print(v) + } +} + +var global = 1 + +class C { + var field = 2 + + fun foo() { + print(field) + print(global) + } +} + +fun withDelegate() { + var s: String by Delegates.notNull() + s = "" +} diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.txt b/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.txt new file mode 100644 index 00000000000..39d296da7a3 --- /dev/null +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.txt @@ -0,0 +1,240 @@ +FILE: VariableAssignmentChecker.kt + public final fun testDelegator(): R|kotlin/Unit| { + lvar x: R|kotlin/Boolean|by R|/LocalFreezableVar.LocalFreezableVar|(Boolean(true)) + lvar y: R|kotlin/String|by R|/LocalFreezableVar.LocalFreezableVar|(String()) + } + public final class LocalFreezableVar : R|kotlin/Any| { + public constructor(value: R|T|): R|LocalFreezableVar| { + super() + } + + private final var value: R|T| = R|/value| + private get(): R|T| + private set(value: R|T|): R|kotlin/Unit| + + public final operator fun getValue(thisRef: R|kotlin/Nothing?|, property: R|kotlin/reflect/KProperty<*>|): R|T| { + ^getValue this@R|/LocalFreezableVar|.R|/LocalFreezableVar.value| + } + + public final operator fun setValue(thisRef: R|kotlin/Nothing?|, property: R|kotlin/reflect/KProperty<*>|, value: R|T|): R|kotlin/Unit| { + this@R|/LocalFreezableVar|.R|/LocalFreezableVar.value| = R|/value| + } + + } + public final class C : R|kotlin/Any| { + public constructor(): R|C| { + super() + } + + } + public final operator fun R|C|.plus(a: R|kotlin/Any|): R|C| { + ^plus this@R|/plus| + } + public final operator fun R|C|.plusAssign(a: R|kotlin/Any|): R|kotlin/Unit| { + } + public final fun testOperatorAssignment(): R|kotlin/Unit| { + lval c: R|C| = R|/C.C|() + R|/c|.R|/plusAssign|(String()) + lvar c1: R|C| = R|/C.C|() + ERROR_EXPR(Operator overload ambiguity. Compatible candidates: [/plus, /plusAssign]) + lvar a: R|kotlin/Int| = Int(1) + R|/a| = R|/a|.R|kotlin/Int.plus|(Int(12)) + R|/a| = R|/a|.R|kotlin/Int.minus|(Int(10)) + } + public final fun destructuringDeclaration(): R|kotlin/Unit| { + lval : R|kotlin/Pair| = R|/getPair|() + lvar v1: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar v2: R|kotlin/String| = R|/|.R|FakeOverride|() + R|kotlin/io/print|(R|/v1|) + lval : R|kotlin/Pair| = R|/getPair|() + lvar v3: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar v4: R|kotlin/String| = R|/|.R|FakeOverride|() + R|kotlin/io/print|(R|/v3|) + R|/v4| = String() + lval : R|kotlin/Pair| = R|/getPair|() + lvar v5: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar v6: R|kotlin/String| = R|/|.R|FakeOverride|() + R|/v5| = Int(1) + lval : R|kotlin/Pair| = R|/getPair|() + lvar v7: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar v8: R|kotlin/String| = R|/|.R|FakeOverride|() + R|/v7| = Int(2) + R|/v8| = String(42) + lval : R|kotlin/Triple| = R|kotlin/Triple.Triple|(Int(1), Int(1), Int(1)) + lval a: R|kotlin/Int| = R|/|.R|FakeOverride|() + lval b: R|kotlin/Int| = R|/|.R|FakeOverride|() + lval c: R|kotlin/Int| = R|/|.R|FakeOverride|() + lval : R|kotlin/Triple| = R|kotlin/Triple.Triple|(Int(1), Int(1), Int(1)) + lvar x: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar y: R|kotlin/Int| = R|/|.R|FakeOverride|() + lvar z: R|kotlin/Int| = R|/|.R|FakeOverride|() + } + public final fun stackOverflowBug(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| + R|/a| = Int(1) + lval : R|kotlin/collections/IntIterator| = Int(1).R|kotlin/Int.rangeTo|(Int(10)).R|kotlin/ranges/IntProgression.iterator|() + while(R|/|.R|kotlin/collections/Iterator.hasNext|()) { + lval i: R|kotlin/Int| = R|/|.R|kotlin/collections/IntIterator.next|() + R|kotlin/io/print|(R|/i|) + } + + } + public final fun smth(flag: R|kotlin/Boolean|): R|kotlin/Unit| { + lvar a: R|kotlin/Int| = Int(1) + when () { + R|/flag| -> { + while(CMP(>, R|/a|.R|kotlin/Int.compareTo|(Int(0)))) { + lval : R|kotlin/Int| = R|/a| + R|/a| = R|/|.R|kotlin/Int.dec|() + R|/| + } + + } + } + + } + public final fun withAnnotation(p: R|kotlin/collections/List|): R|kotlin/Unit| { + @R|kotlin/Suppress|(vararg(String(UNCHECKED_CAST))) lvar v: R|kotlin/collections/List| = (R|/p| as R|kotlin/collections/List|) + R|kotlin/io/print|(R|/v|) + } + public final fun withReadonlyDeligate(): R|kotlin/Unit| { + lval s: R|kotlin/String|by R|kotlin/lazy|( = lazy@fun (): R|kotlin/String| { + ^ String(Hello!) + } + ) + R|/s|.R|kotlin/Any.hashCode|() + } + public final fun getPair(): R|kotlin/Pair| { + ^getPair R|kotlin/Pair.Pair|(Int(1), String(1)) + } + public final fun listReceiver(p: R|kotlin/collections/List|): R|kotlin/Unit| { + } + public final fun withInitializer(): R|kotlin/Unit| { + lvar v1: R|kotlin/Int| = Int(1) + lvar v2: R|kotlin/Int| = Int(2) + lvar v3: R|kotlin/Int| = Int(3) + R|/v1| = Int(1) + lval : R|kotlin/Int| = R|/v2| + R|/v2| = R|/|.R|kotlin/Int.inc|() + R|/| + R|kotlin/io/print|(R|/v3|) + } + public final fun test(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| = Int(0) + while(CMP(>, R|/a|.R|kotlin/Int.compareTo|(Int(0)))) { + lval : R|kotlin/Int| = R|/a| + R|/a| = R|/|.R|kotlin/Int.inc|() + R|/| + } + + } + public final fun foo(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| + lval bool: R|kotlin/Boolean| = Boolean(true) + lval b: R|kotlin/String| + when () { + R|/bool| -> { + R|/a| = Int(4) + } + else -> { + R|/a| = Int(42) + } + } + + R|/bool| = Boolean(false) + } + public final fun cycles(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| = Int(10) + while(CMP(>, R|/a|.R|kotlin/Int.compareTo|(Int(0)))) { + lval : R|kotlin/Int| = R|/a| + R|/a| = R|/|.R|kotlin/Int.dec|() + R|/| + } + + lval b: R|kotlin/Int| + while(CMP(<, R|/a|.R|kotlin/Int.compareTo|(Int(10)))) { + lval : R|kotlin/Int| = R|/a| + R|/a| = R|/|.R|kotlin/Int.inc|() + R|/| + R|/b| = R|/a| + } + + } + public final fun assignedTwice(p: R|kotlin/Int|): R|kotlin/Unit| { + lvar v: R|kotlin/Int| + R|/v| = Int(0) + when () { + CMP(>, R|/p|.R|kotlin/Int.compareTo|(Int(0))) -> { + R|/v| = Int(1) + } + } + + } + public final fun main(args: R|kotlin/Array|): R|kotlin/Unit| { + lvar a: R|kotlin/String?| + when () { + ==(R|/args|.R|kotlin/Array.size|, Int(1)) -> { + R|/a| = R|/args|.R|FakeOverride|(Int(0)) + } + else -> { + R|/a| = R|/args|.R|kotlin/Any.toString|() + } + } + + when () { + !=(R|/a|, Null(null)) && R|/a|.R|kotlin/Any.equals|(String(cde)) -> { + ^main Unit + } + } + + } + public final fun run(f: R|() -> kotlin/Unit|): R|kotlin/Unit| { + ^run R|/f|.R|FakeOverride|() + } + public final fun lambda(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| + R|/a| = Int(10) + R|/run|( = run@fun (): R|kotlin/Unit| { + R|/a| = Int(20) + } + ) + } + public final fun lambdaInitialization(): R|kotlin/Unit| { + lvar a: R|kotlin/Int| + R|/run|( = run@fun (): R|kotlin/Unit| { + R|/a| = Int(20) + } + ) + } + public final fun notAssignedWhenNotUsed(p: R|kotlin/Int|): R|kotlin/Unit| { + lvar v: R|kotlin/Int| + when () { + CMP(>, R|/p|.R|kotlin/Int.compareTo|(Int(0))) -> { + R|/v| = Int(1) + R|kotlin/io/print|(R|/v|) + } + } + + } + public final var global: R|kotlin/Int| = Int(1) + public get(): R|kotlin/Int| + public set(value: R|kotlin/Int|): R|kotlin/Unit| + public final class C : R|kotlin/Any| { + public constructor(): R|C| { + super() + } + + public final var field: R|kotlin/Int| = Int(2) + public get(): R|kotlin/Int| + public set(value: R|kotlin/Int|): R|kotlin/Unit| + + public final fun foo(): R|kotlin/Unit| { + R|kotlin/io/print|(this@R|/C|.R|/C.field|) + R|kotlin/io/print|(R|/global|) + } + + } + public final fun withDelegate(): R|kotlin/Unit| { + lvar s: R|kotlin/String|by Q|kotlin/properties/Delegates|.R|kotlin/properties/Delegates.notNull|() + R|/s| = String() + } diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperators.kt b/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperators.kt index ac476e5bb4d..061b14cade8 100644 --- a/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperators.kt +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperators.kt @@ -1,5 +1,5 @@ fun foo() { var x = 0 - var y = 0 + var y = 0 x = x + y + 5 } diff --git a/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperatorsRightSideRepeat.kt b/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperatorsRightSideRepeat.kt index bc17c7ba1b5..0fabdf0cbea 100644 --- a/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperatorsRightSideRepeat.kt +++ b/compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment/multipleOperatorsRightSideRepeat.kt @@ -1,5 +1,5 @@ fun foo() { var x = 0 - var y = 0 + var y = 0 x = y + x + 5 } diff --git a/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/ExtendedFirDiagnosticsTestGenerated.java b/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/ExtendedFirDiagnosticsTestGenerated.java index 603e246688b..1b3951e6070 100644 --- a/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/ExtendedFirDiagnosticsTestGenerated.java +++ b/compiler/fir/analysis-tests/tests/org/jetbrains/kotlin/fir/ExtendedFirDiagnosticsTestGenerated.java @@ -48,6 +48,11 @@ public class ExtendedFirDiagnosticsTestGenerated extends AbstractExtendedFirDiag runTest("compiler/fir/analysis-tests/testData/extendedCheckers/RedundantVisibilityModifierChecker.kt"); } + @TestMetadata("VariableAssignmentChecker.kt") + public void testVariableAssignmentChecker() throws Exception { + runTest("compiler/fir/analysis-tests/testData/extendedCheckers/VariableAssignmentChecker.kt"); + } + @TestMetadata("compiler/fir/analysis-tests/testData/extendedCheckers/canBeReplacedWithOperatorAssignment") @TestDataPath("$PROJECT_ROOT") @RunWith(JUnit3RunnerWithInners.class) diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/CheckersComponent.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/CheckersComponent.kt index d1feafae5e3..313fe351087 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/CheckersComponent.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/CheckersComponent.kt @@ -7,6 +7,7 @@ package org.jetbrains.kotlin.fir.analysis import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.FirSessionComponent +import org.jetbrains.kotlin.fir.analysis.checkers.cfa.FirControlFlowChecker import org.jetbrains.kotlin.fir.analysis.checkers.declaration.* import org.jetbrains.kotlin.fir.analysis.checkers.expression.* import org.jetbrains.kotlin.fir.analysis.extensions.FirAdditionalCheckersExtension @@ -60,15 +61,19 @@ private class ComposedDeclarationCheckers : DeclarationCheckers() { override val constructorCheckers: List get() = _constructorCheckers + override val controlFlowAnalyserCheckers: List + get() = _controlFlowAnalyserCheckers private val _declarationCheckers: MutableList = mutableListOf() private val _memberDeclarationCheckers: MutableList = mutableListOf() private val _constructorCheckers: MutableList = mutableListOf() + private val _controlFlowAnalyserCheckers: MutableList = mutableListOf() fun register(checkers: DeclarationCheckers) { _declarationCheckers += checkers.declarationCheckers _memberDeclarationCheckers += checkers.allMemberDeclarationCheckers _constructorCheckers += checkers.allConstructorCheckers + _controlFlowAnalyserCheckers += checkers.controlFlowAnalyserCheckers } } diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/AbstractFirPropertyInitializationChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/AbstractFirPropertyInitializationChecker.kt new file mode 100644 index 00000000000..27ebedf4e88 --- /dev/null +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/AbstractFirPropertyInitializationChecker.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.fir.analysis.cfa + +import kotlinx.collections.immutable.PersistentMap +import kotlinx.collections.immutable.persistentMapOf +import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange +import org.jetbrains.kotlin.fir.analysis.checkers.cfa.FirControlFlowChecker +import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* +import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol + +abstract class AbstractFirPropertyInitializationChecker : FirControlFlowChecker() { + abstract override fun analyze(graph: ControlFlowGraph, reporter: DiagnosticReporter) + + class PropertyInitializationInfo( + map: PersistentMap = persistentMapOf() + ) : ControlFlowInfo(map) { + companion object { + val EMPTY = PropertyInitializationInfo() + } + + override val constructor: (PersistentMap) -> PropertyInitializationInfo = + ::PropertyInitializationInfo + + fun merge(other: PropertyInitializationInfo): PropertyInitializationInfo { + var result = this + for (symbol in keys.union(other.keys)) { + val kind1 = this[symbol] ?: EventOccurrencesRange.ZERO + val kind2 = other[symbol] ?: EventOccurrencesRange.ZERO + result = result.put(symbol, kind1 or kind2) + } + return result + } + } + + class LocalPropertyCollector private constructor() : ControlFlowGraphVisitorVoid() { + companion object { + fun collect(graph: ControlFlowGraph): MutableSet { + val collector = LocalPropertyCollector() + graph.traverse(TraverseDirection.Forward, collector) + return collector.symbols + } + } + + private val symbols: MutableSet = mutableSetOf() + + override fun visitNode(node: CFGNode<*>) {} + + override fun visitVariableDeclarationNode(node: VariableDeclarationNode) { + symbols += node.fir.symbol + } + } + + class DataCollector(private val localProperties: Set) : + ControlFlowGraphVisitor>() { + override fun visitNode(node: CFGNode<*>, data: Collection): PropertyInitializationInfo { + if (data.isEmpty()) return PropertyInitializationInfo.EMPTY + return data.reduce(PropertyInitializationInfo::merge) + } + + override fun visitVariableAssignmentNode( + node: VariableAssignmentNode, + data: Collection + ): PropertyInitializationInfo { + val dataForNode = visitNode(node, data) + val reference = node.fir.lValue as? FirResolvedNamedReference ?: return dataForNode + val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return dataForNode + return if (symbol !in localProperties) { + dataForNode + } else { + processVariableWithAssignment(dataForNode, symbol) + } + } + + override fun visitVariableDeclarationNode( + node: VariableDeclarationNode, + data: Collection + ): PropertyInitializationInfo { + val dataForNode = visitNode(node, data) + return if (node.fir.initializer == null && node.fir.delegate == null) { + dataForNode + } else { + processVariableWithAssignment(dataForNode, node.fir.symbol) + } + } + + fun getData(graph: ControlFlowGraph) = + graph.collectDataForNode(TraverseDirection.Forward, PropertyInitializationInfo.EMPTY, DataCollector(localProperties)) + + private fun processVariableWithAssignment( + dataForNode: PropertyInitializationInfo, + symbol: FirPropertySymbol + ): PropertyInitializationInfo { + val existingKind = dataForNode[symbol] ?: EventOccurrencesRange.ZERO + val kind = existingKind + EventOccurrencesRange.EXACTLY_ONCE + return dataForNode.put(symbol, kind) + } + } +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirControlFlowAnalyzer.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirControlFlowAnalyzer.kt index ed510cc850b..6ec13167390 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirControlFlowAnalyzer.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirControlFlowAnalyzer.kt @@ -5,15 +5,17 @@ package org.jetbrains.kotlin.fir.analysis.cfa +import org.jetbrains.kotlin.fir.FirSession import org.jetbrains.kotlin.fir.analysis.checkers.context.CheckerContext +import org.jetbrains.kotlin.fir.analysis.checkersComponent import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter import org.jetbrains.kotlin.fir.declarations.FirClass import org.jetbrains.kotlin.fir.declarations.FirFunction import org.jetbrains.kotlin.fir.declarations.FirProperty import org.jetbrains.kotlin.fir.resolve.dfa.cfg.ControlFlowGraph -class FirControlFlowAnalyzer { - private val propertyInitializationAnalyzer = FirPropertyInitializationAnalyzer() +class FirControlFlowAnalyzer(session: FirSession) { + private val checkers = session.checkersComponent.declarationCheckers.controlFlowAnalyserCheckers fun analyzeClassInitializer(klass: FirClass<*>, graph: ControlFlowGraph, context: CheckerContext, reporter: DiagnosticReporter) { if (graph.owner != null) return @@ -22,7 +24,7 @@ class FirControlFlowAnalyzer { fun analyzeFunction(function: FirFunction<*>, graph: ControlFlowGraph, context: CheckerContext, reporter: DiagnosticReporter) { if (graph.owner != null) return - propertyInitializationAnalyzer.analyze(graph, reporter) + checkers.forEach { it.analyze(graph, reporter) } } fun analyzePropertyInitializer(property: FirProperty, graph: ControlFlowGraph, context: CheckerContext, reporter: DiagnosticReporter) { diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirPropertyInitializationAnalyzer.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirPropertyInitializationAnalyzer.kt index 69b6b7483e6..2ef8e7b7aa5 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirPropertyInitializationAnalyzer.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/FirPropertyInitializationAnalyzer.kt @@ -5,23 +5,24 @@ package org.jetbrains.kotlin.fir.analysis.cfa -import kotlinx.collections.immutable.PersistentMap -import kotlinx.collections.immutable.persistentMapOf import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange import org.jetbrains.kotlin.contracts.description.isDefinitelyVisited import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter import org.jetbrains.kotlin.fir.analysis.diagnostics.FirErrors import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference -import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.CFGNode +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.ControlFlowGraph +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.ControlFlowGraphVisitorVoid +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.QualifiedAccessNode import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol -class FirPropertyInitializationAnalyzer { - fun analyze(graph: ControlFlowGraph, reporter: DiagnosticReporter) { +object FirPropertyInitializationAnalyzer : AbstractFirPropertyInitializationChecker() { + override fun analyze(graph: ControlFlowGraph, reporter: DiagnosticReporter) { val localProperties = LocalPropertyCollector.collect(graph) // we want to analyze only properties without initializers localProperties.retainAll { it.fir.initializer == null && it.fir.delegate == null } if (localProperties.isEmpty()) return - val data = graph.collectDataForNode(TraverseDirection.Forward, PropertyInitializationInfo.EMPTY, DataCollector(localProperties)) + val data = DataCollector(localProperties).getData(graph) val reporterVisitor = UninitializedPropertyReporter(data, localProperties, reporter) graph.traverse(TraverseDirection.Forward, reporterVisitor) } @@ -45,85 +46,4 @@ class FirPropertyInitializationAnalyzer { } } } - - private class PropertyInitializationInfo( - map: PersistentMap = persistentMapOf() - ) : ControlFlowInfo(map) { - companion object { - val EMPTY = PropertyInitializationInfo() - } - - override val constructor: (PersistentMap) -> PropertyInitializationInfo = - ::PropertyInitializationInfo - - fun merge(other: PropertyInitializationInfo): PropertyInitializationInfo { - var result = this - for (symbol in keys.union(other.keys)) { - val kind1 = this[symbol] ?: EventOccurrencesRange.ZERO - val kind2 = other[symbol] ?: EventOccurrencesRange.ZERO - result = result.put(symbol, kind1 or kind2) - } - return result - } - } - - private class LocalPropertyCollector private constructor() : ControlFlowGraphVisitorVoid() { - companion object { - fun collect(graph: ControlFlowGraph): MutableSet { - val collector = LocalPropertyCollector() - graph.traverse(TraverseDirection.Forward, collector) - return collector.symbols - } - } - - private val symbols: MutableSet = mutableSetOf() - - override fun visitNode(node: CFGNode<*>) {} - - override fun visitVariableDeclarationNode(node: VariableDeclarationNode) { - symbols += node.fir.symbol - } - } - - private class DataCollector(private val localProperties: Set) : ControlFlowGraphVisitor>() { - override fun visitNode(node: CFGNode<*>, data: Collection): PropertyInitializationInfo { - if (data.isEmpty()) return PropertyInitializationInfo.EMPTY - return data.reduce(PropertyInitializationInfo::merge) - } - - override fun visitVariableAssignmentNode( - node: VariableAssignmentNode, - data: Collection - ): PropertyInitializationInfo { - val dataForNode = visitNode(node, data) - val reference = node.fir.lValue as? FirResolvedNamedReference ?: return dataForNode - val symbol = reference.resolvedSymbol as? FirPropertySymbol ?: return dataForNode - return if (symbol !in localProperties) { - dataForNode - } else{ - processVariableWithAssignment(dataForNode, symbol) - } - } - - override fun visitVariableDeclarationNode( - node: VariableDeclarationNode, - data: Collection - ): PropertyInitializationInfo { - val dataForNode = visitNode(node, data) - return if (node.fir.initializer == null && node.fir.delegate == null) { - dataForNode - } else { - processVariableWithAssignment(dataForNode, node.fir.symbol) - } - } - - private fun processVariableWithAssignment( - dataForNode: PropertyInitializationInfo, - symbol: FirPropertySymbol - ): PropertyInitializationInfo { - val existingKind = dataForNode[symbol] ?: EventOccurrencesRange.ZERO - val kind = existingKind + EventOccurrencesRange.EXACTLY_ONCE - return dataForNode.put(symbol, kind) - } - } } diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/cfa/FirControlFlowChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/cfa/FirControlFlowChecker.kt new file mode 100644 index 00000000000..43bb4861c5a --- /dev/null +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/cfa/FirControlFlowChecker.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.fir.analysis.checkers.cfa + +import org.jetbrains.kotlin.fir.analysis.diagnostics.DiagnosticReporter +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.ControlFlowGraph + +abstract class FirControlFlowChecker { + abstract fun analyze(graph: ControlFlowGraph, reporter: DiagnosticReporter) +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DeclarationCheckers.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DeclarationCheckers.kt index 06b3cc9cd18..56f8be27076 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DeclarationCheckers.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DeclarationCheckers.kt @@ -5,6 +5,8 @@ package org.jetbrains.kotlin.fir.analysis.checkers.declaration +import org.jetbrains.kotlin.fir.analysis.checkers.cfa.FirControlFlowChecker + abstract class DeclarationCheckers { companion object { val EMPTY: DeclarationCheckers = object : DeclarationCheckers() {} @@ -13,6 +15,7 @@ abstract class DeclarationCheckers { open val declarationCheckers: List = emptyList() open val memberDeclarationCheckers: List = emptyList() open val constructorCheckers: List = emptyList() + open val controlFlowAnalyserCheckers: List = emptyList() internal val allMemberDeclarationCheckers: List get() = memberDeclarationCheckers + declarationCheckers internal val allConstructorCheckers: List get() = constructorCheckers + allMemberDeclarationCheckers diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DefaultDeclarationCheckers.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DefaultDeclarationCheckers.kt index 97da78334dc..dbef5b328db 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DefaultDeclarationCheckers.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/DefaultDeclarationCheckers.kt @@ -5,6 +5,9 @@ package org.jetbrains.kotlin.fir.analysis.checkers.declaration +import org.jetbrains.kotlin.fir.analysis.cfa.FirPropertyInitializationAnalyzer +import org.jetbrains.kotlin.fir.analysis.checkers.cfa.FirControlFlowChecker + object CommonDeclarationCheckers : DeclarationCheckers() { override val declarationCheckers: List = listOf( FirAnnotationClassDeclarationChecker, @@ -19,4 +22,8 @@ object CommonDeclarationCheckers : DeclarationCheckers() { override val constructorCheckers: List = listOf( FirConstructorAllowedChecker ) + + override val controlFlowAnalyserCheckers: List = listOf( + FirPropertyInitializationAnalyzer + ) } \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/ExtendedDeclarationCheckers.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/ExtendedDeclarationCheckers.kt index f26134cd146..1877d167c90 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/ExtendedDeclarationCheckers.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/declaration/ExtendedDeclarationCheckers.kt @@ -5,6 +5,7 @@ package org.jetbrains.kotlin.fir.analysis.checkers.declaration +import org.jetbrains.kotlin.fir.analysis.checkers.cfa.FirControlFlowChecker import org.jetbrains.kotlin.fir.analysis.checkers.extended.* object ExtendedDeclarationCheckers : DeclarationCheckers() { @@ -18,4 +19,7 @@ object ExtendedDeclarationCheckers : DeclarationCheckers() { RedundantExplicitTypeChecker ) -} + override val controlFlowAnalyserCheckers: List = listOf( + VariableAssignmentChecker + ) +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/VariableAssignmentChecker.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/VariableAssignmentChecker.kt new file mode 100644 index 00000000000..a8563ca2b8c --- /dev/null +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/checkers/extended/VariableAssignmentChecker.kt @@ -0,0 +1,110 @@ +/* + * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.fir.analysis.checkers.extended + + +import org.jetbrains.kotlin.contracts.description.EventOccurrencesRange +import org.jetbrains.kotlin.fir.FirFakeSourceElement +import org.jetbrains.kotlin.fir.FirSourceElement +import org.jetbrains.kotlin.fir.analysis.cfa.AbstractFirPropertyInitializationChecker +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.FirErrors +import org.jetbrains.kotlin.fir.psi +import org.jetbrains.kotlin.fir.references.FirResolvedNamedReference +import org.jetbrains.kotlin.fir.resolve.dfa.cfg.* +import org.jetbrains.kotlin.fir.symbols.impl.FirPropertySymbol +import org.jetbrains.kotlin.fir.toFirPsiSourceElement +import org.jetbrains.kotlin.psi.KtProperty + + +object VariableAssignmentChecker : AbstractFirPropertyInitializationChecker() { + override fun analyze(graph: ControlFlowGraph, reporter: DiagnosticReporter) { + val unprocessedProperties = mutableSetOf() + val propertiesCharacteristics = mutableMapOf() + + val localProperties = LocalPropertyCollector.collect(graph) + if (localProperties.isEmpty()) return + + val data = DataCollector(localProperties).getData(graph) + val reporterVisitor = UninitializedPropertyReporter(data, localProperties, unprocessedProperties, propertiesCharacteristics) + graph.traverse(TraverseDirection.Forward, reporterVisitor) + + for (property in unprocessedProperties) { + if (property.fir.source is FirFakeSourceElement<*>) continue + if (property.callableId.callableName.asString() == "") continue + propertiesCharacteristics[property] = EventOccurrencesRange.ZERO + } + + var lastDestructuringSource: FirSourceElement? = null + var destructuringCanBeVal = false + var lastDestructuredVariables = 0 + + for ((symbol, value) in propertiesCharacteristics) { + val source = symbol.getValOrVarSource + if (symbol.callableId.callableName.asString() == "") { + lastDestructuringSource = symbol.getValOrVarSource + val childrenCount = symbol.fir.psi?.children?.size ?: continue + lastDestructuredVariables = childrenCount - 1 // -1 cuz we don't need expression node after equals operator + destructuringCanBeVal = true + continue + } + + if (lastDestructuringSource != null) { + // if this is the last variable in destructuring declaration and destructuringCanBeVal == true and it can be val + if (lastDestructuredVariables == 1 && destructuringCanBeVal && canBeVal(symbol, value)) { + reporter.report(lastDestructuringSource, FirErrors.CAN_BE_VAL) + lastDestructuringSource = null + } else if (!canBeVal(symbol, value)) { + destructuringCanBeVal = false + } + lastDestructuredVariables-- + } else if (canBeVal(symbol, value) && symbol.fir.delegate == null ) { + reporter.report(source, FirErrors.CAN_BE_VAL) + } + } + } + + private fun canBeVal(symbol: FirPropertySymbol, value: EventOccurrencesRange) = + (value == EventOccurrencesRange.EXACTLY_ONCE + || value == EventOccurrencesRange.AT_MOST_ONCE + || value == EventOccurrencesRange.ZERO + ) && symbol.fir.isVar + + private class UninitializedPropertyReporter( + val data: Map, PropertyInitializationInfo>, + val localProperties: Set, + val unprocessedProperties: MutableSet, + val propertiesCharacteristics: MutableMap + ) : ControlFlowGraphVisitorVoid() { + override fun visitNode(node: CFGNode<*>) {} + + override fun visitVariableAssignmentNode(node: VariableAssignmentNode) { + val symbol = (node.fir.calleeReference as? FirResolvedNamedReference)?.resolvedSymbol as? FirPropertySymbol + ?: return + if (symbol !in localProperties) return + unprocessedProperties.remove(symbol) + + val currentCharacteristic = propertiesCharacteristics.getOrDefault(symbol, EventOccurrencesRange.ZERO) + propertiesCharacteristics[symbol] = currentCharacteristic.or(data.getValue(node)[symbol] ?: EventOccurrencesRange.ZERO) + } + + override fun visitVariableDeclarationNode(node: VariableDeclarationNode) { + val symbol = node.fir.symbol + if (node.fir.initializer == null && node.fir.delegate == null) { + unprocessedProperties.add(symbol) + } else { + propertiesCharacteristics[symbol] = EventOccurrencesRange.AT_MOST_ONCE + } + } + } + + private val FirPropertySymbol.getValOrVarSource + get() = (fir.psi as? KtProperty)?.valOrVarKeyword?.toFirPsiSourceElement() + ?: fir.psi?.firstChild?.toFirPsiSourceElement() + ?: fir.source +} \ No newline at end of file diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/collectors/components/ControlFlowAnalysisDiagnosticComponent.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/collectors/components/ControlFlowAnalysisDiagnosticComponent.kt index f6050b31d26..77b99c327f2 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/collectors/components/ControlFlowAnalysisDiagnosticComponent.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/collectors/components/ControlFlowAnalysisDiagnosticComponent.kt @@ -13,7 +13,7 @@ import org.jetbrains.kotlin.fir.references.FirControlFlowGraphReference import org.jetbrains.kotlin.fir.resolve.dfa.controlFlowGraph class ControlFlowAnalysisDiagnosticComponent(collector: AbstractDiagnosticCollector) : AbstractDiagnosticCollectorComponent(collector) { - private val controlFlowAnalyzer = FirControlFlowAnalyzer() + private val controlFlowAnalyzer = FirControlFlowAnalyzer(session) // ------------------------------- Class initializer ------------------------------- diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt index ada89c59c97..37e5b6461a3 100644 --- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt +++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/diagnostics/FirErrors.kt @@ -87,7 +87,6 @@ object FirErrors { val REDUNDANT_MODALITY_MODIFIER by warning0() val REDUNDANT_RETURN_UNIT_TYPE by warning0() val REDUNDANT_EXPLICIT_TYPE by warning0() + val CAN_BE_VAL by warning0() val CAN_BE_REPLACED_WITH_OPERATOR_ASSIGNMENT by warning0() } - - diff --git a/compiler/resolution/src/org/jetbrains/kotlin/contracts/description/EventOccurrencesRange.kt b/compiler/resolution/src/org/jetbrains/kotlin/contracts/description/EventOccurrencesRange.kt index f8a010a9fb3..c13eaffc863 100644 --- a/compiler/resolution/src/org/jetbrains/kotlin/contracts/description/EventOccurrencesRange.kt +++ b/compiler/resolution/src/org/jetbrains/kotlin/contracts/description/EventOccurrencesRange.kt @@ -12,16 +12,22 @@ enum class EventOccurrencesRange(private val left: Int, private val right: Int) ZERO(0, 0), // 0..0 AT_MOST_ONCE(0, 1), // 0..1 EXACTLY_ONCE(1, 1), // 1..1 - AT_LEAST_ONCE(1, 2), // 1..* - UNKNOWN(0, 2); // 0..* + AT_LEAST_ONCE(1, 3), // 1..* + MORE_THAN_ONCE(2, 3), // 2..* + UNKNOWN(0, 3); // 0..* companion object { - private fun fromRange(left: Int, right: Int): EventOccurrencesRange = when (min(left, 1) to min(right, 2)) { + private fun fromRange(left: Int, right: Int): EventOccurrencesRange = when (min(left, 2) to min(right, 3)) { 0 to 0 -> ZERO 0 to 1 -> AT_MOST_ONCE + 0 to 2 -> UNKNOWN + 0 to 3 -> UNKNOWN 1 to 1 -> EXACTLY_ONCE 1 to 2 -> AT_LEAST_ONCE - 0 to 2 -> UNKNOWN + 1 to 3 -> AT_LEAST_ONCE + 2 to 2 -> MORE_THAN_ONCE + 2 to 3 -> MORE_THAN_ONCE + 3 to 3 -> MORE_THAN_ONCE else -> throw IllegalArgumentException() } @@ -34,5 +40,5 @@ enum class EventOccurrencesRange(private val left: Int, private val right: Int) operator fun plus(other: EventOccurrencesRange): EventOccurrencesRange = Companion.plus(this, other) } -fun EventOccurrencesRange.isDefinitelyVisited(): Boolean = this == EventOccurrencesRange.EXACTLY_ONCE || this == EventOccurrencesRange.AT_LEAST_ONCE -fun EventOccurrencesRange.canBeRevisited(): Boolean = this == EventOccurrencesRange.UNKNOWN || this == EventOccurrencesRange.AT_LEAST_ONCE +fun EventOccurrencesRange.isDefinitelyVisited(): Boolean = this == EventOccurrencesRange.EXACTLY_ONCE || this == EventOccurrencesRange.AT_LEAST_ONCE || this == EventOccurrencesRange.MORE_THAN_ONCE +fun EventOccurrencesRange.canBeRevisited(): Boolean = this == EventOccurrencesRange.UNKNOWN || this == EventOccurrencesRange.AT_LEAST_ONCE || this == EventOccurrencesRange.MORE_THAN_ONCE