Skip to content

Commit 70099a7

Browse files
Fix the race condition introduced with 3470888
### What's done: - Original issue: #1548. - Original PR: #1553. - This change reverts 3470888. - Enhances the KDoc of `DiktatRule`. - Fixes #1548.
1 parent 0cb47bb commit 70099a7

File tree

5 files changed

+87
-28
lines changed

5 files changed

+87
-28
lines changed

diktat-common/src/main/kotlin/org/cqfn/diktat/common/config/rules/RulesConfigReader.kt

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package org.cqfn.diktat.common.config.rules
66

77
import org.cqfn.diktat.common.config.reader.JsonResourceConfigReader
88
import org.cqfn.diktat.common.config.rules.RulesConfigReader.Companion.log
9+
import org.cqfn.diktat.common.utils.LazyMessage
910
import org.cqfn.diktat.common.utils.loggerWithKtlintConfig
1011

1112
import com.charleskorn.kaml.Yaml
@@ -16,6 +17,7 @@ import mu.KotlinLogging
1617
import java.io.BufferedReader
1718
import java.io.File
1819
import java.util.Locale
20+
import java.util.concurrent.atomic.AtomicBoolean
1921
import java.util.concurrent.atomic.AtomicInteger
2022

2123
import kotlinx.serialization.ExperimentalSerializationApi
@@ -99,19 +101,30 @@ open class RulesConfigReader(override val classLoader: ClassLoader) : JsonResour
99101
override fun getConfigFile(resourceFileName: String): BufferedReader? {
100102
val resourceFile = File(resourceFileName)
101103
return if (resourceFile.exists()) {
102-
log.debug("Using diktat-analysis.yml file from the following path: ${resourceFile.absolutePath}")
104+
log.debugOnce {
105+
"Using $DIKTAT_ANALYSIS_CONF file from the following path: ${resourceFile.absolutePath}"
106+
}
103107
File(resourceFileName).bufferedReader()
104108
} else {
105-
log.debug("Using the default diktat-analysis.yml file from the class path")
109+
log.debugOnce {
110+
"Using the default $DIKTAT_ANALYSIS_CONF file from the class path"
111+
}
106112
classLoader.getResourceAsStream(resourceFileName)?.bufferedReader()
107113
}
108114
}
109115

110116
companion object {
117+
internal val log: KLogger = KotlinLogging.loggerWithKtlintConfig(RulesConfigReader::class)
118+
private val debugMessageLogged = AtomicBoolean()
119+
111120
/**
112-
* A [Logger] that can be used
121+
* Don't use with more than a single [lazyMessage].
113122
*/
114-
val log: KLogger = KotlinLogging.loggerWithKtlintConfig(RulesConfigReader::class)
123+
private fun KLogger.debugOnce(lazyMessage: LazyMessage) {
124+
if (debugMessageLogged.compareAndSet(false, true)) {
125+
debug(lazyMessage)
126+
}
127+
}
115128
}
116129
}
117130

diktat-common/src/main/kotlin/org/cqfn/diktat/common/utils/LoggingUtils.kt

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import com.pinterest.ktlint.core.initKtLintKLogger
88
import mu.KotlinLogging
99
import kotlin.reflect.KClass
1010

11+
/**
12+
* Lazy string message.
13+
*/
14+
typealias LazyMessage = () -> String
15+
1116
/**
1217
* Create a logger using [KotlinLogging] and configure it by ktlint's mechanism
1318
*

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRule.kt

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import org.jetbrains.kotlin.com.intellij.lang.ASTNode
1414
private typealias DiktatConfigRule = org.cqfn.diktat.common.config.rules.Rule
1515

1616
/**
17-
* This is a wrapper around Ktlint Rule
17+
* This is a wrapper around _KtLint_ `Rule`.
1818
*
1919
* @param id id of the rule
2020
* @property configRules all rules from configuration
@@ -33,7 +33,17 @@ abstract class DiktatRule(
3333
var isFixMode: Boolean = false
3434

3535
/**
36-
* Will be initialized in visit
36+
* The **file-specific** error emitter, initialized in
37+
* [beforeVisitChildNodes] and used in [logic] implementations.
38+
*
39+
* Since the file is indirectly a part of the state of a `Rule`, the same
40+
* `Rule` instance should **never be re-used** to check more than a single
41+
* file, or confusing effects (incl. race conditions) will occur.
42+
* See the documentation of the [Rule] class for more details.
43+
*
44+
* @see Rule
45+
* @see beforeVisitChildNodes
46+
* @see logic
3747
*/
3848
lateinit var emitWarn: EmitType
3949

diktat-rules/src/main/kotlin/org/cqfn/diktat/ruleset/rules/DiktatRuleSetProvider.kt

+51-21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.cqfn.diktat.common.config.rules.DIKTAT_CONF_PROPERTY
1010
import org.cqfn.diktat.common.config.rules.DIKTAT_RULE_SET_ID
1111
import org.cqfn.diktat.common.config.rules.RulesConfig
1212
import org.cqfn.diktat.common.config.rules.RulesConfigReader
13+
import org.cqfn.diktat.common.utils.LazyMessage
1314
import org.cqfn.diktat.common.utils.loggerWithKtlintConfig
1415
import org.cqfn.diktat.ruleset.constants.Warnings
1516
import org.cqfn.diktat.ruleset.rules.OrderedRuleSet.Companion.ordered
@@ -90,10 +91,12 @@ import org.cqfn.diktat.ruleset.rules.chapter6.classes.StatelessClassesRule
9091

