From 384af015b4966b6edc1195237da317ccfea9ee92 Mon Sep 17 00:00:00 2001 From: Mikhail Pyltsin Date: Wed, 3 Jan 2024 12:11:30 +0000 Subject: [PATCH] [java-console] IDEA-331307 Provide navigation for logs for jvm languages GitOrigin-RevId: c827937afc1da8cdfe1cc7c1297093df284ecdc8 --- .../resources/META-INF/JvmAnalysisPlugin.xml | 5 + .../messages/JvmAnalysisBundle.properties | 5 + .../console/ClassFinderFilter.kt | 139 +++++++++++ .../customization/console/ClassInfoCache.kt | 84 +++++++ .../ClassLoggingConsoleFilterProvider.kt | 37 +++ .../console/LogConsoleLogHandlerCollectors.kt | 50 ++++ .../console/LogFinderHyperlinkHandler.kt | 163 +++++++++++++ ...lderCountMatchesArgumentCountInspection.kt | 229 +----------------- .../logging/LoggingPlaceholderUtil.kt | 212 ++++++++++++++++ .../codeInspection/logging/LoggingUtil.kt | 5 + .../internal/LogFinderHyperlinkTestBase.kt | 55 +++++ .../logging/LoggingInspectionTestBase.kt | 133 +--------- .../testFramework/logging/LoggingTestUtils.kt | 157 ++++++++++++ .../console/JavaLogFinderHyperlinkTest.kt | 208 ++++++++++++++++ .../console/KotlinLogFinderHyperlinkTest.kt | 106 ++++++++ 15 files changed, 1247 insertions(+), 341 deletions(-) create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassLoggingConsoleFilterProvider.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogConsoleLogHandlerCollectors.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt create mode 100644 jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderUtil.kt create mode 100644 jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt create mode 100644 jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingTestUtils.kt create mode 100644 jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt create mode 100644 jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/customization/console/KotlinLogFinderHyperlinkTest.kt diff --git a/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml b/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml index 91d7e50b9ce7..44c56ed22245 100644 --- a/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml +++ b/jvm/jvm-analysis-impl/resources/META-INF/JvmAnalysisPlugin.xml @@ -152,6 +152,11 @@ serviceImplementation="com.intellij.psi.UastAnnotationCacheOwnerNormalizer"/> + + + + + diff --git a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties index 9890aca78708..3276ef9a07b8 100644 --- a/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties +++ b/jvm/jvm-analysis-impl/resources/messages/JvmAnalysisBundle.properties @@ -205,6 +205,8 @@ current.version=Current version dialog.title.choose.annotation=Choose {0} jvm.inspections.dependency.intention.description=Opens a dialog to configure dependency rules between scopes. +jvm.class.filter.choose.calls=Similar Log Calls: + inspection.suppression.annotation.display.name=Inspection suppression annotation inspection.suppression.annotation.problem.descriptor=Annotation suppresses {0} #loc inspection.suppression.comment.problem.descriptor=Comment suppresses {0} #loc @@ -212,3 +214,6 @@ ignored.suppressions=Ignored suppressions: remove.suppress.comment.fix.family.name=Remove //{0} allow.suppressions.fix.family.name=Allow suppressions allow.suppressions.fix.text=Allow these suppressions + +group.advanced.settings.jvm=JVM languages +advanced.setting.process.console.output.to.find.class.names=Process terminal output to find class names and highlight them diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt new file mode 100644 index 000000000000..bfe60f950abe --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassFinderFilter.kt @@ -0,0 +1,139 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.execution.filters.Filter +import com.intellij.execution.filters.HyperlinkInfo +import com.intellij.execution.filters.HyperlinkInfoFactory +import com.intellij.openapi.editor.markup.EffectType +import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.util.ui.NamedColorUtil +import org.jetbrains.annotations.NonNls + +internal data class ProbableClassName(val from: Int, + val to: Int, + val fullLine: String, + val shortClassName: String, + val packageName: String, + val virtualFiles: List) + +private val EXCEPTION_IN_THREAD: @NonNls String = "Exception in thread \"" +private val CAUSED_BY: @NonNls String = "Caused by: " +private val AT: @NonNls String = "\tat " + +internal class ClassFinderFilter(private val myProject: Project, myScope: GlobalSearchScope) : Filter { + private val cache = ClassInfoCache(myProject, myScope) + + override fun applyFilter(line: String, entireLength: Int): Filter.Result? { + val textStartOffset = entireLength - line.length + + val expectedClasses = findProbableClasses(line, cache) + val results: MutableList = ArrayList() + for (probableClass in expectedClasses) { + val attributes = hyperLinkAttributes() + + results.add( + Filter.Result(textStartOffset + probableClass.from, + textStartOffset + probableClass.to, + getHyperLink(probableClass), + attributes, + attributes)) + } + if (results.isEmpty()) { + return null + } + return Filter.Result(results) + } + + private fun hyperLinkAttributes(): TextAttributes { + val attributes = TextAttributes() + attributes.effectType = EffectType.BOLD_DOTTED_LINE + attributes.effectColor = NamedColorUtil.getInactiveTextColor() + return attributes + } + + + private fun getHyperLink(probableClassName: ProbableClassName): HyperlinkInfo { + return HyperlinkInfoFactory.getInstance() + .createMultipleFilesHyperlinkInfo(probableClassName.virtualFiles, 0, myProject, LogFinderHyperlinkHandler(probableClassName)) + } + + companion object { + private fun findProbableClasses(line: String, cache: ClassInfoCache): List { + if (line.startsWith(EXCEPTION_IN_THREAD) || line.startsWith(CAUSED_BY) || line.startsWith(AT)) { + return emptyList() + } + + val result = mutableListOf() + var start = -1 + var pointCount = 0 + for (i in line.indices) { + val c = line[i] + if (start == -1 && StringUtil.isJavaIdentifierStart(c)) { + start = i + continue + } + if (start != -1 && c == '.') { + pointCount++ + continue + } + if (start != -1 && + ((line[i - 1] == '.' && StringUtil.isJavaIdentifierStart(c)) || + (line[i - 1] != '.' && StringUtil.isJavaIdentifierPart(c)))) { + if (i == line.lastIndex && pointCount >= 2) { + addProbableClass(line, start, i + 1, cache, result) + } + continue + } + + if (pointCount >= 2) { + addProbableClass(line, start, i, cache, result) + } + pointCount = 0 + start = -1 + } + return result + } + + private fun addProbableClass(line: String, + startInclusive: Int, + endExclusive: Int, + cache: ClassInfoCache, + result: MutableList) { + val fullClassName = line.substring(startInclusive, endExclusive).removeSuffix(".") + if (canBeShortenedFullyQualifiedClassName(fullClassName) && isJavaStyle(fullClassName)) { + val packageName = StringUtil.getPackageName(fullClassName) + val className = fullClassName.substring(packageName.length + 1) + val resolvedClasses = cache.resolveClasses(className, packageName) + if (resolvedClasses.classes.isNotEmpty()) { + val probableClassName = ProbableClassName(startInclusive + fullClassName.lastIndexOf(".") + 1, startInclusive + fullClassName.length, + line, className, packageName, resolvedClasses.classes.values.toList()) + result.add(probableClassName) + } + } + } + + private fun isJavaStyle(shortenedClassName: String): Boolean { + val packageName = StringUtil.getPackageName(shortenedClassName) + val className = shortenedClassName.substring(packageName.length + 1) + return !className.contains("_") && + !packageName.contains("_") && + className.isNotEmpty() && packageName.isNotEmpty() && + Character.isUpperCase(className[0]) && + Character.isLowerCase(packageName[0]) + } + + private fun canBeShortenedFullyQualifiedClassName(fullClassName: String): Boolean { + val parts = fullClassName.split(".") + for (part in parts) { + if (part.isEmpty() || !StringUtil.isJavaIdentifier(part)) { + return false + } + } + return true + } + } +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt new file mode 100644 index 000000000000..168168c0f033 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassInfoCache.kt @@ -0,0 +1,84 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.openapi.project.DumbService.Companion.isDumb +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiElement +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.search.PsiShortNamesCache +import com.intellij.util.containers.ContainerUtil + +class ClassInfoCache(val project: Project, private val mySearchScope: GlobalSearchScope) { + + companion object { + private fun findClasses(project: Project, + scope: GlobalSearchScope, + shortClassName: String, + targetPackageName: String): List { + val result = mutableSetOf() + val list = PsiShortNamesCache.EP_NAME.getExtensionList(project) + for (cache in list) { + val classes = cache.getClassesByName(shortClassName, scope) + for (clazz in classes) { + if (!canBeShortenedPackages(clazz, targetPackageName)) { + continue + } + result.add(clazz) + } + } + return result.toList() + } + + private fun canBeShortenedPackages(clazz: PsiClass, targetPackageName: String): Boolean { + val qualifiedName = clazz.qualifiedName ?: return false + val actualPackageName = StringUtil.getPackageName(qualifiedName) + val actualPackageNames = actualPackageName.split(".") + val targetPackageNames = targetPackageName.split(".") + if (actualPackageNames.size != targetPackageNames.size) return false + for (i in actualPackageNames.indices) { + if (!actualPackageNames[i].startsWith(targetPackageNames[i])) { + return false + } + } + return true + } + } + + private val myCache = ContainerUtil.createConcurrentSoftValueMap() + + fun resolveClasses(className: String, packageName: String): ClassResolveInfo { + val key = "$packageName.$className" + val cached = myCache[key] + if (cached != null && cached.isValid) { + return cached + } + + if (isDumb(project)) { + return ClassResolveInfo.EMPTY + } + + val classes = findClasses(project, mySearchScope, className, packageName) + + val mapWithClasses = classes.filter { it.isValid && it.containingFile != null } + .map { Pair(it, it.containingFile.virtualFile) } + .filter { it.second != null } + .toMap() + + val result = if (mapWithClasses.isEmpty()) ClassResolveInfo.EMPTY else ClassResolveInfo(mapWithClasses) + myCache[key] = result + return result + } + + class ClassResolveInfo internal constructor(val classes: Map) { + + val isValid: Boolean + get() = classes.keys.all { obj: PsiElement -> obj.isValid } + + companion object { + val EMPTY: ClassResolveInfo = ClassResolveInfo(mapOf()) + } + } +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassLoggingConsoleFilterProvider.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassLoggingConsoleFilterProvider.kt new file mode 100644 index 000000000000..4add167f7788 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/ClassLoggingConsoleFilterProvider.kt @@ -0,0 +1,37 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.codeInspection.logging.LoggingUtil +import com.intellij.execution.filters.ConsoleFilterProvider +import com.intellij.execution.filters.Filter +import com.intellij.java.library.JavaLibraryUtil +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.module.ModuleType +import com.intellij.openapi.module.ModuleTypeId +import com.intellij.openapi.options.advanced.AdvancedSettings +import com.intellij.openapi.project.DumbService +import com.intellij.openapi.project.Project +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.search.GlobalSearchScope + +class ClassLoggingConsoleFilterProvider : ConsoleFilterProvider { + override fun getDefaultFilters(project: Project): Array { + if (!AdvancedSettings.getBoolean("process.console.output.to.find.class.names")) { + return Filter.EMPTY_ARRAY + } + if (DumbService.isDumb(project)) { + return Filter.EMPTY_ARRAY + } + val modules = ModuleManager.getInstance(project).modules + if (modules.none { ModuleTypeId.JAVA_MODULE.equals(ModuleType.get(it).id, ignoreCase = true) }) { + return Filter.EMPTY_ARRAY + } + + if (!(JavaLibraryUtil.hasLibraryClass(project, LoggingUtil.SLF4J_LOGGER) || + JavaLibraryUtil.hasLibraryClass(project, LoggingUtil.LOG4J_LOGGER) || + JavaPsiFacade.getInstance(project).findClass(LoggingUtil.IDEA_LOGGER, GlobalSearchScope.allScope(project)) != null)) { + return Filter.EMPTY_ARRAY + } + return arrayOf(ClassFinderFilter(project, GlobalSearchScope.allScope(project))) + } +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogConsoleLogHandlerCollectors.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogConsoleLogHandlerCollectors.kt new file mode 100644 index 000000000000..ffff33b2cc6d --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogConsoleLogHandlerCollectors.kt @@ -0,0 +1,50 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.internal.statistic.eventLog.EventLogGroup +import com.intellij.internal.statistic.eventLog.events.EventField +import com.intellij.internal.statistic.eventLog.events.EventFields.Int +import com.intellij.internal.statistic.eventLog.events.EventFields.String +import com.intellij.internal.statistic.eventLog.events.EventPair +import com.intellij.internal.statistic.eventLog.events.IntEventField +import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector +import com.intellij.openapi.project.Project + + +object LogConsoleLogHandlerCollectors : CounterUsagesCollector() { + + private const val CLASS_TYPE = "class" + private const val LOG_TYPE = "log_call" + + private val ourGroup = EventLogGroup("jvm.console.log.filter", 1) + + private val TYPE_FINDER: EventField = String("type", listOf(CLASS_TYPE, LOG_TYPE)) + + private val NUMBER_ITEMS: IntEventField = Int("number_items") + + private val HANDLE_EVENT = ourGroup.registerVarargEvent("handle", + TYPE_FINDER, + NUMBER_ITEMS) + + override fun getGroup(): EventLogGroup { + return ourGroup + } + + fun logHandleClass(project: Project?, numberItems: Int) { + val data = ArrayList>(2) + + data.add(TYPE_FINDER.with(CLASS_TYPE)) + data.add(NUMBER_ITEMS.with(numberItems)) + + HANDLE_EVENT.log(project, data) + } + + fun logHandleLogCalls(project: Project?, numberItems: Int) { + val data = ArrayList>(2) + + data.add(TYPE_FINDER.with(LOG_TYPE)) + data.add(NUMBER_ITEMS.with(numberItems)) + + HANDLE_EVENT.log(project, data) + } +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt new file mode 100644 index 000000000000..982e3c3e3601 --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/analysis/customization/console/LogFinderHyperlinkHandler.kt @@ -0,0 +1,163 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.analysis.customization.console + +import com.intellij.analysis.JvmAnalysisBundle +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.codeInsight.navigation.PsiTargetNavigator +import com.intellij.codeInspection.logging.* +import com.intellij.execution.filters.HyperlinkInfoFactory +import com.intellij.ide.util.EditSourceUtil +import com.intellij.openapi.actionSystem.ex.ActionUtil.underModalProgress +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.NlsSafe +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.platform.backend.presentation.TargetPresentation +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiManager +import com.intellij.psi.PsiRecursiveElementVisitor +import org.jetbrains.annotations.Nls +import org.jetbrains.uast.* + +internal class LogFinderHyperlinkHandler(private val probableClassName: ProbableClassName) : HyperlinkInfoFactory.HyperlinkHandler { + override fun onLinkFollowed(project: Project, file: VirtualFile, targetEditor: Editor, originalEditor: Editor?) { + LogConsoleLogHandlerCollectors.logHandleClass(project, probableClassName.virtualFiles.size) + + val psiFile = PsiManager.getInstance(project).findFile(file) + if (psiFile == null) return + val uElement = psiFile.toUElement() + if (uElement == null) return + val visitor = underModalProgress(project, CodeInsightBundle.message("progress.title.resolving.reference")) { + val logVisitor = LogVisitor(probableClassName) + psiFile.accept(logVisitor) + logVisitor + } + + if (visitor.similarClasses.isNotEmpty()) { + val uClass = visitor.similarClasses.minBy { it.sourcePsi?.textRange?.startOffset ?: Int.MAX_VALUE } + val sourcePsi = uClass.uastAnchor?.sourcePsi + if (sourcePsi != null) { + EditSourceUtil.navigateToPsiElement(sourcePsi) + } + } + else { + return + } + + if (!visitor.similarCalls.isEmpty()) { + navigateToCalls(visitor, project, targetEditor) + } + } + + private fun navigateToCalls(visitor: LogVisitor, + project: Project, + targetEditor: Editor) { + if (visitor.similarCalls.size == 1) { + val sourcePsi = visitor.similarCalls.first().sourcePsi ?: return + EditSourceUtil.navigateToPsiElement(sourcePsi) + LogConsoleLogHandlerCollectors.logHandleLogCalls(project, 1) + } + else { + val targetElements = visitor.similarCalls.mapNotNull { it.sourcePsi }.toList() + PsiTargetNavigator(targetElements) + .presentationProvider { element -> + TargetPresentation.builder(getText(element)) + .containerText(getContainerText(element)) + .presentation() + } + .elementsConsumer { elements, navigator -> + if (!elements.isEmpty()) { + val message = JvmAnalysisBundle.message("jvm.class.filter.choose.calls") + navigator.title(message) + navigator.tabTitle(message) + } + } + .navigate(targetEditor, null) { + LogConsoleLogHandlerCollectors.logHandleLogCalls(project, targetElements.size) + EditSourceUtil.navigateToPsiElement(it) + } + } + } + + private fun getContainerText(element: PsiElement): @NlsSafe String { + val method = element.toUElement()?.getParentOfType() ?: return "" + val className = method.getParentOfType()?.javaPsi?.name ?: "" + val document = PsiDocumentManager.getInstance(element.getProject()).getDocument(element.containingFile) ?: return "" + val lineNumber = document.getLineNumber(element.textRange.endOffset) + val container = className + "." + method.name + "(): " + (lineNumber + 1) + return StringUtil.shortenTextWithEllipsis(container, 40, 10) + } + + @Nls + private fun getText(element: PsiElement): String { + val text = element.text + val document = PsiDocumentManager.getInstance(element.getProject()).getDocument(element.containingFile) ?: return text + val lineNumber = document.getLineNumber(element.textRange.endOffset) + val textRange = element.textRange + .intersection(TextRange(document.getLineStartOffset(lineNumber), document.getLineEndOffset(lineNumber))) ?: element.textRange + val trimmedText = document.getText(textRange).trim() + return StringUtil.shortenTextWithEllipsis(trimmedText, 30, 0) + } +} + +private class LogVisitor(private val probableClassName: ProbableClassName) : PsiRecursiveElementVisitor() { + val similarClasses = mutableSetOf() + val similarCalls = mutableSetOf() + override fun visitElement(element: PsiElement) { + val uClass = element.toUElementOfType() + if (uClass != null && + probableClassName.shortClassName == uClass.javaPsi.name) { + similarClasses.add(uClass) + } + val uCall = element.toUElementOfType() + if (uCall != null && checkCalls(uCall, probableClassName)) { + similarCalls.add(uCall) + } + super.visitElement(element) + } + + private fun checkCalls(uCall: UCallExpression, probableClassName: ProbableClassName): Boolean { + val isLogger = LoggingUtil.LOG_MATCHERS.uCallMatches(uCall) || + LoggingUtil.LEGACY_LOG_MATCHERS.uCallMatches(uCall) || + LoggingUtil.IDEA_LOG_MATCHER.uCallMatches(uCall) + if (!isLogger) { + return false + } + val method = uCall.resolveToUElementOfType() ?: return false + val arguments = uCall.valueArguments + val parameters = method.uastParameters + val index = getLogStringIndex(parameters) + var logStringArgument: UExpression? = null + if (index == null) { + val searcher = LOGGER_TYPE_SEARCHERS.mapFirst(uCall) + if (searcher != null) { + logStringArgument = findMessageSetterStringArg(uCall, searcher) + } + } + else { + logStringArgument = arguments[index - 1] + } + if (logStringArgument == null && parameters.isNotEmpty()) logStringArgument = arguments[0] + if (logStringArgument == null) return false + val calculateValue = LoggingStringPartEvaluator.calculateValue(logStringArgument) ?: return false + val fullLine = probableClassName.fullLine + val classFullName = probableClassName.packageName + "." + probableClassName.shortClassName + var startPoint = probableClassName.fullLine.indexOf(classFullName) + if (startPoint == -1) return false + startPoint += classFullName.length + if (calculateValue.none { it.isConstant && it.text != null }) return false + for (value in calculateValue) { + if (value.isConstant && value.text != null) { + for (part in value.text.split("{}")) { + startPoint = fullLine.indexOf(part, startPoint) + if (startPoint == -1) return false + startPoint += part.length + } + } + } + return true + } +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderCountMatchesArgumentCountInspection.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderCountMatchesArgumentCountInspection.kt index 7d86e6e60ba4..970be0edcee3 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderCountMatchesArgumentCountInspection.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderCountMatchesArgumentCountInspection.kt @@ -12,18 +12,13 @@ import com.intellij.psi.* import com.intellij.psi.util.InheritanceUtil import com.intellij.uast.UastHintedVisitorAdapter import com.intellij.util.containers.addIfNotNull -import com.siyeh.ig.callMatcher.CallMapper -import com.siyeh.ig.callMatcher.CallMatcher import com.siyeh.ig.format.FormatDecode -import com.siyeh.ig.psiutils.TypeUtils import org.jetbrains.uast.* import org.jetbrains.uast.visitor.AbstractUastNonRecursiveVisitor import org.jetbrains.uast.visitor.AbstractUastVisitor -private const val MAX_BUILDER_LENGTH = 20 class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLocalInspectionTool() { - @JvmField var slf4jToLog4J2Type = Slf4jToLog4J2Type.AUTO @@ -87,7 +82,7 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo argumentCount = findAdditionalArgumentCount(node, searcher, true) ?: return true } else { - val index = getIndex(parameters) ?: return true + val index = getLogStringIndex(parameters) ?: return true argumentCount = arguments.size - index lastArgumentIsException = hasThrowableType(arguments[arguments.size - 1]) @@ -118,7 +113,7 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo val resultType = when (loggerType) { - LoggerType.SLF4J -> { //according to the reference, an exception should not have a placeholder + PlaceholderLoggerType.SLF4J -> { //according to the reference, an exception should not have a placeholder argumentCount = if (lastArgumentIsException) argumentCount - 1 else argumentCount if (placeholderCountHolder.status == PlaceholdersStatus.PARTIAL) { if (placeholderCountHolder.count <= argumentCount) ResultType.SUCCESS else ResultType.PARTIAL_PLACE_HOLDER_MISMATCH @@ -127,7 +122,7 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo if (placeholderCountHolder.count == argumentCount) ResultType.SUCCESS else ResultType.PLACE_HOLDER_MISMATCH } } - LoggerType.SLF4J_EQUAL_PLACEHOLDERS, LoggerType.LOG4J_EQUAL_PLACEHOLDERS, LoggerType.AKKA_PLACEHOLDERS -> { + PlaceholderLoggerType.SLF4J_EQUAL_PLACEHOLDERS, PlaceholderLoggerType.LOG4J_EQUAL_PLACEHOLDERS, PlaceholderLoggerType.AKKA_PLACEHOLDERS -> { if (placeholderCountHolder.status == PlaceholdersStatus.PARTIAL) { if (placeholderCountHolder.count <= argumentCount) ResultType.SUCCESS else ResultType.PARTIAL_PLACE_HOLDER_MISMATCH } @@ -135,7 +130,8 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo if (placeholderCountHolder.count == argumentCount) ResultType.SUCCESS else ResultType.PLACE_HOLDER_MISMATCH } } - LoggerType.LOG4J_OLD_STYLE, LoggerType.LOG4J_FORMATTED_STYLE -> { // if there is more than one argument and the last argument is an exception, but there is a placeholder for + PlaceholderLoggerType.LOG4J_OLD_STYLE, + PlaceholderLoggerType.LOG4J_FORMATTED_STYLE -> { // if there is more than one argument and the last argument is an exception, but there is a placeholder for // the exception, then the stack trace won't be logged. val type: ResultType if (placeholderCountHolder.status == PlaceholdersStatus.PARTIAL) { @@ -162,75 +158,6 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo return true } - private val BUILDER_CHAIN = setOf("addKeyValue", "addMarker", "setCause") - private val ADD_ARGUMENT = "addArgument" - private val SET_MESSAGE = "setMessage" - private fun findMessageSetterStringArg(node: UCallExpression, - loggerType: LoggerTypeSearcher): UExpression? { - if (loggerType != SLF4J_BUILDER_HOLDER) { - return null - } - var currentCall = node.receiver - for (ignore in 0..MAX_BUILDER_LENGTH) { - if (currentCall is UQualifiedReferenceExpression) { - currentCall = currentCall.selector - continue - } - if (currentCall !is UCallExpression) { - return null - } - val methodName = currentCall.methodName ?: return null - if (methodName == SET_MESSAGE && currentCall.valueArgumentCount == 1) { - val uExpression = currentCall.valueArguments[0] - if (!TypeUtils.isJavaLangString(uExpression.getExpressionType())) { - return null - } - return uExpression - } - if (BUILDER_CHAIN.contains(methodName)) { - currentCall = currentCall.receiver - continue - } - return null - } - return null - } - - /** - * @return The count of additional arguments, or null if it is impossible to count. - */ - private fun findAdditionalArgumentCount(node: UCallExpression, - loggerType: LoggerTypeSearcher, - allowIntermediateMessage: Boolean): Int? { - if (loggerType != SLF4J_BUILDER_HOLDER) { - return 0 - } - var additionalArgumentCount = 0 - var currentCall = node.receiver - for (ignore in 0..MAX_BUILDER_LENGTH) { - if (currentCall is UQualifiedReferenceExpression) { - currentCall = currentCall.selector - } - if (currentCall is UCallExpression) { - val methodName = currentCall.methodName ?: return null - if (methodName == ADD_ARGUMENT) { - additionalArgumentCount++ - currentCall = currentCall.receiver - continue - } - if (methodName.startsWith("at") && LoggingUtil.getLoggerLevel(currentCall) != null) { - return additionalArgumentCount - } - if (BUILDER_CHAIN.contains(methodName) || (allowIntermediateMessage && methodName == SET_MESSAGE)) { - currentCall = currentCall.receiver - continue - } - return null - } - return null - } - return null - } private fun collectParts(logStringArgument: UExpression): List? { return LoggingStringPartEvaluator.calculateValue(logStringArgument) @@ -258,10 +185,10 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo result.argumentCount, result.placeholderCount) } - private fun solvePlaceholderCount(loggerType: LoggerType, + private fun solvePlaceholderCount(loggerType: PlaceholderLoggerType, argumentCount: Int, holders: List): PlaceholderCountResult { - return if (loggerType == LoggerType.LOG4J_FORMATTED_STYLE) { + return if (loggerType == PlaceholderLoggerType.LOG4J_FORMATTED_STYLE) { val prefix = StringBuilder() var full = true for (holder in holders) { @@ -294,7 +221,7 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo } } - private fun countPlaceholders(holders: List, loggerType: LoggerType): PlaceholderCountResult { + private fun countPlaceholders(holders: List, loggerType: PlaceholderLoggerType): PlaceholderCountResult { var count = 0 var full = true for (holderIndex in holders.indices) { @@ -310,7 +237,7 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo for (i in 0 until length) { val c = string[i] if (c == '\\' && - (loggerType == LoggerType.SLF4J_EQUAL_PLACEHOLDERS || loggerType == LoggerType.SLF4J)) { + (loggerType == PlaceholderLoggerType.SLF4J_EQUAL_PLACEHOLDERS || loggerType == PlaceholderLoggerType.SLF4J)) { escaped = !escaped } else if (c == '{') { @@ -334,8 +261,8 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo return PlaceholderCountResult(count, if (full) PlaceholdersStatus.EXACTLY else PlaceholdersStatus.PARTIAL) } - private fun couldBeThrowableSupplier(loggerType: LoggerType, lastParameter: UParameter?, lastArgument: UExpression?): Boolean { - if (loggerType != LoggerType.LOG4J_OLD_STYLE && loggerType != LoggerType.LOG4J_FORMATTED_STYLE) { + private fun couldBeThrowableSupplier(loggerType: PlaceholderLoggerType, lastParameter: UParameter?, lastArgument: UExpression?): Boolean { + if (loggerType != PlaceholderLoggerType.LOG4J_OLD_STYLE && loggerType != PlaceholderLoggerType.LOG4J_FORMATTED_STYLE) { return false } if (lastParameter == null || lastArgument == null) { @@ -379,133 +306,8 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo } return InheritanceUtil.isInheritor(type, CommonClassNames.JAVA_LANG_THROWABLE) } - - private fun getIndex(parameters: List): Int? { - val index: Int? - if (!TypeUtils.isJavaLangString(parameters[0].type)) { - if (parameters.size < 2 || !TypeUtils.isJavaLangString(parameters[1].type)) { - index = null - } - else { - index = 2 - } - } - else { - index = 1 - } - return index - } - - private val LOGGER_TYPE_SEARCHERS = CallMapper() - .register(CallMatcher.instanceCall(LoggingUtil.SLF4J_LOGGER, "trace", "debug", "info", "warn", "error"), SLF4J_HOLDER) - .register(CallMatcher.instanceCall(LoggingUtil.SLF4J_EVENT_BUILDER, "log"), SLF4J_BUILDER_HOLDER) - .register(CallMatcher.instanceCall(LoggingUtil.LOG4J_LOGGER, "trace", "debug", "info", "warn", "error", "fatal", "log"), LOG4J_HOLDER) - .register(CallMatcher.instanceCall(LoggingUtil.LOG4J_LOG_BUILDER, "log"), LOG4J_LOG_BUILDER_HOLDER) - .register(CallMatcher.instanceCall(LoggingUtil.AKKA_LOGGING, "debug", "error", "format", "info", "log", "warning"), - AKKA_PLACEHOLDERS) } - - private interface LoggerTypeSearcher { - fun findType(expression: UCallExpression, context: LoggerContext): LoggerType? - } - - private val SLF4J_HOLDER = object : LoggerTypeSearcher { - override fun findType(expression: UCallExpression, context: LoggerContext): LoggerType { - return if (context.log4jAsImplementationForSlf4j) { //use old style as more common - LoggerType.LOG4J_OLD_STYLE - } - else LoggerType.SLF4J - } - } - - private val LOG4J_LOG_BUILDER_HOLDER = object : LoggerTypeSearcher { - override fun findType(expression: UCallExpression, context: LoggerContext): LoggerType? { - var qualifierExpression = getImmediateLoggerQualifier(expression) - if (qualifierExpression is UReferenceExpression) { - val target: UVariable = qualifierExpression.resolveToUElement() as? UVariable ?: return null - if (!target.isFinal) { - return LoggerType.LOG4J_EQUAL_PLACEHOLDERS //formatted builder is really rare - } - qualifierExpression = target.uastInitializer?.skipParenthesizedExprDown() - } - if (qualifierExpression is UQualifiedReferenceExpression) { - qualifierExpression = qualifierExpression.selector - } - if (qualifierExpression is UCallExpression) { - return when (LOG4J_HOLDER.findType(qualifierExpression, context)) { - LoggerType.LOG4J_FORMATTED_STYLE -> LoggerType.LOG4J_FORMATTED_STYLE - LoggerType.LOG4J_OLD_STYLE -> LoggerType.LOG4J_EQUAL_PLACEHOLDERS - else -> null - } - } - return LoggerType.LOG4J_EQUAL_PLACEHOLDERS - } - } - - - private val SLF4J_BUILDER_HOLDER = object : LoggerTypeSearcher { - override fun findType(expression: UCallExpression, context: LoggerContext): LoggerType { - if (context.log4jAsImplementationForSlf4j) { - return LoggerType.SLF4J_EQUAL_PLACEHOLDERS - } - return LoggerType.SLF4J - } - } - - private val LOG4J_HOLDER = object : LoggerTypeSearcher { - override fun findType(expression: UCallExpression, context: LoggerContext): LoggerType? { - val qualifierExpression = getImmediateLoggerQualifier(expression) - var initializer: UExpression? = null - if (qualifierExpression is UReferenceExpression) { - var resolvedToUElement = qualifierExpression.resolveToUElement() - if (resolvedToUElement is UMethod) { - resolvedToUElement = resolvedToUElement.sourcePsi.toUElement() - } - val target: UVariable = resolvedToUElement as? UVariable ?: return null - val sourcePsi = target.sourcePsi as? PsiVariable - // for lombok - if (sourcePsi != null && !sourcePsi.isPhysical) { - return LoggerType.LOG4J_OLD_STYLE - } - if (!target.isFinal) { - return null - } - initializer = target.uastInitializer - if (initializer == null) return null - } - else if (qualifierExpression is UCallExpression) { - initializer = qualifierExpression - } - initializer = initializer?.skipParenthesizedExprDown() - if (initializer is UQualifiedReferenceExpression) { - initializer = initializer.selector - } - return if (initializer is UCallExpression && LoggingUtil.FORMATTED_LOG4J.uCallMatches(initializer)) { - LoggerType.LOG4J_FORMATTED_STYLE - } - else LoggerType.LOG4J_OLD_STYLE - } - - } - - private val AKKA_PLACEHOLDERS = object : LoggerTypeSearcher { - override fun findType(expression: UCallExpression, context: LoggerContext): LoggerType { - return LoggerType.AKKA_PLACEHOLDERS - } - } - - private fun getImmediateLoggerQualifier(expression: UCallExpression): UExpression? { - val result = expression.receiver?.skipParenthesizedExprDown() - if (result is UQualifiedReferenceExpression) { - return result.selector - } - return result - } - - private data class LoggerContext(val log4jAsImplementationForSlf4j: Boolean) - - private enum class ResultType { PARTIAL_PLACE_HOLDER_MISMATCH, PLACE_HOLDER_MISMATCH, INCORRECT_STRING, SUCCESS } @@ -514,15 +316,6 @@ class LoggingPlaceholderCountMatchesArgumentCountInspection : AbstractBaseUastLo AUTO, YES, NO } - private enum class LoggerType { - SLF4J, SLF4J_EQUAL_PLACEHOLDERS, LOG4J_OLD_STYLE, LOG4J_FORMATTED_STYLE, LOG4J_EQUAL_PLACEHOLDERS, AKKA_PLACEHOLDERS - } - - - private enum class PlaceholdersStatus { - EXACTLY, PARTIAL, ERROR_TO_PARSE_STRING, EMPTY - } - private data class PlaceholderCountResult(val count: Int, val status: PlaceholdersStatus) diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderUtil.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderUtil.kt new file mode 100644 index 000000000000..ca4011d9726c --- /dev/null +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingPlaceholderUtil.kt @@ -0,0 +1,212 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.codeInspection.logging + +import com.intellij.psi.PsiVariable +import com.siyeh.ig.callMatcher.CallMapper +import com.siyeh.ig.callMatcher.CallMatcher +import com.siyeh.ig.psiutils.TypeUtils +import org.jetbrains.uast.* + +internal const val MAX_BUILDER_LENGTH = 20 + +internal fun getLogStringIndex(parameters: List): Int? { + if (parameters.isEmpty()) return null + if (!TypeUtils.isJavaLangString(parameters[0].type)) { + if (parameters.size < 2 || !TypeUtils.isJavaLangString(parameters[1].type)) { + return null + } + else { + return 2 + } + } + else { + return 1 + } +} + +private val SLF4J_HOLDER = object : LoggerTypeSearcher { + override fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType { + return if (context.log4jAsImplementationForSlf4j) { //use old style as more common + PlaceholderLoggerType.LOG4J_OLD_STYLE + } + else PlaceholderLoggerType.SLF4J + } +} + +private val LOG4J_LOG_BUILDER_HOLDER = object : LoggerTypeSearcher { + override fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType? { + var qualifierExpression = getImmediateLoggerQualifier(expression) + if (qualifierExpression is UReferenceExpression) { + val target: UVariable = qualifierExpression.resolveToUElement() as? UVariable ?: return null + if (!target.isFinal) { + return PlaceholderLoggerType.LOG4J_EQUAL_PLACEHOLDERS //formatted builder is really rare + } + qualifierExpression = target.uastInitializer?.skipParenthesizedExprDown() + } + if (qualifierExpression is UQualifiedReferenceExpression) { + qualifierExpression = qualifierExpression.selector + } + if (qualifierExpression is UCallExpression) { + return when (LOG4J_HOLDER.findType(qualifierExpression, context)) { + PlaceholderLoggerType.LOG4J_FORMATTED_STYLE -> PlaceholderLoggerType.LOG4J_FORMATTED_STYLE + PlaceholderLoggerType.LOG4J_OLD_STYLE -> PlaceholderLoggerType.LOG4J_EQUAL_PLACEHOLDERS + else -> null + } + } + return PlaceholderLoggerType.LOG4J_EQUAL_PLACEHOLDERS + } +} + + +internal val SLF4J_BUILDER_HOLDER = object : LoggerTypeSearcher { + override fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType { + if (context.log4jAsImplementationForSlf4j) { + return PlaceholderLoggerType.SLF4J_EQUAL_PLACEHOLDERS + } + return PlaceholderLoggerType.SLF4J + } +} + +private val LOG4J_HOLDER = object : LoggerTypeSearcher { + override fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType? { + val qualifierExpression = getImmediateLoggerQualifier(expression) + var initializer: UExpression? = null + if (qualifierExpression is UReferenceExpression) { + var resolvedToUElement = qualifierExpression.resolveToUElement() + if (resolvedToUElement is UMethod) { + //convert kotlin light method of property's accessor to UField + resolvedToUElement = resolvedToUElement.sourcePsi.toUElement() + } + val target: UVariable = resolvedToUElement as? UVariable ?: return null + val sourcePsi = target.sourcePsi as? PsiVariable + // for lombok or other annotation generators. LOG4J_OLD_STYLE is the most common decision for that + if (sourcePsi != null && !sourcePsi.isPhysical) { + return PlaceholderLoggerType.LOG4J_OLD_STYLE + } + if (!target.isFinal) { + return null + } + initializer = target.uastInitializer + if (initializer == null) return null + } + else if (qualifierExpression is UCallExpression) { + initializer = qualifierExpression + } + initializer = initializer?.skipParenthesizedExprDown() + if (initializer is UQualifiedReferenceExpression) { + initializer = initializer.selector + } + return if (initializer is UCallExpression && LoggingUtil.FORMATTED_LOG4J.uCallMatches(initializer)) { + PlaceholderLoggerType.LOG4J_FORMATTED_STYLE + } + else PlaceholderLoggerType.LOG4J_OLD_STYLE + } +} + +private val AKKA_PLACEHOLDERS = object : LoggerTypeSearcher { + override fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType { + return PlaceholderLoggerType.AKKA_PLACEHOLDERS + } +} + +internal class LoggerContext(val log4jAsImplementationForSlf4j: Boolean) + +internal interface LoggerTypeSearcher { + fun findType(expression: UCallExpression, context: LoggerContext): PlaceholderLoggerType? +} + +private fun getImmediateLoggerQualifier(expression: UCallExpression): UExpression? { + val result = expression.receiver?.skipParenthesizedExprDown() + if (result is UQualifiedReferenceExpression) { + return result.selector + } + return result +} + +internal val LOGGER_TYPE_SEARCHERS: CallMapper = CallMapper() + .register(CallMatcher.instanceCall(LoggingUtil.SLF4J_LOGGER, "trace", "debug", "info", "warn", "error"), SLF4J_HOLDER) + .register(CallMatcher.instanceCall(LoggingUtil.SLF4J_EVENT_BUILDER, "log"), SLF4J_BUILDER_HOLDER) + .register(CallMatcher.instanceCall(LoggingUtil.LOG4J_LOGGER, "trace", "debug", "info", "warn", "error", "fatal", "log"), LOG4J_HOLDER) + .register(CallMatcher.instanceCall(LoggingUtil.LOG4J_LOG_BUILDER, "log"), LOG4J_LOG_BUILDER_HOLDER) + .register(CallMatcher.instanceCall(LoggingUtil.AKKA_LOGGING, "debug", "error", "format", "info", "log", "warning"), + AKKA_PLACEHOLDERS) + + +private val BUILDER_CHAIN = setOf("addKeyValue", "addMarker", "setCause") +private const val ADD_ARGUMENT = "addArgument" +private const val SET_MESSAGE = "setMessage" + +internal fun findMessageSetterStringArg(node: UCallExpression, + loggerType: LoggerTypeSearcher): UExpression? { + if (loggerType != SLF4J_BUILDER_HOLDER) { + return null + } + var currentCall = node.receiver + for (ignore in 0..MAX_BUILDER_LENGTH) { + if (currentCall is UQualifiedReferenceExpression) { + currentCall = currentCall.selector + continue + } + if (currentCall !is UCallExpression) { + return null + } + val methodName = currentCall.methodName ?: return null + if (methodName == SET_MESSAGE && currentCall.valueArgumentCount == 1) { + val uExpression = currentCall.valueArguments[0] + if (!TypeUtils.isJavaLangString(uExpression.getExpressionType())) { + return null + } + return uExpression + } + if (BUILDER_CHAIN.contains(methodName) || methodName == ADD_ARGUMENT) { + currentCall = currentCall.receiver + continue + } + return null + } + return null +} + +/** + * @return The count of additional arguments, or null if it is impossible to count. + */ +internal fun findAdditionalArgumentCount(node: UCallExpression, + loggerType: LoggerTypeSearcher, + allowIntermediateMessage: Boolean): Int? { + if (loggerType != SLF4J_BUILDER_HOLDER) { + return 0 + } + var additionalArgumentCount = 0 + var currentCall = node.receiver + for (ignore in 0..MAX_BUILDER_LENGTH) { + if (currentCall is UQualifiedReferenceExpression) { + currentCall = currentCall.selector + } + if (currentCall is UCallExpression) { + val methodName = currentCall.methodName ?: return null + if (methodName == ADD_ARGUMENT) { + additionalArgumentCount++ + currentCall = currentCall.receiver + continue + } + if (methodName.startsWith("at") && LoggingUtil.getLoggerLevel(currentCall) != null) { + return additionalArgumentCount + } + if (BUILDER_CHAIN.contains(methodName) || (allowIntermediateMessage && methodName == SET_MESSAGE)) { + currentCall = currentCall.receiver + continue + } + return null + } + return null + } + return null +} + +internal enum class PlaceholderLoggerType { + SLF4J, SLF4J_EQUAL_PLACEHOLDERS, LOG4J_OLD_STYLE, LOG4J_FORMATTED_STYLE, LOG4J_EQUAL_PLACEHOLDERS, AKKA_PLACEHOLDERS +} + +internal enum class PlaceholdersStatus { + EXACTLY, PARTIAL, ERROR_TO_PARSE_STRING, EMPTY +} diff --git a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingUtil.kt b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingUtil.kt index 5866c048c69a..fe93992fde70 100644 --- a/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingUtil.kt +++ b/jvm/jvm-analysis-impl/src/com/intellij/codeInspection/logging/LoggingUtil.kt @@ -23,6 +23,8 @@ internal class LoggingUtil { internal const val AKKA_LOGGING = "akka.event.LoggingAdapter" + internal const val IDEA_LOGGER = "com.intellij.openapi.diagnostic.Logger" + private val LOGGER_CLASSES = setOf(SLF4J_LOGGER, LOG4J_LOGGER) private val LEGACY_LOGGER_CLASSES = setOf(LEGACY_LOG4J_LOGGER, LEGACY_CATEGORY_LOGGER, LEGACY_APACHE_COMMON_LOGGER, LEGACY_JAVA_LOGGER) @@ -49,6 +51,9 @@ internal class LoggingUtil { CallMatcher.instanceCall(LEGACY_APACHE_COMMON_LOGGER, "trace", "debug", "info", "warn", "error", "fatal"), CallMatcher.instanceCall(LEGACY_JAVA_LOGGER, "fine", "log", "finer", "finest", "logp", "logrb", "info", "severe", "warning", "config") ) + internal val IDEA_LOG_MATCHER: CallMatcher = CallMatcher.anyOf( + CallMatcher.instanceCall(IDEA_LOGGER, "trace", "debug", "info", "warn", "error"), + ) private val LEGACY_METHODS_WITH_LEVEL = setOf("log", "l7dlog", "logp", "logrb") diff --git a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt new file mode 100644 index 000000000000..2ded6db07921 --- /dev/null +++ b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/internal/LogFinderHyperlinkTestBase.kt @@ -0,0 +1,55 @@ +package com.intellij.jvm.analysis.internal.testFramework.internal + +import com.intellij.analysis.customization.console.ClassLoggingConsoleFilterProvider +import com.intellij.jvm.analysis.testFramework.LightJvmCodeInsightFixtureTestCase +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.options.advanced.AdvancedSettings +import com.intellij.openapi.options.advanced.AdvancedSettingsImpl +import org.intellij.lang.annotations.Language +import org.junit.Assert + +class LogItem(val text: String, val position: LogicalPosition?) + +abstract class LogFinderHyperlinkTestBase : LightJvmCodeInsightFixtureTestCase() { + + protected fun checkColumnFinderJava(@Language("Java") classText: String, + fileName: String, + logItems: List) { + myFixture.configureByText("$fileName.java", classText) + checkInside(classText, logItems) + } + + protected fun checkColumnFinderKotlin(@Language("kotlin") classText: String, + fileName: String, + logItems: List) { + myFixture.configureByText("$fileName.kt", classText) + checkInside(classText, logItems) + } + + private fun checkInside(classText: String, + logItems: List) { + (AdvancedSettings.getInstance() as AdvancedSettingsImpl) + .setSetting("process.console.output.to.find.class.names", true, testRootDisposable) + val editor = myFixture.editor + Assert.assertEquals(classText, editor.document.text) + val filters = ClassLoggingConsoleFilterProvider().getDefaultFilters(project) + assertSize(1, filters) + val filter = filters.first() + var len = 0 + for (logItem in logItems) { + val logLine = logItem.text + len += logLine.length + val result = filter.applyFilter(logLine, len) + if (logItem.position == null) { + Assert.assertNull(logItem.toString(), result) + continue + } + Assert.assertNotNull(logItem.toString(), result) + val info = result!!.firstHyperlinkInfo + Assert.assertNotNull(logItem.toString(), info) + info!!.navigate(project) + val actualPos = editor.caretModel.logicalPosition + Assert.assertEquals(logItem.toString(), logItem.position, actualPos) + } + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingInspectionTestBase.kt b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingInspectionTestBase.kt index 6ebb65edcae5..5862c98c8a14 100644 --- a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingInspectionTestBase.kt +++ b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingInspectionTestBase.kt @@ -1,133 +1,20 @@ package com.intellij.jvm.analysis.internal.testFramework.logging +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils.addAkka +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils.addJUL +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils.addKotlinAdapter +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils.addLog4J +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils.addSlf4J import com.intellij.jvm.analysis.testFramework.JvmInspectionTestBase abstract class LoggingInspectionTestBase : JvmInspectionTestBase() { override fun setUp() { super.setUp() - myFixture.addClass(""" - package org.slf4j.spi; - public interface LoggingEventBuilder { - LoggingEventBuilder addArgument(Object object); - LoggingEventBuilder setMessage(String message); - LoggingEventBuilder addKeyValue(String key, Object object); - void log(String format, Object... arguments); - void log(); - } - """.trimIndent()) - myFixture.addClass(""" - package org.slf4j; - import org.slf4j.spi.LoggingEventBuilder; - @SuppressWarnings("ALL") public class LoggerFactory { - public static Logger getLogger(Class clazz) { return null; } - public static Logger getLogger() { return null; } - } - public interface Logger { - void info(String format, Object... arguments); - void debug(String format, Object... arguments); - void warn(String format, Object... arguments); - void trace(String format, Object... arguments); - void error(String format, Object... arguments); - boolean isDebugEnabled(); - boolean isInfoEnabled(); - LoggingEventBuilder atInfo(); - LoggingEventBuilder atDebug(); - LoggingEventBuilder atWarn(); - LoggingEventBuilder atError(); - } - """.trimIndent()) - myFixture.addClass(""" - package org.apache.logging.log4j; - import org.apache.logging.log4j.util.Supplier; - public interface Logger { - boolean isDebugEnabled(); - boolean isInfoEnabled(); - boolean isWarnEnabled(); - void info(String message, Object... params); - void info(); - void debug(String message, Object... params); - void warn(String message, Object... params); - void error(String message, Object... params); - void trace(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(); - boolean isInfoEnabled(){return true;} - } - """.trimIndent()) - myFixture.addClass(""" - package org.apache.logging.log4j; - @SuppressWarnings("ALL") public class LogManager { - public static Logger getLogger() { - return null; - } - public static Logger getFormatterLogger() { - return null; - } - } - """.trimIndent()) - myFixture.addClass(""" - package org.apache.logging.log4j.util; - public interface Supplier { - T get(); - } - """.trimIndent()) - myFixture.addClass(""" - package org.apache.logging.log4j; - import org.apache.logging.log4j.util.Supplier; - public interface LogBuilder { - void log(String format); - void log(String format, Object p0); - void log(String format, Object... params); - void log(String format, Supplier... params); - } - """.trimIndent()) - myFixture.addClass(""" - package java.util.logging; - public class Logger { - public static Logger getLogger(String name) { - return null; - } - public void warning(String msg) {} - public boolean isLoggable(Level level) {} - } - """.trimIndent()) - myFixture.addClass(""" - package java.util.logging; - @SuppressWarnings("ALL") public class Level { - public static final Level FINE = new Level(); - public static final Level WARNING = new Level(); - } - """.trimIndent()) - myFixture.addClass(""" - package kotlin.jvm.functions; - public interface Function0 { - T invoke(); - } - """.trimIndent()) - myFixture.addClass(""" - package akka.event; - public interface LoggingAdapter { - void info(final String template, final Object arg1, final Object arg2); - void log(final int level, final String template, final Object arg1, final Object arg2, final Object arg3); - void log(final int level, final String template, final Object arg1, final Object arg2); - void log(final int level, final String template, final Object arg1); - void error(final Throwable cause, final String template, final Object arg1); - void error(final Throwable cause, final String template, final Object arg1, final Object arg2); - void error(final Throwable cause, final String template, final Object arg1, final Object arg2, final Object arg3); - void error(final String template, final Object arg1); - void error(final String template, final Object arg1, final Object arg2); - void error(final String template, final Object arg1, final Object arg2, final Object arg3); - } - """.trimIndent()) + addSlf4J(myFixture) + addLog4J(myFixture) + addJUL(myFixture) + addKotlinAdapter(myFixture) + addAkka(myFixture) } } \ No newline at end of file diff --git a/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingTestUtils.kt b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingTestUtils.kt new file mode 100644 index 000000000000..724e583cc893 --- /dev/null +++ b/jvm/jvm-analysis-internal-testFramework/src/com/intellij/jvm/analysis/internal/testFramework/logging/LoggingTestUtils.kt @@ -0,0 +1,157 @@ +package com.intellij.jvm.analysis.internal.testFramework.logging + +import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture + +object LoggingTestUtils { + fun addAkka(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" + package akka.event; + public interface LoggingAdapter { + void info(final String template, final Object arg1, final Object arg2); + void log(final int level, final String template, final Object arg1, final Object arg2, final Object arg3); + void log(final int level, final String template, final Object arg1, final Object arg2); + void log(final int level, final String template, final Object arg1); + void error(final Throwable cause, final String template, final Object arg1); + void error(final Throwable cause, final String template, final Object arg1, final Object arg2); + void error(final Throwable cause, final String template, final Object arg1, final Object arg2, final Object arg3); + void error(final String template, final Object arg1); + void error(final String template, final Object arg1, final Object arg2); + void error(final String template, final Object arg1, final Object arg2, final Object arg3); + } + """.trimIndent()) + } + + fun addKotlinAdapter(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" + package kotlin.jvm.functions; + public interface Function0 { + T invoke(); + } + """.trimIndent()) + } + + fun addJUL(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" + package java.util.logging; + public class Logger { + public static Logger getLogger(String name) { + return null; + } + public void warning(String msg) {} + public boolean isLoggable(Level level) {} + } + """.trimIndent()) + fixture.addClass(""" + package java.util.logging; + @SuppressWarnings("ALL") public class Level { + public static final Level FINE = new Level(); + public static final Level WARNING = new Level(); + } + """.trimIndent()) + } + + fun addLog4J(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" + package org.apache.logging.log4j; + import org.apache.logging.log4j.util.Supplier; + public interface Logger { + boolean isDebugEnabled(); + boolean isInfoEnabled(); + boolean isWarnEnabled(); + void info(String message, Object... params); + void info(); + void debug(String message, Object... params); + void warn(String message, Object... params); + void error(String message, Object... params); + void trace(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(); + boolean isInfoEnabled(){return true;} + } + """.trimIndent()) + fixture.addClass(""" + package org.apache.logging.log4j; + @SuppressWarnings("ALL") public class LogManager { + public static Logger getLogger() { + return null; + } + public static Logger getFormatterLogger() { + return null; + } + } + """.trimIndent()) + fixture.addClass(""" + package org.apache.logging.log4j.util; + public interface Supplier { + T get(); + } + """.trimIndent()) + fixture.addClass(""" + package org.apache.logging.log4j; + import org.apache.logging.log4j.util.Supplier; + public interface LogBuilder { + void log(String format); + void log(String format, Object p0); + void log(String format, Object... params); + void log(String format, Supplier... params); + } + """.trimIndent()) + } + + fun addIdeaLog(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" +package com.intellij.openapi.diagnostic; + +public interface Logger { + void info(String format, Object... arguments); + void debug(String format, Object... arguments); + void warn(String format, Object... arguments); + void trace(String format, Object... arguments); + void error(String format, Object... arguments); +} +""".trimIndent()) + } + + fun addSlf4J(fixture: JavaCodeInsightTestFixture) { + fixture.addClass(""" + package org.slf4j.spi; + public interface LoggingEventBuilder { + LoggingEventBuilder addArgument(Object object); + LoggingEventBuilder setMessage(String message); + LoggingEventBuilder addKeyValue(String key, Object object); + void log(String format, Object... arguments); + void log(); + } + """.trimIndent()) + fixture.addClass(""" + package org.slf4j; + import org.slf4j.spi.LoggingEventBuilder; + @SuppressWarnings("ALL") public class LoggerFactory { + public static Logger getLogger(Class clazz) { return null; } + public static Logger getLogger() { return null; } + } + public interface Logger { + void info(String format, Object... arguments); + void debug(String format, Object... arguments); + void warn(String format, Object... arguments); + void trace(String format, Object... arguments); + void error(String format, Object... arguments); + boolean isDebugEnabled(); + boolean isInfoEnabled(); + LoggingEventBuilder atInfo(); + LoggingEventBuilder atDebug(); + LoggingEventBuilder atWarn(); + LoggingEventBuilder atError(); + } + """.trimIndent()) + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt new file mode 100644 index 000000000000..429d7c075c7e --- /dev/null +++ b/jvm/jvm-analysis-java-tests/testSrc/com/intellij/customization/console/console/JavaLogFinderHyperlinkTest.kt @@ -0,0 +1,208 @@ +package com.intellij.customization.console.console + +import com.intellij.jvm.analysis.internal.testFramework.internal.LogFinderHyperlinkTestBase +import com.intellij.jvm.analysis.internal.testFramework.internal.LogItem +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils +import com.intellij.openapi.editor.LogicalPosition + +class JavaLogFinderHyperlinkTest : LogFinderHyperlinkTestBase() { + fun testSimpleLog4j2() { + LoggingTestUtils.addLog4J(myFixture) + checkColumnFinderJava( + fileName = "Log4jk", + classText = """ +package com.example.loggingjava.java; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public final class Log4j { + + private static final Logger logger = LogManager.getLogger(); + + public static void main(String[] args) { + System.out.println("1"); + log4j(); + log4j2(1); + } + + private static void log4j2(int i) { + String message = "Second" + " Request {}" + i; + logger.error(message, i); + } + + private static void log4j() { + logger.info("First request"); + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("19:50:03.422 [main] INFO c.e.l.j.Log4j - First request", LogicalPosition(21, 8)), + LogItem("19:50:03.430 [main] ERROR com.example.loggingjava.java.Log4j - Second Request 11", LogicalPosition(17, 8)), + ) + ) + } + + fun testSimpleSlf4j() { + LoggingTestUtils.addSlf4J(myFixture) + checkColumnFinderJava( + fileName = "Slf4J", + classText = """ +package com.example.loggingjava.java; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class Slf4J { + private final static Logger log = LoggerFactory.getLogger(Slf4J.class); + + public static void main(String[] args) { + System.out.println(2); + log1(1); + log2(2); + } + + private static void log1(int i) { + String msg = getMsg(i); + log.info(msg); + } + + private static void log2(int i) { + String msg = "log2" + i; + log.info(msg); + } + + private static String getMsg(int i) { + return "test" + i; + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("20:37:13.351 [main] INFO com.example.loggingjava.java.Slf4J - test1", LogicalPosition(5, 19)), + LogItem("20:37:13.356 [main] INFO com.e.l.j.Slf4J - log22", LogicalPosition(21, 8)), + ) + ) + } + + fun testIdeaLog() { + LoggingTestUtils.addIdeaLog(myFixture) + checkColumnFinderJava( + fileName = "IdeaLog", + classText = """ +package com.example.loggingjava.java; + +import com.intellij.openapi.diagnostic.Logger; + +public final class IdeaLog { + + public static void start(Logger log) { + System.out.println(2); + log1(1, log); + log2(2, log); + } + + private static void log1(int i, Logger log) { + String msg = getMsg(i); + log.info(msg); + } + + private static void log2(int i, Logger log) { + String msg = "log2" + i; + log.info(msg); + } + + private static String getMsg(int i) { + return "test" + i; + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("20:37:13.351 [main] INFO com.example.loggingjava.java.IdeaLog - test1", LogicalPosition(4, 19)), + LogItem("20:37:13.356 [main] INFO com.e.l.j.IdeaLog - log22", LogicalPosition(19, 8)), + ) + ) + } + + fun testSlf4JFluentApi() { + LoggingTestUtils.addSlf4J(myFixture) + checkColumnFinderJava( + fileName = "Slf4JFluent", + classText = """ +package com.example.loggingjava.java; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class Slf4JFluent { + private final static Logger log = LoggerFactory.getLogger(Slf4JFluent.class); + + public static void main(String[] args) { + System.out.println(2); + log1(1); + log2(2); + } + + private static void log1(int i) { + String msg = "test" + i; + log.atInfo() + .setMessage(msg) + .log(); + } + + private static void log2(int i) { + String msg = "log2" + i; + log.atInfo() + .log(msg); + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("com.example.log.java.SomethingSimilarToClass", null), + LogItem("20:13:25.878 [main] INFO com.example.log.java.Slf4JFluent - test1", LogicalPosition(16, 8)), + LogItem("20:13:25.883 [main] INFO com.example.logging.java.Slf4JFluent - log22", LogicalPosition(23, 8)), + ) + ) + } + + fun testSkipException() { + LoggingTestUtils.addSlf4J(myFixture) + checkColumnFinderJava( + fileName = "EmptySpringApplication", + classText = """ +package org.example.emptyspring; + +import org.slf4j.Logger; + +public final class EmptySpringApplication { + + + private static final Logger log = org.slf4j.LoggerFactory.getLogger(EmptySpringApplication.class); + + public static void main(String[] args) { + request2("1"); + } + + private static void request2(Object number) { + log.info("new request {}", number); + throw new RuntimeException(); + } +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe", null), + LogItem("1", null), + LogItem("10:27:22.721 [main] INFO org.example.emptyspring.EmptySpringApplication -- new request 1\n", LogicalPosition(14, 8)), + LogItem("Exception in thread \"main\" java.lang.RuntimeException\n", null), + LogItem("\tat org.example.emptyspring.EmptySpringApplication.request2(EmptySpringApplication.java:16)\n", null), + ) + ) + } +} \ No newline at end of file diff --git a/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/customization/console/KotlinLogFinderHyperlinkTest.kt b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/customization/console/KotlinLogFinderHyperlinkTest.kt new file mode 100644 index 000000000000..4ab4771b6745 --- /dev/null +++ b/jvm/jvm-analysis-kotlin-tests/testSrc/com/intellij/customization/console/KotlinLogFinderHyperlinkTest.kt @@ -0,0 +1,106 @@ +package com.intellij.customization.console + +import com.intellij.jvm.analysis.internal.testFramework.internal.LogFinderHyperlinkTestBase +import com.intellij.jvm.analysis.internal.testFramework.internal.LogItem +import com.intellij.jvm.analysis.internal.testFramework.logging.LoggingTestUtils +import com.intellij.openapi.editor.LogicalPosition + +@Suppress("ConvertToStringTemplate") +class KotlinLogFinderHyperlinkTest : LogFinderHyperlinkTestBase() { + fun testSimpleLog4j2() { + LoggingTestUtils.addLog4J(myFixture) + checkColumnFinderKotlin( + fileName = "Log4jk", + classText = """ +package com.example.loggingjava.java + +import org.apache.logging.log4j.LogManager + +val logOutside = LogManager.getLogger() + +fun log() { + logOutside.info("top level logOutside") + Log4jk.log.info("top level logCompanion") +} + +class Log4jk { + fun log() { + logOutside.info("inside method logOutside") + logInside.info("inside method logInside") + log.info("inside method logCompanion") + } + + private val logInside = LogManager.getLogger() + + companion object { + val log = LogManager.getLogger() + + + fun log() { + logOutside.info("inside companion logOutside") + log.info("inside companion logCompanion") + } + } +} + +fun main() { + Log4jk().log() + Log4jk.log() + log() +} +""".trimIndent(), + logItems = listOf( + LogItem("java.exe AppMainV2 com.example.l.java.Log4jkKt", LogicalPosition(0, 0)), + LogItem("09:52:49.884 [main] INFO com.example.logg.java.Log4jkKt - inside method logOutside", LogicalPosition(13, 19)), + LogItem("09:52:49.888 [main] INFO com.e.log.java.Log4jk - inside method logInside", LogicalPosition(14, 18)), + LogItem("09:52:49.888 [main] INFO com.example.loggin.java.Log4jk - inside method logCompanion", LogicalPosition(15, 12)), + LogItem("09:52:49.888 [main] INFO com.example.logg.java.Log4jkKt - inside companion logOutside", LogicalPosition(25, 23)), + LogItem("09:52:49.888 [main] INFO com.example.log.j.Log4jk - inside companion logCompanion", LogicalPosition(26, 16)), + LogItem("09:52:49.888 [main] INFO com.e.loggingjava.java.Log4jkKt - top level logOutside", LogicalPosition(7, 15)), + LogItem("09:52:49.888 [main] INFO c.example.loggingjava.java.Log4jk - top level logCompanion", LogicalPosition(8, 15)), + ) + ) + } + + fun testTopLevelSlf4j() { + LoggingTestUtils.addSlf4J(myFixture) + checkColumnFinderKotlin( + fileName = "Slf4Jk", + classText = """ +package com.example.loggingjava.java + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +private val log: Logger = LoggerFactory.getLogger(Slf4Jk::class.java) + +private fun log1(i: Int) { + val msg = getMsg(i) + log.info(msg) +} + +private fun log2(i: Int) { + val msg = "log2" + i + log.info(msg) +} + +private fun getMsg(i: Int): String { + return "test" + i +} + +fun main(args: Array) { + println(2) + log1(1) + log2(2) +} + +object Slf4Jk +""".trimIndent(), + logItems = listOf( + LogItem("java.exe AppMainV2 com.example.l.java.Slf4JkKt", LogicalPosition(0, 0)), + LogItem("10:34:44.491 [main] INFO com.example.l.j.Slf4Jk - test1", LogicalPosition(27, 7)), + LogItem("10:34:44.495 [main] INFO com.example.l.j.Slf4Jk - log22", LogicalPosition(14, 8)), + ) + ) + } +} \ No newline at end of file