[jvm-inspections] IDEA-310343 Patterns as arguments for logging

GitOrigin-RevId: 95c45d47447b70188c9d2b869923ff12e2b5898d
This commit is contained in:
Mikhail Pyltsin
2023-01-25 16:16:28 +01:00
committed by intellij-monorepo-bot
parent 5f2ac62609
commit 009300fc21
15 changed files with 546 additions and 66 deletions

View File

@@ -96,7 +96,7 @@
<inspectionElementsMerger implementation="com.intellij.codeInspection.test.junit.JUnitMalformedDeclarationInspectionMerger"/>
<!--logging-->
<localInspection language="UAST" enabledByDefault="false" level="WARNING" shortName="LoggingStringTemplateAsArgument"
<localInspection language="UAST" enabledByDefault="true" level="WARNING" shortName="LoggingStringTemplateAsArgument"
groupBundle="messages.JvmAnalysisBundle" bundle="messages.JvmAnalysisBundle"
groupPathKey="jvm.inspections.group.name" groupKey="jvm.inspections.logging.frameworks.group.name"
key="jvm.inspection.logging.string.template.as.argument.display.name"

View File

@@ -1,6 +1,7 @@
<html>
<body>
Reports string templates that are used as arguments to <b>SLF4J</b> and <b>Log4j 2</b> logging methods.
Methods <code>org.apache.logging.log4j.Logger.log()</code> is supported only for <b>all log levels</b> option.
String templates are evaluated at runtime even when the logging message is not logged; this can negatively impact performance.
It is recommended to use a parameterized log message instead, which will not be evaluated when logging is disabled.
<p><b>Example (for Kotlin):</b></p>
@@ -18,6 +19,19 @@ It is recommended to use a parameterized log message instead, which will not be
when string templates contain method calls or assignment expressions.
</p>
<!-- tooltip end -->
<ul>
<li>
Use the <b>Warn on</b> list to ignore certain higher logging levels. Higher logging levels may be always enabled, and the arguments will always be evaluated.
</li>
<li>
Use the <b>Do not warn when only expressions with primitive types, their wrappers or String are included</b> options to ignore
string templates, which contain only expressions with primitive types, their wrappers or String.
For example, it could be useful to prevent loading lazy collections.
Note that, creating string even only with expressions with primitive types,
their wrappers or String at runtime can negatively impact performance.
</li>
</ul>
<p><small>New in 2023.1</small></p>
</body>
</html>

View File

@@ -195,6 +195,13 @@ jvm.inspections.migrate.assert.to.matcher.description=Assert expression <code>#r
jvm.inspection.logging.string.template.as.argument.display.name=String template as argument to logging call
jvm.inspection.logging.string.template.as.argument.problem.descriptor=String template as argument to <code>#ref()</code> logging call #loc
jvm.inspection.logging.string.template.as.argument.quickfix.name=Replace with placeholders
jvm.inspection.logging.string.template.as.argument.skip.on.primitives=Do not warn when only expressions with primitive types, their wrappers or String are included
jvm.inspection.logging.string.template.as.argument.warn.on.label=Warn on:
jvm.inspection.logging.string.template.as.argument.all.levels.option=all log levels
jvm.inspection.logging.string.template.as.argument.warn.level.and.lower.option=warn level and lower
jvm.inspection.logging.string.template.as.argument.info.level.and.lower.option=info level and lower
jvm.inspection.logging.string.template.as.argument.debug.level.and.lower.option=debug level and lower
jvm.inspection.logging.string.template.as.argument.trace.level.option=trace level
jvm.inspection.test.failed.line.display.name=Failed line in test

View File