9192
import com.pinterest.ktlint.core.RuleSet
9293
import com.pinterest.ktlint.core.RuleSetProvider
94+
import mu.KLogger
9395
import mu.KotlinLogging
9496
import org.jetbrains.kotlin.org.jline.utils.Levenshtein
9597

9698
import java.io.File
99+
import java.util.concurrent.atomic.AtomicBoolean
97100

98101
/**
99102
* [RuleSetProvider] that provides diKTat ruleset.
@@ -109,31 +112,32 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
109112
yield(resolveConfigFileFromSystemProperty())
110113
}
111114

112-
/**
113-
* As of _KtLint_ **0.47**, each rule is expected to have a state and is executed
114-
* twice per file, and a new `Rule` instance is created per each file checked.
115-
*
116-
* Diktat rules have no mutable state yet and use the deprecated _KtLint_
117-
* API, so we initialize them only _once_ for performance reasons and also
118-
* to avoid redundant logging.
119-
*/
120-
private val ruleSet: RuleSet by lazy {
121-
log.debug("Will run $DIKTAT_RULE_SET_ID with $diktatConfigFile" +
122-
" (it can be placed to the run directory or the default file from resources will be used)")
115+
@Suppress(
116+
"LongMethod",
117+
"TOO_LONG_FUNCTION",
118+
)
119+
@Deprecated(
120+
"Marked for removal in KtLint 0.48. See changelog or KDoc for more information.",
121+
)
122+
override fun get(): RuleSet {
123+
log.debugOnce {
124+
"Will run $DIKTAT_RULE_SET_ID with $diktatConfigFile" +
125+
" (it can be placed to the run directory or the default file from resources will be used)"
126+
}
123127
val configPath = possibleConfigs
124128
.firstOrNull { it != null && File(it).exists() }
125129
diktatConfigFile = configPath
126130
?: run {
127-
val possibleConfigsList = possibleConfigs.toList()
128-
log.warn(
131+
log.warnOnce {
132+
val possibleConfigsList = possibleConfigs.toList()
129133
"Configuration file not found in directory where diktat is run (${possibleConfigsList[0]}) " +
130134
"or in the directory where diktat.jar is stored (${possibleConfigsList[1]}) " +
131135
"or in system property <diktat.config.path> (${possibleConfigsList[2]}), " +
132136
"the default file included in jar will be used. " +
133137
"Some configuration options will be disabled or substituted with defaults. " +
134138
"Custom configuration file should be placed in diktat working directory if run from CLI " +
135139
"or provided as configuration options in plugins."
136-
)
140+
}
137141
diktatConfigFile
138142
}
139143

@@ -229,18 +233,12 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
229233
.map {
230234
it.invoke(configRules)
231235
}
232-
RuleSet(
236+
return RuleSet(
233237
DIKTAT_RULE_SET_ID,
234238
rules = rules.toTypedArray()
235239
).ordered()
236240
}
237241

238-
@Deprecated(
239-
"Marked for removal in KtLint 0.48. See changelog or KDoc for more information.",
240-
)
241-
override fun get(): RuleSet =
242-
ruleSet
243-
244242
private fun validate(config: RulesConfig) =
245243
require(config.name == DIKTAT_COMMON || config.name in Warnings.names) {
246244
val closestMatch = Warnings.names.minByOrNull { Levenshtein.distance(it, config.name) }
@@ -272,5 +270,37 @@ class DiktatRuleSetProvider(private var diktatConfigFile: String = DIKTAT_ANALYS
272270

273271
companion object {
274272
private val log = KotlinLogging.loggerWithKtlintConfig(DiktatRuleSetProvider::class)
273+
274+
/**
275+
* @see warnMessageLogged
276+
*/
277+
private val debugMessageLogged = AtomicBoolean()
278+
279+
/**
280+
* @see debugMessageLogged
281+
*/
282+
private val warnMessageLogged = AtomicBoolean()
283+
284+
/**
285+
* Don't use with more than a single [lazyMessage].
286+
*
287+
* @see KLogger.warnOnce
288+
*/
289+
private fun KLogger.debugOnce(lazyMessage: LazyMessage) {
290+
if (debugMessageLogged.compareAndSet(false, true)) {
291+
debug(lazyMessage)
292+
}
293+
}
294+
295+
/**
296+
* Don't use with more than a single [lazyMessage].
297+
*
298+
* @see KLogger.debugOnce
299+
*/
300+
private fun KLogger.warnOnce(lazyMessage: LazyMessage) {
301+
if (warnMessageLogged.compareAndSet(false, true)) {
302+
warn(lazyMessage)
303+
}
304+
}
275305
}
276306
}

diktat-rules/src/test/kotlin/org/cqfn/diktat/util/TestUtils.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
package org.cqfn.diktat.util
1010

11+
import org.cqfn.diktat.common.utils.LazyMessage
1112
import org.cqfn.diktat.ruleset.constants.EmitType
1213

1314
import com.pinterest.ktlint.core.KtLint
@@ -28,7 +29,7 @@ internal const val TEST_FILE_NAME = "TestFileName.kt"
2829
* @param lazyFailureMessage the message to evaluate in case of a failure.
2930
* @return a non-`null` value.
3031
*/
31-
internal fun <T> T?.assertNotNull(lazyFailureMessage: () -> String = { "Expecting actual not to be null" }): T =
32+
internal fun <T> T?.assertNotNull(lazyFailureMessage: LazyMessage = { "Expecting actual not to be null" }): T =
3233
this ?: fail(lazyFailureMessage())
3334

3435
/**

0 commit comments

Comments
 (0)