@@ -3,10 +3,13 @@ package com.intellij.codeInspection.logging
import com.intellij.analysis.JvmAnalysisBundle
import com.intellij.codeInspection.*
import com.intellij.codeInspection.options.OptPane
import com.intellij.lang.Language
import com.intellij.openapi.project.Project
import com.intellij.psi.CommonClassNames
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiType
import com.intellij.psi.util.TypeConversionUtil
import com.intellij.uast.UastHintedVisitorAdapter
import org.jetbrains.uast.*
import org.jetbrains.uast.expressions.UInjectionHost
@@ -15,6 +18,33 @@ import org.jetbrains.uast.generate.replace
import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor
class LoggingStringTemplateAsArgumentInspection : AbstractBaseUastLocalInspectionTool() {
@JvmField
var myLimitLevelType: LimitLevelType = LimitLevelType.ALL
@JvmField
var mySkipPrimitives: Boolean = true
override fun getOptionsPane(): OptPane {
return OptPane.pane(
OptPane.dropdown(
"myLimitLevelType",
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.warn.on.label"),
OptPane.option(LimitLevelType.ALL,
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.all.levels.option")),
OptPane.option(LimitLevelType.WARN_AND_LOWER,
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.warn.level.and.lower.option")),
OptPane.option(LimitLevelType.INFO_AND_LOWER,
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.info.level.and.lower.option")),
OptPane.option(LimitLevelType.DEBUG_AND_LOWER,
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.debug.level.and.lower.option")),
OptPane.option(LimitLevelType.TRACE,
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.trace.level.option")),
),
OptPane.checkbox("mySkipPrimitives",
JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.skip.on.primitives"))
)
}
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor =
UastHintedVisitorAdapter.create(
holder.file.language,
@@ -22,52 +52,103 @@ class LoggingStringTemplateAsArgumentInspection : AbstractBaseUastLocalInspectio
arrayOf(UCallExpression::class.java),
directOnly = true
)
}
private class LoggingStringTemplateAsArgumentVisitor(
private val holder: ProblemsHolder,
) : AbstractUastNonRecursiveVisitor() {
inner class LoggingStringTemplateAsArgumentVisitor(
private val holder: ProblemsHolder,
) : AbstractUastNonRecursiveVisitor() {
override fun visitCallExpression(node: UCallExpression): Boolean {
if (!LOG_MATCHERS.uCallMatches(node)) return true
val valueArguments = node.valueArguments
if (valueArguments.isEmpty()) return true
var stringExpression = valueArguments[0]
var indexStringExpression = 0
if (!canBeText(stringExpression.getExpressionType())) {
if (valueArguments.size < 2) {
override fun visitCallExpression(node: UCallExpression): Boolean {
if (!LOG_MATCHERS.uCallMatches(node)) return true
if (skipAccordingLevel(node)) return true
val valueArguments = node.valueArguments
val uMethod = node.resolve().toUElement() as? UMethod ?: return true
val uastParameters = uMethod.uastParameters
if (valueArguments.isEmpty() || uastParameters.isEmpty()) return true
var indexStringExpression = 0
if (!uastParameters[indexStringExpression].type.canBeText()) {
if (valueArguments.size < 2 || uastParameters.size < 2) {
return true
}
indexStringExpression = 1
if (!uastParameters[indexStringExpression].type.canBeText()) {
return true
}
}
val stringExpression = valueArguments[indexStringExpression]
val parts: MutableList<UExpression> = mutableListOf()
//if parameter is String and argument is NOT String, probably it is String Template like "$object" for Kotlin
if (stringExpression !is UPolyadicExpression && !stringExpression.getExpressionType().canBeText() &&
stringExpression.lang == Language.findLanguageByID("kotlin")) {
val text: String = stringExpression.sourcePsi?.parent?.text ?: return true
if (text.startsWith("$")) {
parts.add(stringExpression)
}
}
if (stringExpression is UPolyadicExpression && isPattern(stringExpression)) {
parts.addAll(stringExpression.operands)
}
if (parts.isEmpty()) {
return true
}
stringExpression = valueArguments[1]
indexStringExpression = 1
if (!canBeText(stringExpression.getExpressionType())) {
if (mySkipPrimitives && allExpressionsInPatternArePrimitivesOrWrappers(parts)) {
return true
}
}
if (!isPattern(stringExpression)) {
if (isGuarded(node)) {
return true
}
val message = JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.problem.descriptor")
holder.registerUProblem(node, message, ConvertToPlaceHolderQuickfix(indexStringExpression))
return true
}
private fun allExpressionsInPatternArePrimitivesOrWrappers(operands: List<UExpression>): Boolean {
return operands.all { it.getExpressionType().isPrimitiveOrWrappers() }
}
val message = JvmAnalysisBundle.message("jvm.inspection.logging.string.template.as.argument.problem.descriptor")
holder.registerUProblem(node, message, ConvertToPlaceHolderQuickfix(indexStringExpression))
return true
}
private fun skipAccordingLevel(node: UCallExpression): Boolean {
if (myLimitLevelType != LimitLevelType.ALL) {
val loggerLevel = getLoggerLevel(node)
if (loggerLevel == null) return true
val notSkip: Boolean = when (loggerLevel) {
LevelType.FATAL -> false
LevelType.ERROR -> false
LevelType.WARNING -> myLimitLevelType.ordinal == LimitLevelType.WARN_AND_LOWER.ordinal
LevelType.INFO -> myLimitLevelType.ordinal <= LimitLevelType.INFO_AND_LOWER.ordinal
LevelType.DEBUG -> myLimitLevelType.ordinal <= LimitLevelType.DEBUG_AND_LOWER.ordinal
LevelType.TRACE -> myLimitLevelType.ordinal <= LimitLevelType.TRACE.ordinal
}
return !notSkip
}
else {
return false
}
}
private fun isPattern(stringExpression: UExpression): Boolean {
//Perhaps, it needs to be customized for Java Pattern in the future
return stringExpression is UPolyadicExpression && stringExpression is UInjectionHost &&
!stringExpression.operands.all { it is ULiteralExpression }
private fun isPattern(stringExpression: UPolyadicExpression): Boolean {
//it needs to be customized for Java
return stringExpression is UInjectionHost &&
stringExpression.lang == Language.findLanguageByID("kotlin") &&
!stringExpression.operands.all { it is ULiteralExpression }
}
}
}
private fun canBeText(expressionType: PsiType?): Boolean {
if (
expressionType?.equalsToText(CommonClassNames.JAVA_LANG_STRING) == true ||
expressionType?.equalsToText(CommonClassNames.JAVA_LANG_CHAR_SEQUENCE) == true
) return true
return false
}
private fun PsiType?.canBeText(): Boolean {
return this?.equalsToText(CommonClassNames.JAVA_LANG_STRING) == true ||
this?.equalsToText(CommonClassNames.JAVA_LANG_CHAR_SEQUENCE) == true
}
private fun PsiType?.isPrimitiveOrWrappers(): Boolean {
return this != null && (TypeConversionUtil.isPrimitiveAndNotNull(this) ||
TypeConversionUtil.isPrimitiveWrapper(this) ||
canBeText())
}
class ConvertToPlaceHolderQuickfix(private val indexStringExpression: Int) : LocalQuickFix {
@@ -82,34 +163,40 @@ class ConvertToPlaceHolderQuickfix(private val indexStringExpression: Int) : Loc
parametersBeforeString.add(valueArguments[0])
}
val builderString = StringBuilder()
val template = valueArguments[indexStringExpression] as? UPolyadicExpression ?: return
val stringTemplate = valueArguments[indexStringExpression]
var indexOuterPlaceholder = indexStringExpression + 1
val loggerType = getLoggerType(uCallExpression)
if (stringTemplate is UPolyadicExpression) {
val loggerType = getLoggerType(uCallExpression)
for (operand in template.operands) {
if (operand is ULiteralExpression && operand.isString) {
val text = operand.value.toString()
val countPlaceHolders = countPlaceHolders(text, loggerType)
for (index in 0 until countPlaceHolders) {
val nextIndex = indexOuterPlaceholder + index
if (nextIndex < valueArguments.size) {
parametersAfterString.add(valueArguments[nextIndex])
for (operand in stringTemplate.operands) {
if (operand is ULiteralExpression && operand.isString) {
val text = operand.value.toString()
val countPlaceHolders = countPlaceHolders(text, loggerType)
for (index in 0 until countPlaceHolders) {
val nextIndex = indexOuterPlaceholder + index
if (nextIndex < valueArguments.size) {
parametersAfterString.add(valueArguments[nextIndex])
}
else {
break
}
}
else {
break
indexOuterPlaceholder += countPlaceHolders
builderString.append(text)
}
else {
if (builderString.endsWith("\\") && (loggerType == LoggerType.SLF4J_LOGGER_TYPE || loggerType == LoggerType.SLF4J_BUILDER_TYPE)) {
builderString.append("\\")
}
builderString.append("{}")
parametersAfterString.add(operand)
}
indexOuterPlaceholder += countPlaceHolders
builderString.append(text)
}
else {
if (builderString.endsWith("\\") && (loggerType == LoggerType.SLF4J_LOGGER_TYPE || loggerType == LoggerType.SLF4J_BUILDER_TYPE)) {
builderString.append("\\")
}
builderString.append("{}")
parametersAfterString.add(operand)
}
}
else {
builderString.append("{}")
parametersAfterString.add(stringTemplate)
}
if (indexOuterPlaceholder < valueArguments.size) {
for (index in indexOuterPlaceholder until valueArguments.size) {
@@ -118,10 +205,11 @@ class ConvertToPlaceHolderQuickfix(private val indexStringExpression: Int) : Loc
}
val elementFactory = uCallExpression.getUastElementFactory(project) ?: return
val newText = elementFactory.createStringLiteralExpression(builderString.toString(), uCallExpression.sourcePsi) ?: return
val newParameters = mutableListOf<UExpression>()
newParameters.addAll(parametersBeforeString)
newParameters.add(newText)
newParameters.addAll(parametersAfterString)
val newParameters = mutableListOf<UExpression>().apply {
addAll(parametersBeforeString)
add(newText)
addAll(parametersAfterString)
}
val methodName = uCallExpression.methodName ?: return
val newCall = elementFactory.createCallExpression(uCallExpression.receiver, methodName, newParameters, uCallExpression.returnType,
@@ -130,4 +218,8 @@ class ConvertToPlaceHolderQuickfix(private val indexStringExpression: Int) : Loc
val oldCall = uCallExpression.getQualifiedParentOrThis()
oldCall.replace(newCall)
}
}
enum class LimitLevelType {
ALL, WARN_AND_LOWER, INFO_AND_LOWER, DEBUG_AND_LOWER, TRACE
}

View File

@@ -2,7 +2,7 @@
package com.intellij.codeInspection.logging
import com.siyeh.ig.callMatcher.CallMatcher
import org.jetbrains.uast.UCallExpression
import org.jetbrains.uast.*
const val SLF4J_LOGGER = "org.slf4j.Logger"
@@ -42,6 +42,111 @@ fun getLoggerType(uCall: UCallExpression?): LoggerType? {
}
}
fun isGuarded(call: UCallExpression): Boolean {
val variable: UVariable = getLoggerQualifier(call) ?: return false
val loggerLevel = getLoggerLevel(call) ?: return false
val ifExpression: UIfExpression? = call.getParentOfType<UIfExpression>()
val condition = ifExpression?.condition ?: return false
return isGuardedIn(condition, variable, loggerLevel)
}
fun isGuardedIn(condition: UExpression, variable: UVariable, loggerLevel: LevelType): Boolean {
val loggerLevelFromCondition: LevelType = getLevelFromCondition(condition, variable) ?: return false
return loggerLevelFromCondition == loggerLevel
}
fun getLevelFromCondition(condition: UExpression, variable: UVariable): LevelType? {
if (condition is UCallExpression) {
if ((condition.receiver as? UResolvable)?.resolveToUElement()?.sourcePsi != variable.sourcePsi) {
return null
}
val methodName = condition.methodName ?: return null
return levelTypeFromGuard(methodName)
}
if (condition is UQualifiedReferenceExpression) {
if ((condition.receiver as? UResolvable)?.resolveToUElement()?.sourcePsi != variable.sourcePsi) {
return null
}
val methodName = condition.resolvedName ?: return null
return levelTypeFromGuard(methodName)
}
if (condition is UPolyadicExpression) {
for (operand in condition.operands) {
val levelFromCondition = getLevelFromCondition(operand, variable)
if (levelFromCondition != null) return levelFromCondition
}
}
return null
}
private fun levelTypeFromGuard(methodName: String): LevelType? {
if (!methodName.startsWith("is") || !methodName.endsWith("Enabled")) {
return null
}
for (level in LevelType.values()) {
if (methodName.substring(2, methodName.length - 7).equals(level.name, ignoreCase = true)) {
return level
}
}
return null
}
fun getLoggerQualifier(call: UCallExpression?): UVariable? {
if (call == null) return null
var receiver: UExpression? = call.receiver
if (receiver is UCallExpression) {
receiver = receiver.receiver
}
if (receiver is UQualifiedReferenceExpression) {
receiver = receiver.receiver
}
if (receiver is USimpleNameReferenceExpression) {
val resolved = receiver.resolveToUElement() as? UVariable ?: return null
if (resolved.type.equalsToText(SLF4J_LOGGER) ||
resolved.type.equalsToText(LOG4J_LOGGER)) {
return resolved
}
if (resolved.type.equalsToText(SLF4J_EVENT_BUILDER) ||
resolved.type.equalsToText(LOG4J_LOG_BUILDER)) {
val uastInitializer = (resolved.uastInitializer as? UQualifiedReferenceExpression) ?: return null
return getLoggerQualifier(uastInitializer.selector as? UCallExpression)
}
}
return null
}
fun getLoggerLevel(uCall: UCallExpression?): LevelType? {
if (uCall == null) {
return null
}
var levelName = uCall.methodName
if ("log" == levelName) {
//also, it could be LOG4J_LOGGER, for example, log(level, pattern),
//but let's skip it, because it is usually used for dynamic choice
var receiver: UElement = uCall.receiver ?: return null
if (receiver is UQualifiedReferenceExpression) {
receiver = receiver.selector
}
else if (receiver is USimpleNameReferenceExpression) {
val variable = receiver.resolveToUElement() as? UVariable ?: return null
receiver = (variable.uastInitializer as? UQualifiedReferenceExpression)?.selector ?: return null
}
levelName = (receiver as? UCallExpression)?.methodName
}
if (levelName == null) {
return null
}
for (value in LevelType.values()) {
if (value.name.equals(levelName, ignoreCase = true) ||
"at${value.name}".equals(levelName, ignoreCase = true)
) {
return value
}
}
return null
}
fun countPlaceHolders(text: String, loggerType: LoggerType?): Int {
var count = 0
var placeHolder = false
@@ -73,4 +178,8 @@ fun countPlaceHolders(text: String, loggerType: LoggerType?): Int {
enum class LoggerType {
SLF4J_LOGGER_TYPE, SLF4J_BUILDER_TYPE, LOG4J_LOGGER_TYPE, LOG4J_BUILDER_TYPE
}
enum class LevelType {
FATAL, ERROR, WARNING, INFO, DEBUG, TRACE
}

View File

@@ -6,23 +6,26 @@ class StringTemplateAsArgument {
private val loggerLog4J = LogManager.getLogger()
fun testLoggerSlf4JBuilder() {
val variable1 = "test"
val variable1 = 1
loggerSlf4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>("variable1: ${variable1}")
}
fun testLoggerLog4J() {
val variable1 = "test"
val variable1 = 1
loggerLog4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: ${variable1}")
}
fun testLoggerLog4JBuilder() {
val variable1 = "test"
val variable1 = 1
loggerLog4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>( "variable1: ${variable1}")
}
fun testLoggerSlf4J() {
val variable1 = "test"
val variable1 = 1
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("${variable1}")
loggerSlf4J.info("${getString()}")
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("${getInt()}")
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: ${variable1}")
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: $variable1")
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: $variable1", RuntimeException())
@@ -32,4 +35,7 @@ class StringTemplateAsArgument {
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("{} variable1: $variable1 {} {} {}", 1, 2, RuntimeException())
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("{} variable1: $variable1 {}", 1, 2)
}
fun getString() = "test"
fun getInt() = 1
}

View File

@@ -1,18 +1,29 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
import java.lang.RuntimeException
class StringTemplateAsArgumentFix {
private val loggerSlf4J = LoggerFactory.getLogger()
fun testLoggerSlf4J() {
val variable1 = "test"
val variable2 = 1
loggerSlf4J.info("${variable1}")
loggerSlf4J.info("{}", variable2)
loggerSlf4J.info("{}", getMethod())
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.info("variable1: {}", variable1, RuntimeException())
loggerSlf4J.info("{} variable1: {}", 1, variable1)
loggerSlf4J.info("{} variable1: {} {} variable1: {}", 1, variable1, 2, variable1)
loggerSlf4J.info("{} variable1: {} {} variable1: {} {}", 1, variable1, 2, variable1, 3)
loggerSlf4J.info("{} variable1: {} {} variable1: {} {}", 1, variable1, 2, variable1, 3, RuntimeException())
loggerSlf4J.info("{} variable1: {}", 1, variable1, RuntimeException())
loggerSlf4J.info("{} variable1: {} {}", 1, variable1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: {} {} {}", 1, variable1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: {} {} {} {}", 1, variable1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: {} {}", 1, variable1, 2)
}
fun getMethod() = 1
}

View File

@@ -1,18 +1,29 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
import java.lang.RuntimeException
class StringTemplateAsArgumentFix {
private val loggerSlf4J = LoggerFactory.getLogger()
fun testLoggerSlf4J() {
val variable1 = "test"
val variable2 = 1
loggerSlf4J.info("${variable1}")
loggerSlf4J.info("${variable2}")
loggerSlf4J.info("${getMethod()}")
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.in<caret>fo("variable1: ${variable1}")
loggerSlf4J.info("variable1: $variable1")
loggerSlf4J.info("variable1: $variable1", RuntimeException())
loggerSlf4J.info("{} variable1: $variable1", 1)
loggerSlf4J.info("{} variable1: $variable1 {} variable1: $variable1", 1, 2)
loggerSlf4J.info("{} variable1: $variable1 {} variable1: $variable1 {}", 1, 2, 3)
loggerSlf4J.info("{} variable1: $variable1 {} variable1: $variable1 {}", 1, 2, 3, RuntimeException())
loggerSlf4J.info("{} variable1: $variable1", 1, RuntimeException())
loggerSlf4J.info("{} variable1: $variable1 {}", 1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: $variable1 {} {}", 1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: $variable1 {} {} {}", 1, 2, RuntimeException())
loggerSlf4J.info("{} variable1: $variable1 {}", 1, 2)
}
fun getMethod() = 1
}

View File

@@ -0,0 +1,70 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
data class Data(val i: Int)
class StringTemplateAsArgumentGuarded {
private val loggerSlf4J = LoggerFactory.getLogger()
private val loggerLog4J = LogManager.getLogger()
fun guardedLog4J() {
val data = Data(1)
if (loggerLog4J.isInfoEnabled) {
loggerLog4J.info("$data" )
}
if (loggerLog4J.isInfoEnabled()) {
loggerLog4J.info("$data" )
}
loggerLog4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("$data")
}
fun guardedLog4JBuilder() {
val data = Data(1)
val atInfo = loggerLog4J.atInfo()
if (loggerLog4J.isInfoEnabled) {
loggerLog4J.atInfo().log("$data" )
}
if (loggerLog4J.isInfoEnabled) {
atInfo.log("$data" )
}
if (loggerLog4J.isDebugEnabled) {
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
if (loggerLog4J.isDebugEnabled()) {
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
loggerLog4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
fun guardedSlf4j() {
val data = Data(1)
if (loggerSlf4J.isInfoEnabled) {
loggerSlf4J.info("$data" )
}
if (loggerSlf4J.isInfoEnabled()) {
loggerSlf4J.info("$data" )
}
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("$data")
}
fun guardedSlf4jBuilder() {
val data = Data(1)
val atInfo = loggerSlf4J.atInfo()
if (loggerSlf4J.isInfoEnabled) {
loggerSlf4J.atInfo().log("$data" )
}
if (loggerSlf4J.isInfoEnabled) {
atInfo.log("$data" )
}
if (loggerSlf4J.isDebugEnabled) {
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
if (loggerSlf4J.isDebugEnabled()) {
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
loggerSlf4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
atInfo.<warning descr="String template as argument to 'log()' logging call">log</warning>("$data" )
}
}

View File

@@ -0,0 +1,38 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
data class Data(val i: Int)
class StringTemplateAsArgumentSkipPrimitives {
private val loggerSlf4J = LoggerFactory.getLogger()
private val loggerLog4J = LogManager.getLogger()
fun testLoggerSlf4JBuilder() {
val variable1 = 1
val data = Data(1)
loggerSlf4J.atInfo().log("variable1: ${variable1}")
loggerSlf4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>("data: ${data}")
}
fun testLoggerLog4J() {
val variable1 = 1
val data = Data(1)
loggerLog4J.info("variable1: ${variable1}")
loggerLog4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("data: ${data}")
}
fun testLoggerLog4JBuilder() {
val variable1 = 1
val data = Data(1)
loggerLog4J.atInfo().log( "variable1: ${variable1}")
loggerLog4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>( "data: ${data}")
}
fun testLoggerSlf4J() {
val variable1 = 1
val data = Data(1)
loggerSlf4J.info("variable1: {}", variable1)
loggerSlf4J.info("${variable1}")
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("${data}")
}
}

View File

@@ -0,0 +1,38 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
class StringTemplateAsArgumentWarnDebug {
private val loggerSlf4J = LoggerFactory.getLogger()
private val loggerLog4J = LogManager.getLogger()
fun testLoggerSlf4JBuilder() {
val variable1 = 1
loggerSlf4J.atInfo().log("variable1: ${variable1}")
loggerSlf4J.atDebug().<warning descr="String template as argument to 'log()' logging call">log</warning>("variable1: ${variable1}")
loggerSlf4J.atWarn().log("variable1: ${variable1}")
}
fun testLoggerLog4J() {
val variable1 = 1
loggerLog4J.info("variable1: ${variable1}")
loggerLog4J.<warning descr="String template as argument to 'debug()' logging call">debug</warning>("variable1: ${variable1}")
loggerLog4J.warn("variable1: ${variable1}")
}
fun testLoggerLog4JBuilder() {
val variable1 = 1
loggerLog4J.atInfo().log( "variable1: ${variable1}")
loggerLog4J.atDebug().<warning descr="String template as argument to 'log()' logging call">log</warning>( "variable1: ${variable1}")
loggerLog4J.atWarn().log( "variable1: ${variable1}")
}
fun testLoggerSlf4J() {
val variable1 = 1
loggerSlf4J.info("variable1: ${variable1}")
loggerSlf4J.<warning descr="String template as argument to 'debug()' logging call">debug</warning>("variable1: ${variable1}")
loggerSlf4J.warn("variable1: ${variable1}")
}
fun getString() = "test"
fun getInt() = 1
}

View File

@@ -0,0 +1,38 @@
import org.apache.logging.log4j.LogManager
import org.slf4j.LoggerFactory
class StringTemplateAsArgumentWarnInfo {
private val loggerSlf4J = LoggerFactory.getLogger()
private val loggerLog4J = LogManager.getLogger()
fun testLoggerSlf4JBuilder() {
val variable1 = 1
loggerSlf4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>("variable1: ${variable1}")
loggerSlf4J.atDebug().<warning descr="String template as argument to 'log()' logging call">log</warning>("variable1: ${variable1}")
loggerSlf4J.atWarn().log("variable1: ${variable1}")
}
fun testLoggerLog4J() {
val variable1 = 1
loggerLog4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: ${variable1}")
loggerLog4J.<warning descr="String template as argument to 'debug()' logging call">debug</warning>("variable1: ${variable1}")
loggerLog4J.warn("variable1: ${variable1}")
}
fun testLoggerLog4JBuilder() {
val variable1 = 1
loggerLog4J.atInfo().<warning descr="String template as argument to 'log()' logging call">log</warning>( "variable1: ${variable1}")
loggerLog4J.atDebug().<warning descr="String template as argument to 'log()' logging call">log</warning>( "variable1: ${variable1}")
loggerLog4J.atWarn().log( "variable1: ${variable1}")
}
fun testLoggerSlf4J() {
val variable1 = 1
loggerSlf4J.<warning descr="String template as argument to 'info()' logging call">info</warning>("variable1: ${variable1}")
loggerSlf4J.<warning descr="String template as argument to 'debug()' logging call">debug</warning>("variable1: ${variable1}")
loggerSlf4J.warn("variable1: ${variable1}")
}
fun getString() = "test"
fun getInt() = 1
}

View File

@@ -10,11 +10,27 @@ private const val INSPECTION_PATH = "/codeInspection/logging/stringTemplateAsArg
class KotlinLoggingStringTemplateAsArgumentInspectionTest : LoggingStringTemplateAsArgumentInspectionTestBase() {
override fun getBasePath() = KotlinJvmAnalysisTestUtil.TEST_DATA_PROJECT_RELATIVE_BASE_PATH + INSPECTION_PATH
fun `test highlighting`() {
fun `test highlighting, myLimitLevelType=2 mySkipPrimitives=false`() {
myFixture.testHighlighting("StringTemplateAsArgumentWarnInfo.kt")
}
fun `test highlighting, myLimitLevelType=3 mySkipPrimitives=false`() {
myFixture.testHighlighting("StringTemplateAsArgumentWarnDebug.kt")
}
fun `test highlighting, myLimitLevelType=0 mySkipPrimitives=true`() {
myFixture.testHighlighting("StringTemplateAsArgumentSkipPrimitives.kt")
}
fun `test highlighting, myLimitLevelType=0 mySkipPrimitives=false`() {
myFixture.testHighlighting("StringTemplateAsArgument.kt")
}
fun `test fix`() {
fun `test fix, myLimitLevelType=0 mySkipPrimitives=false`() {
myFixture.testQuickFix(file = "StringTemplateAsArgumentFix.kt", checkPreview = true)
}
fun `test highlighting, with guards`() {
myFixture.testHighlighting("StringTemplateAsArgumentGuarded.kt")
}
}

View File

@@ -21,7 +21,13 @@ abstract class LoggingInspectionTestBase : UastInspectionTestBase() {
}
public interface Logger {
void info(String format, Object... arguments);
void debug(String format, Object... arguments);
void warn(String format, Object... arguments);
boolean isDebugEnabled();
boolean isInfoEnabled();
LoggingEventBuilder atInfo();
LoggingEventBuilder atDebug();
LoggingEventBuilder atWarn();
LoggingEventBuilder atError();
}
""".trimIndent())
@@ -29,12 +35,20 @@ abstract class LoggingInspectionTestBase : UastInspectionTestBase() {
package org.apache.logging.log4j;
import org.apache.logging.log4j.util.Supplier;
public interface Logger {
boolean isDebugEnabled();
boolean isInfoEnabled();
void info(String message, Object... params);
void debug(String message, Object... params);
void warn(String message, Object... params);
void info(String message);
void debug(String message);
void warn(String message);
void fatal(String message, Object... params);
void error(Supplier<?> var1, Throwable var2);
void info(String message, Supplier<?>... params);
LogBuilder atInfo();
LogBuilder atDebug();
LogBuilder atWarn();
LogBuilder atFatal();
LogBuilder atError();
}

View File

@@ -1,9 +1,25 @@
package com.intellij.codeInspection.tests.logging
import com.intellij.codeInspection.InspectionProfileEntry
import com.intellij.codeInspection.logging.LimitLevelType
import com.intellij.codeInspection.logging.LoggingStringTemplateAsArgumentInspection
abstract class LoggingStringTemplateAsArgumentInspectionTestBase : LoggingInspectionTestBase() {
override val inspection: InspectionProfileEntry
get() = LoggingStringTemplateAsArgumentInspection()
get() {
val testName = getTestName(false)
val properties = testName.split(" ").filter {
it.contains("=")
}.map { it.split("=") }
val loggingStringTemplateAsArgumentInspection = LoggingStringTemplateAsArgumentInspection()
for (property in properties) {
if (property[0] == "myLimitLevelType") {
loggingStringTemplateAsArgumentInspection.myLimitLevelType = LimitLevelType.values()[property[1].toInt()]
}
if (property[0] == "mySkipPrimitives") {
loggingStringTemplateAsArgumentInspection.mySkipPrimitives = property[1] == "true"
}
}
return loggingStringTemplateAsArgumentInspection
}
}