[java-inspections] IDEA-337706 Support conversion System.out to log calls

- quickfix for SystemOutErrInspection and ThrowablePrintedToSystemOutInspection
- StringConcatenationArgumentToLogCallInspection now uses String.valueOf for one argument (exception type)
- SystemOutErrInspection has info level
- ConvertSystemOutToLogCallFix doesn't highlight ThrowablePrintedToSystemOutInspection's problems

GitOrigin-RevId: f611e6ccb0e15e5b806ed776b88b12b097d3bb5d
This commit is contained in:
Mikhail Pyltsin
2024-04-03 11:41:32 +00:00
committed by intellij-monorepo-bot
parent 206d995dcd
commit 77cac2bb83
11 changed files with 930 additions and 182 deletions
@@ -245,6 +245,8 @@ use.obsolete.collection.type.problem.descriptor=Obsolete collection type <code>#
use.obsolete.collection.type.ignore.library.arguments.option=Ignore obsolete collection types where they are required
use.system.out.err.display.name=Use of 'System.out' or 'System.err'
use.system.out.err.problem.descriptor=Uses of <code>#ref</code> should probably be replaced with more robust logging #loc
use.system.out.err.problem.fix.err.option=Log method for 'System.err':
use.system.out.err.problem.fix.out.option=Log method for 'System.out':
dumpstack.call.display.name=Call to 'Thread.dumpStack()'
dumpstack.call.problem.descriptor=Call to <code>Thread.#ref()</code> should probably be replaced with more robust logging #loc
printstacktrace.call.display.name=Call to 'printStackTrace()'
@@ -1956,6 +1958,9 @@ invert.method.quickfix=Invert method
invert.quickfix=Invert ''{0}''
throwable.printed.to.system.out.display.name='Throwable' printed to 'System.out'
throwable.printed.to.system.out.problem.descriptor=''Throwable'' argument <code>#ref</code> to ''System.{0}.{1}()'' call
throwable.printed.to.system.out.problem.fix.level.option=Log method for fix:
convert.system.out.to.log.call.family.name=Convert ''System.out'' call to log call
convert.system.out.to.log.call.name=Convert ''System.out'' call to call of ''{0}''
suppress.for.tests.scope.quickfix=Suppress for 'Tests' scope
implicit.default.charset.usage.display.name=Implicit platform default charset
implicit.default.charset.usage.problem.descriptor=Call to <code>#ref()</code> uses the platform's default charset
@@ -1,91 +0,0 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.siyeh.ig.maturity;
import com.intellij.psi.*;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
/**
* @author Bas Leijdekkers
*/
public final class ThrowablePrintedToSystemOutInspection extends BaseInspection {
@NotNull
@Override
protected String buildErrorString(Object... infos) {
final String fieldName = (String)infos[0];
final String methodName = (String)infos[1];
return InspectionGadgetsBundle.message("throwable.printed.to.system.out.problem.descriptor", fieldName, methodName);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
public BaseInspectionVisitor buildVisitor() {
return new ThrowablePrintedToSystemOutVisitor();
}
private static class ThrowablePrintedToSystemOutVisitor extends BaseInspectionVisitor {
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
final PsiReferenceExpression methodExpression = expression.getMethodExpression();
@NonNls final String methodName = methodExpression.getReferenceName();
if (!"print".equals(methodName) && !"println".equals(methodName)) {
return;
}
final PsiExpressionList argumentList = expression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length != 1) {
return;
}
final PsiExpression argument = arguments[0];
if (!TypeUtils.expressionHasTypeOrSubtype(argument, CommonClassNames.JAVA_LANG_THROWABLE)) {
return;
}
final PsiMethod method = expression.resolveMethod();
if (method == null) {
return;
}
final PsiExpression qualifier = methodExpression.getQualifierExpression();
if (!(qualifier instanceof PsiReferenceExpression qualifierReference)) {
return;
}
final PsiElement target = qualifierReference.resolve();
if (!(target instanceof PsiField field)) {
return;
}
@NonNls final String fieldName = field.getName();
if (!"out".equals(fieldName) && !"err".equals(fieldName)) {
return;
}
final PsiClass aClass = field.getContainingClass();
if (aClass == null || !"java.lang.System".equals(aClass.getQualifiedName())) {
return;
}
registerError(argument, fieldName, methodName);
}
}
}
@@ -1412,7 +1412,7 @@
<!--group.names.code.maturity.issues-->
<localInspection groupPath="Java" language="JAVA" suppressId="UseOfSystemOutOrSystemErr" shortName="SystemOutErr" bundle="messages.InspectionGadgetsBundle"
key="use.system.out.err.display.name" groupBundle="messages.InspectionsBundle"
groupKey="group.names.code.maturity.issues" enabledByDefault="false" level="WARNING"
groupKey="group.names.code.maturity.issues" enabledByDefault="true" level="INFORMATION"
implementationClass="com.siyeh.ig.maturity.SystemOutErrInspection"/>
<localInspection groupPath="Java" language="JAVA" suppressId="CallToPrintStackTrace" shortName="ThrowablePrintStackTrace" bundle="messages.InspectionGadgetsBundle"
key="printstacktrace.call.display.name" groupBundle="messages.InspectionsBundle"
@@ -110,6 +110,10 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
if (isFormattedLog4J(logCall)) return null;
return getQuickFix(problemType, targetExpression);
}
public static @Nullable PsiUpdateModCommandQuickFix getQuickFix(@NotNull ProblemType problemType, @NotNull PsiExpression targetExpression) {
return switch (problemType) {
case CONCATENATION ->
StringConcatenationArgumentToLogCallFix.isAvailable(targetExpression) ? new StringConcatenationArgumentToLogCallFix() : null;
@@ -168,7 +172,11 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
return new StringConcatenationArgumentToLogCallVisitor();
}
private static class StringConcatenationArgumentToLogCallFix extends PsiUpdateModCommandQuickFix {
public interface EvaluatedStringFix {
void fix(@NotNull Project project, @NotNull PsiMethodCallExpression logCall);
}
private static class StringConcatenationArgumentToLogCallFix extends PsiUpdateModCommandQuickFix implements EvaluatedStringFix {
StringConcatenationArgumentToLogCallFix() { }
@@ -184,6 +192,11 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
if (!(grandParent instanceof PsiMethodCallExpression methodCallExpression)) {
return;
}
fix(project, methodCallExpression);
}
@Override
public void fix(@NotNull Project project, @NotNull PsiMethodCallExpression methodCallExpression) {
final PsiExpressionList argumentList = methodCallExpression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length == 0) {
@@ -306,10 +319,16 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
newMethodCall.append('}');
}
else {
for (PsiExpression newArgument : newArguments) {
newMethodCall.append(',');
if (newArgument != null) {
newMethodCall.append(newArgument.getText());
if (newArguments.size() == 1 && newArguments.get(0) != null &&
InheritanceUtil.isInheritor(newArguments.get(0).getType(), CommonClassNames.JAVA_LANG_THROWABLE)) {
newMethodCall.append(", String.valueOf(").append(newArguments.get(0).getText()).append(")");
}
else {
for (PsiExpression newArgument : newArguments) {
newMethodCall.append(',');
if (newArgument != null) {
newMethodCall.append(newArgument.getText());
}
}
}
}
@@ -351,7 +370,7 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
}
}
private static abstract class FormatArgumentToLogCallFix extends PsiUpdateModCommandQuickFix {
private static abstract class FormatArgumentToLogCallFix extends PsiUpdateModCommandQuickFix implements EvaluatedStringFix {
@NotNull
private final Map<TextRange, Integer> myTextMapping;
@@ -366,11 +385,7 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
if (!(element.getParent() instanceof PsiReferenceExpression referenceExpression &&
referenceExpression.getParent() instanceof PsiMethodCallExpression callExpression)) {
return;
}
public void fix(@NotNull Project project, @NotNull PsiMethodCallExpression callExpression) {
PsiExpression[] expressions = callExpression.getArgumentList().getExpressions();
if (expressions.length != 1) {
return;
@@ -395,6 +410,15 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
tracker.replace(callExpression, builder.toString());
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
if (!(element.getParent() instanceof PsiReferenceExpression referenceExpression &&
referenceExpression.getParent() instanceof PsiMethodCallExpression callExpression)) {
return;
}
fix(project, callExpression);
}
@NotNull
private String createNewArgumentsFromCall(@NotNull PsiMethodCallExpression formatCallExpression,
@NotNull CommentTracker tracker) {
@@ -432,7 +456,7 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
@Nullable
static LocalQuickFix create(@NotNull PsiExpression expression) {
static PsiUpdateModCommandQuickFix create(@NotNull PsiExpression expression) {
if (!(expression instanceof PsiMethodCallExpression callExpression)) {
return null;
}
@@ -487,7 +511,7 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
}
@Nullable
static LocalQuickFix create(@NotNull PsiExpression originalExpression) {
static PsiUpdateModCommandQuickFix create(@NotNull PsiExpression originalExpression) {
if (!(originalExpression instanceof PsiMethodCallExpression callExpression)) {
return null;
}
@@ -559,7 +583,7 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
}
}
private enum ProblemType {
public enum ProblemType {
CONCATENATION, STRING_FORMAT, MESSAGE_FORMAT
}
@@ -598,54 +622,73 @@ public final class StringConcatenationArgumentToLogCallInspection extends BaseIn
if (arguments.length == 0) {
return;
}
if (arguments.length == 1 && arguments[0] instanceof PsiMethodCallExpression callExpression) {
FormatDecode.FormatArgument formatArgument =
FormatDecode.FormatArgument.extract(callExpression, List.of("format"), List.of("String"), true);
if (formatArgument != null) {
registerMethodCallError(expression, ProblemType.STRING_FORMAT, expression, callExpression);
return;
}
if (MESSAGE_FORMAT_FORMAT.test(callExpression)) {
registerMethodCallError(expression, ProblemType.MESSAGE_FORMAT, expression, callExpression);
return;
}
}
PsiExpression argument = arguments[0];
if (!ExpressionUtils.hasStringType(argument)) {
if (arguments.length < 2) {
return;
}
argument = arguments[1];
if (!ExpressionUtils.hasStringType(argument)) {
return;
}
}
if (!containsNonConstantConcatenation(argument)) {
return;
}
registerMethodCallError(expression, ProblemType.CONCATENATION, expression, argument);
}
LogConcatenationContext result = getLogConcatenationContext(arguments);
if (result == null) return;
private static boolean containsNonConstantConcatenation(@Nullable PsiExpression expression) {
if (expression instanceof PsiParenthesizedExpression parenthesizedExpression) {
return containsNonConstantConcatenation(parenthesizedExpression.getExpression());
}
else if (expression instanceof PsiPolyadicExpression polyadicExpression) {
if (!ExpressionUtils.hasStringType(polyadicExpression)) {
return false;
}
if (!JavaTokenType.PLUS.equals(polyadicExpression.getOperationTokenType())) {
return false;
}
final PsiExpression[] operands = polyadicExpression.getOperands();
for (PsiExpression operand : operands) {
if (!ExpressionUtils.isEvaluatedAtCompileTime(operand)) {
return true;
}
}
}
return false;
registerMethodCallError(expression, result.problemType(), expression, result.argument());
}
}
public record LogConcatenationContext(@NotNull PsiExpression argument, @NotNull ProblemType problemType) {
}
@Nullable
public static LogConcatenationContext getLogConcatenationContext(PsiExpression @NotNull [] arguments) {
PsiExpression argument = arguments[0];
ProblemType problemType = null;
if (arguments.length == 1 && argument instanceof PsiMethodCallExpression callExpression) {
FormatDecode.FormatArgument formatArgument =
FormatDecode.FormatArgument.extract(callExpression, List.of("format"), List.of("String"), true);
if (formatArgument != null) {
problemType = ProblemType.STRING_FORMAT;
}
else if (MESSAGE_FORMAT_FORMAT.test(callExpression)) {
problemType = ProblemType.MESSAGE_FORMAT;
}
}
if (problemType == null) {
if (!ExpressionUtils.hasStringType(argument)) {
if (arguments.length < 2) {
return null;
}
argument = arguments[1];
}
if (!ExpressionUtils.hasStringType(argument)) {
return null;
}
if (!containsNonConstantConcatenation(argument)) {
return null;
}
problemType = ProblemType.CONCATENATION;
}
return new LogConcatenationContext(argument, problemType);
}
private static boolean containsNonConstantConcatenation(@Nullable PsiExpression expression) {
if (expression instanceof PsiParenthesizedExpression parenthesizedExpression) {
return containsNonConstantConcatenation(parenthesizedExpression.getExpression());
}
else if (expression instanceof PsiPolyadicExpression polyadicExpression) {
if (!ExpressionUtils.hasStringType(polyadicExpression)) {
return false;
}
if (!JavaTokenType.PLUS.equals(polyadicExpression.getOperationTokenType())) {
return false;
}
final PsiExpression[] operands = polyadicExpression.getOperands();
for (PsiExpression operand : operands) {
if (!ExpressionUtils.isEvaluatedAtCompileTime(operand)) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,301 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.maturity;
import com.intellij.java.JavaBundle;
import com.intellij.lang.logging.JvmLogger;
import com.intellij.modcommand.*;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.VariableKind;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.ui.logging.JvmLoggingSettingsStorage;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.logging.StringConcatenationArgumentToLogCallInspection;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.VariableNameGenerator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
import static com.intellij.lang.logging.UnspecifiedLogger.UNSPECIFIED_LOGGER_ID;
import static com.intellij.psi.CommonClassNames.JAVA_IO_PRINT_STREAM;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_SYSTEM;
public class ConvertSystemOutToLogCallFix extends PsiBasedModCommandAction<PsiMethodCallExpression> {
private static final CallMatcher myCallMatcher = CallMatcher.instanceCall(JAVA_IO_PRINT_STREAM, "println", "print");
private static final Set<String> SUPPORTED_LOGGING_SYSTEMS = Set.of("Log4j2", "Slf4j", "Lombok Slf4j", "Lombok Log4j2");
@NotNull
private final List<JvmLogger> myLoggers;
@NotNull
private final String myMethodName;
@Nullable
private final String myLogName;
private ConvertSystemOutToLogCallFix(@NotNull List<JvmLogger> loggers,
@Nullable String logName,
@NotNull String method,
@NotNull PsiMethodCallExpression logCall) {
super(logCall);
myLoggers = loggers;
myMethodName = method;
myLogName = logName;
}
@Override
public @NotNull String getFamilyName() {
if (myLoggers.size() != 1) {
return InspectionGadgetsBundle.message("convert.system.out.to.log.call.family.name");
}
return InspectionGadgetsBundle.message("convert.system.out.to.log.call.name", myLoggers.get(0).toString());
}
@Override
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull PsiMethodCallExpression originalCall) {
if (myLoggers.isEmpty()) {
return ModCommand.nop();
}
if (myLogName != null) {
return ModCommand.psiUpdate(originalCall, logCall -> replaceWithLogCall(logCall, myLogName, myMethodName));
}
if (myLoggers.size() > 1) {
return ModCommand.chooseAction(JavaBundle.message("dialog.title.choose.logger"),
ContainerUtil.map(myLoggers,
log -> new ConvertSystemOutToLogCallFix(List.of(log), null, myMethodName,
originalCall)));
}
return ModCommand.psiUpdate(originalCall, callExpression -> {
JvmLogger logger = myLoggers.get(0);
PsiClass upperClass = getUpperClass(callExpression); //not always a good idea for all cases, but asking is too annoying
if (upperClass == null) {
return;
}
String logFieldName = logger.getLogFieldName(upperClass);
if (logFieldName == null) {
return;
}
Project project = context.project();
PsiElement loggerElement = logger.createLogger(project, upperClass);
if (loggerElement == null) {
return;
}
logger.insertLoggerAtClass(project, upperClass, loggerElement);
replaceWithLogCall(callExpression, logFieldName, myMethodName);
});
}
private static void replaceWithLogCall(@NotNull PsiMethodCallExpression callExpression,
@NotNull String logName,
@NotNull String logMethodName) {
PsiExpression[] expressions = callExpression.getArgumentList().getExpressions();
if(expressions.length != 1) {
return;
}
PsiExpression expression = expressions[0];
Project project = callExpression.getProject();
String arguments = getArguments(expression);
String text = logName + "." + logMethodName + "(" + arguments + ")";
CommentTracker tracker = new CommentTracker();
PsiElement newElement = tracker.replace(callExpression, text);
if (!(newElement instanceof PsiMethodCallExpression newCall)) {
return;
}
StringConcatenationArgumentToLogCallInspection.LogConcatenationContext context =
StringConcatenationArgumentToLogCallInspection.getLogConcatenationContext(newCall.getArgumentList().getExpressions());
if (context == null) {
return;
}
PsiUpdateModCommandQuickFix fix = StringConcatenationArgumentToLogCallInspection.getQuickFix(context.problemType(), context.argument());
if (fix instanceof StringConcatenationArgumentToLogCallInspection.EvaluatedStringFix evaluatedStringFix) {
evaluatedStringFix.fix(project, newCall);
}
}
@NotNull
private static String getArguments(@NotNull PsiExpression expression) {
PsiType expressionType = expression.getType();
if (expressionType==null ||expressionType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
return expression.getText();
}
if (InheritanceUtil.isInheritor(expressionType, CommonClassNames.JAVA_LANG_EXCEPTION)) {
return "\"e: \" , " + expression.getText();
}
return "String.valueOf(" + expression.getText() + ")";
}
@Nullable
public static ModCommandAction createFix(@NotNull PsiMethodCallExpression callExpression,
@NotNull String method) {
if (!myCallMatcher.matches(callExpression)) {
return null;
}
PsiReferenceExpression expressionPrint = callExpression.getMethodExpression();
if (!(expressionPrint.getQualifierExpression() instanceof PsiReferenceExpression expression)) {
return null;
}
String name = expression.getReferenceName();
if (!HardcodedMethodConstants.OUT.equals(name) &&
!HardcodedMethodConstants.ERR.equals(name)) {
return null;
}
PsiElement referent = expression.resolve();
if (!(referent instanceof PsiField systemField)) {
return null;
}
final PsiClass containingClass = systemField.getContainingClass();
if (containingClass == null) {
return null;
}
final String className = containingClass.getQualifiedName();
if (!JAVA_LANG_SYSTEM.equals(className)) {
return null;
}
if (callExpression.getArgumentList().getExpressions().length != 1) {
return null;
}
Module module = ModuleUtilCore.findModuleForFile(callExpression.getContainingFile());
if (module == null) {
return null;
}
List<JvmLogger> loggers = JvmLogger.Companion.findSuitableLoggers(module, true);
if (loggers.isEmpty()) {
return null;
}
PsiClass psiClass = getUpperClass(callExpression);
if (psiClass == null) {
return null;
}
Project project = callExpression.getProject();
GlobalSearchScope globalSearchScope = GlobalSearchScope.everythingScope(project);
List<JvmLogger> loggersWithTargetMethod = new ArrayList<>();
for (JvmLogger logger : loggers) {
PsiClass loggerClass = JavaPsiFacade.getInstance(project).findClass(logger.getLoggerTypeName(), globalSearchScope);
if (loggerClass != null && loggerClass.findMethodsByName(method, true).length > 0 &&
SUPPORTED_LOGGING_SYSTEMS.contains(logger.getId())) {
loggersWithTargetMethod.add(logger);
}
}
if (loggersWithTargetMethod.isEmpty()) {
return null;
}
List<PsiField> existedLoggers = new ArrayList<>();
PsiField[] fields = psiClass.getFields();
Set<String> allSupportedLoggers = loggersWithTargetMethod.stream().map(log -> log.getLoggerTypeName()).collect(Collectors.toSet());
for (PsiField field : fields) {
if (allSupportedLoggers.contains(field.getType().getCanonicalText())) {
existedLoggers.add(field);
}
}
if (!existedLoggers.isEmpty()) {
String possibleLogName = existedLoggers.get(0).getName();
PsiResolveHelper helper = JavaPsiFacade.getInstance(project).getResolveHelper();
PsiVariable variable = helper.resolveAccessibleReferencedVariable(possibleLogName, callExpression);
if (variable instanceof PsiField field &&
allSupportedLoggers.contains(field.getType().getCanonicalText())) {
return new ConvertSystemOutToLogCallFix(loggersWithTargetMethod, possibleLogName, method, callExpression);
}
return null;
}
List<JvmLogger> availableLoggers = new ArrayList<>();
for (JvmLogger logger: loggersWithTargetMethod) {
String logFieldName = logger.getLogFieldName(psiClass);
if (logFieldName == null) {
continue;
}
String proposedName = new VariableNameGenerator(callExpression, VariableKind.LOCAL_VARIABLE).byName(logFieldName)
.generate(true);
if (!proposedName.equals(logFieldName)) {
continue;
}
if (logger.isPossibleToPlaceLoggerAtClass(psiClass)) {
availableLoggers.add(logger);
}
}
if (availableLoggers.isEmpty()) {
return null;
}
JvmLoggingSettingsStorage loggingSettingsStorage = project.getService(JvmLoggingSettingsStorage.class);
JvmLoggingSettingsStorage.State state = loggingSettingsStorage.getState();
String id = state.getLoggerId();
if (UNSPECIFIED_LOGGER_ID.equals(id)) {
return new ConvertSystemOutToLogCallFix(availableLoggers, null, method, callExpression);
}
Optional<JvmLogger> chosenLogger = availableLoggers.stream().filter(log -> log.getId().equals(id)).findAny();
if (chosenLogger.isEmpty()) {
return null;
}
return new ConvertSystemOutToLogCallFix(chosenLogger.stream().toList(), null, method, callExpression);
}
private static @Nullable PsiClass getUpperClass(@NotNull PsiElement source) {
PsiElement target = source;
PsiElement next = source;
while (next != null) {
target = next;
next = PsiTreeUtil.findFirstParent(target, true,
parent -> parent instanceof PsiClass &&
!(parent instanceof PsiImplicitClass) &&
!(parent instanceof PsiAnonymousClass));
}
return target instanceof PsiClass ? (PsiClass)target : null;
}
public enum PopularLogLevel {
TRACE, DEBUG, INFO, WARN, ERROR;
@NlsSafe
public String toMethodName() {
return name().toLowerCase(Locale.ROOT);
}
}
}
@@ -2,28 +2,83 @@
package com.siyeh.ig.maturity;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.modcommand.ModCommandService;
import com.intellij.profile.codeInspection.InspectionProjectProfileManager;
import com.intellij.psi.*;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.fixes.SuppressForTestsScopeFix;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_SYSTEM;
/**
* @author Bas Leijdekkers
*/
public final class SystemOutErrInspection extends BaseInspection {
@Nullable
public ConvertSystemOutToLogCallFix.PopularLogLevel myErrLogLevel = ConvertSystemOutToLogCallFix.PopularLogLevel.ERROR;
public ConvertSystemOutToLogCallFix.PopularLogLevel myOutLogLevel = ConvertSystemOutToLogCallFix.PopularLogLevel.INFO;
@Override
protected LocalQuickFix buildFix(Object... infos) {
public @NotNull OptPane getOptionsPane() {
return OptPane.pane(
OptPane.dropdown(
"myErrLogLevel",
InspectionGadgetsBundle.message("use.system.out.err.problem.fix.err.option"),
ConvertSystemOutToLogCallFix.PopularLogLevel.class,
level -> level.toMethodName()),
OptPane.dropdown(
"myOutLogLevel",
InspectionGadgetsBundle.message("use.system.out.err.problem.fix.out.option"),
ConvertSystemOutToLogCallFix.PopularLogLevel.class,
level -> level.toMethodName())
);
}
@Override
protected LocalQuickFix @NotNull [] buildFixes(Object... infos) {
List<LocalQuickFix> fixes = new ArrayList<>();
final PsiElement context = (PsiElement)infos[0];
return SuppressForTestsScopeFix.build(this, context);
SuppressForTestsScopeFix testsScopeFix = SuppressForTestsScopeFix.build(this, context);
if(testsScopeFix != null) {
fixes.add(testsScopeFix);
}
if (context instanceof PsiReferenceExpression referenceExpression &&
referenceExpression.getParent() instanceof PsiReferenceExpression probablyReferenceToCall &&
probablyReferenceToCall.getParent() instanceof PsiMethodCallExpression callExpression) {
String name = referenceExpression.getReferenceName();
String methodName = null;
if (HardcodedMethodConstants.OUT.equals(name)) {
methodName = myOutLogLevel.name().toLowerCase(Locale.ROOT);
}
else if (HardcodedMethodConstants.ERR.equals(name)) {
methodName = myErrLogLevel.name().toLowerCase(Locale.ROOT);
}
if (methodName != null) {
ModCommandAction fix = ConvertSystemOutToLogCallFix.createFix(callExpression, methodName);
if (fix != null) {
LocalQuickFix localQuickFix = ModCommandService.getInstance().wrapToQuickFix(fix);
fixes.add(localQuickFix);
}
}
}
return fixes.toArray(LocalQuickFix.EMPTY_ARRAY);
}
@Override
@@ -44,11 +99,28 @@ public final class SystemOutErrInspection extends BaseInspection {
return new SystemOutErrVisitor();
}
private static class SystemOutErrVisitor extends BaseInspectionVisitor {
private class SystemOutErrVisitor extends BaseInspectionVisitor {
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
PsiReferenceExpression methodExpression = expression.getMethodExpression();
if (methodExpression.getQualifierExpression() instanceof PsiReferenceExpression referenceExpression) {
inspectReferenceIfItIsSystemOutErr(referenceExpression);
}
}
@Override
public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
super.visitReferenceExpression(expression);
if (expression.getParent() instanceof PsiReferenceExpression parentReferenceExpression &&
parentReferenceExpression.getParent() instanceof PsiMethodCallExpression) {
return;
}
inspectReferenceIfItIsSystemOutErr(expression);
}
private void inspectReferenceIfItIsSystemOutErr(@NotNull PsiReferenceExpression expression) {
final String name = expression.getReferenceName();
if (!HardcodedMethodConstants.OUT.equals(name) &&
!HardcodedMethodConstants.ERR.equals(name)) {
@@ -63,9 +135,23 @@ public final class SystemOutErrInspection extends BaseInspection {
return;
}
final String className = containingClass.getQualifiedName();
if (!"java.lang.System".equals(className)) {
if (!JAVA_LANG_SYSTEM.equals(className)) {
return;
}
if (expression.getParent() instanceof PsiReferenceExpression probablyReferenceToCall &&
probablyReferenceToCall.getParent() instanceof PsiMethodCallExpression callExpression) {
if (ThrowablePrintedToSystemOutInspection.getExceptionIsPrintedToSystemOutResult(callExpression) != null) {
return;
}
boolean informationLevel = InspectionProjectProfileManager.isInformationLevel(getShortName(), expression);
if (informationLevel) {
registerError(callExpression, expression);
}
}
registerError(expression, expression);
}
}
@@ -0,0 +1,131 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.maturity;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.modcommand.ModCommandService;
import com.intellij.psi.*;
import com.siyeh.HardcodedMethodConstants;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
import static com.intellij.psi.CommonClassNames.JAVA_LANG_SYSTEM;
/**
* @author Bas Leijdekkers
*/
public final class ThrowablePrintedToSystemOutInspection extends BaseInspection {
public ConvertSystemOutToLogCallFix.PopularLogLevel myLogLevel = ConvertSystemOutToLogCallFix.PopularLogLevel.ERROR;
@Override
public @NotNull OptPane getOptionsPane() {
return OptPane.pane(
OptPane.dropdown(
"myLogLevel",
InspectionGadgetsBundle.message("throwable.printed.to.system.out.problem.fix.level.option"),
ConvertSystemOutToLogCallFix.PopularLogLevel.class,
level -> level.toMethodName())
);
}
@NotNull
@Override
protected String buildErrorString(Object... infos) {
final String fieldName = (String)infos[0];
final String methodName = (String)infos[1];
return InspectionGadgetsBundle.message("throwable.printed.to.system.out.problem.descriptor", fieldName, methodName);
}
@Override
public boolean isEnabledByDefault() {
return true;
}
@Override
public BaseInspectionVisitor buildVisitor() {
return new ThrowablePrintedToSystemOutVisitor();
}
@Override
protected @Nullable LocalQuickFix buildFix(Object... infos) {
if (infos.length != 3) {
return null;
}
if (!(infos[2] instanceof PsiExpression expression &&
expression.getParent() instanceof PsiExpressionList expressionList &&
expressionList.getParent() instanceof PsiMethodCallExpression call)) {
return null;
}
ModCommandAction fix = ConvertSystemOutToLogCallFix.createFix(call, myLogLevel.name().toLowerCase(Locale.ROOT));
if (fix != null) {
return ModCommandService.getInstance().wrapToQuickFix(fix);
}
return null;
}
private static class ThrowablePrintedToSystemOutVisitor extends BaseInspectionVisitor {
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
ExceptionIsPrintedToSystemOutResult result = getExceptionIsPrintedToSystemOutResult(expression);
if (result == null) return;
registerError(result.argument(), result.fieldName(), result.methodName(), result.argument());
}
}
public static @Nullable ExceptionIsPrintedToSystemOutResult getExceptionIsPrintedToSystemOutResult(@NotNull PsiMethodCallExpression expression) {
final PsiReferenceExpression methodExpression = expression.getMethodExpression();
@NonNls final String methodName = methodExpression.getReferenceName();
if (!"print".equals(methodName) && !"println".equals(methodName)) {
return null;
}
final PsiExpressionList argumentList = expression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length != 1) {
return null;
}
final PsiExpression argument = arguments[0];
if (!TypeUtils.expressionHasTypeOrSubtype(argument, CommonClassNames.JAVA_LANG_THROWABLE)) {
return null;
}
final PsiMethod method = expression.resolveMethod();
if (method == null) {
return null;
}
final PsiExpression qualifier = methodExpression.getQualifierExpression();
if (!(qualifier instanceof PsiReferenceExpression qualifierReference)) {
return null;
}
final PsiElement target = qualifierReference.resolve();
if (!(target instanceof PsiField field)) {
return null;
}
@NonNls final String fieldName = field.getName();
if (!HardcodedMethodConstants.OUT.equals(fieldName) && !HardcodedMethodConstants.ERR.equals(fieldName)) {
return null;
}
final PsiClass aClass = field.getContainingClass();
if (aClass == null || !JAVA_LANG_SYSTEM.equals(aClass.getQualifiedName())) {
return null;
}
return new ExceptionIsPrintedToSystemOutResult(methodName, argument, fieldName);
}
public record ExceptionIsPrintedToSystemOutResult(@NotNull String methodName,
@NotNull PsiExpression argument,
@NotNull String fieldName) {
}
}
@@ -3,6 +3,28 @@
Reports usages of <code>System.out</code> or <code>System.err</code>.
<p>Such statements are often used for temporary debugging and should be either removed from the production code, or replaced by a more robust
logging facility.</p>
<p>The provided quick-fix supports <b>SLF4J</b> and <b>Log4j 2</b>.
It replaces <code>System.out</code> and <code>System.err</code> calls with log calls</p>
<p><b>Example:</b></p>
<pre><code>
public static void test(Object o) {
System.out.println("Test: "+ o);
}
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
private static final Logger log = LoggerFactory.getLogger(Test.class);
public static void test(Object o) {
log.info("Test: {}", o);;
}
</code></pre>
<!-- tooltip end -->
<p>
Use the <b>Log method for 'System.err'</b> option to specify a method which it is used to log a message for 'System.err' calls.
Use the <b>Log method for 'System.out'</b> option to specify a method which it is used to log a message for 'System.out' calls.
</p>
</body>
</html>
@@ -5,8 +5,9 @@ Reports calls to <code>System.out.println()</code> with an exception as an argum
It is recommended that you use logger instead.</p>
<p>Calls to <code>System.out.print()</code>, <code>System.err.println()</code>, and <code>System.err.print()</code> with an exception argument are also
reported. It is better to use a logger to log exceptions instead.</p>
<!-- tooltip end -->
<p>For example, instead of:</p>
<p>The provided quick-fix supports <b>SLF4J</b> and <b>Log4j 2</b>.
It replaces <code>System.out.println()</code> call with log calls</p>
<p><b>Example:</b></p>
<pre><code>
try {
foo();
@@ -14,14 +15,17 @@ reported. It is better to use a logger to log exceptions instead.</p>
System.out.println(e);
}
</code></pre>
<p>use the following code:</p>
<p>After the quick-fix is applied:</p>
<pre><code>
try {
foo();
} catch (Exception e) {
logger.warn(e); // logger call may be different
log.error("e: ", e);
}
</code></pre>
<!-- tooltip end -->
<p>
Use the <b>Log method for fix</b> option to specify a method which it is used to log a message.
</p>
</body>
</html>
@@ -36,6 +36,190 @@ public class SystemOutErrInspectionTest extends LightJavaInspectionTestCase {
"}");
}
private void addSlf4j() {
addEnvironmentClass("""
package org.slf4j;
@SuppressWarnings("ALL") public class LoggerFactory {
public static Logger getLogger(Class clazz) { return null; }
}
public interface Logger {
void error(String format, Object... arguments);
void info(String format, Object... arguments);
}
""");
}
private void addMessageFormat() {
addEnvironmentClass("""
package java.text;
public final class MessageFormat{
public static String format(String format, Object... arguments) {return format;}
}
""");
}
public void testSimpleLogFix() {
addSlf4j();
doTest(
"""
class Test{
void foo(Object o) {
/*Uses of 'System.out' should probably be replaced with more robust logging*/System.out<caret>/**/.println("Something " + o);
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Object o) {
log.info("Something {}", o);
}
}
""");
}
public void testSimpleExistedLogFix() {
addSlf4j();
doTest(
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Exception o) {
/*Uses of 'System.out' should probably be replaced with more robust logging*/System.out<caret>/**/.println("Something " + o);
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Exception o) {
log.info("Something {}", String.valueOf(o));
}
}
""");
}
public void testSimpleExistedLogWithRecordFix() {
addSlf4j();
doTest(
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
record R(){}
void foo(R r) {
/*Uses of 'System.out' should probably be replaced with more robust logging*/System.out<caret>/**/.println(r);
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
record R(){}
void foo(R r) {
log.info(String.valueOf(r));
}
}
""");
}
public void testSimpleExistedLogWithStringFormatFix() {
addSlf4j();
doTest(
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Object r) {
/*Uses of 'System.out' should probably be replaced with more robust logging*/System.out<caret>/**/.println(String.format("test %s test", r));
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Object r) {
log.info("test {} test", r);
}
}
""");
}
public void testSimpleExistedLogWithMessageFormatFix() {
addSlf4j();
addMessageFormat();
doTest(
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Object r) {
/*Uses of 'System.out' should probably be replaced with more robust logging*/System.out<caret>/**/.println(MessageFormat.format("test {0} test", r));
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo(Object r) {
log.info("test {} test", r);
}
}
""");
}
@Override
protected InspectionProfileEntry getInspection() {
return new SystemOutErrInspection();
@@ -1,18 +1,4 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.siyeh.ig.maturity;
import com.intellij.codeInspection.InspectionProfileEntry;
@@ -36,6 +22,83 @@ public class ThrowablePrintedToSystemOutInspectionTest extends LightJavaInspecti
doStatementTest("System.out.println(/*'Throwable' argument 'new RuntimeException()' to 'System.out.println()' call*/new RuntimeException()/**/);");
}
@SuppressWarnings("ThrowableNotThrown")
public void testSimpleLogFix() {
addSlf4j();
doTest(
"""
class Test{
void foo() {
final RuntimeException x = new RuntimeException();
System.out.println(/*'Throwable' argument 'x' to 'System.out.println()' call*/x<caret>/**/);
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger log = LoggerFactory.getLogger(Test.class);
void foo() {
final RuntimeException x = new RuntimeException();
log.error("e: ", x);
}
}
""");
}
private void addSlf4j() {
addEnvironmentClass("""
package org.slf4j;
@SuppressWarnings("ALL") public class LoggerFactory {
public static Logger getLogger(Class clazz) { return null; }
}
public interface Logger {
void error(String format, Object... arguments);
}
""");
}
@SuppressWarnings("ThrowableNotThrown")
public void testSimpleExistedLogFix() {
addSlf4j();
doTest(
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger logger = LoggerFactory.getLogger(Test.class);
void foo() {
final RuntimeException x = new RuntimeException();
System.out.println(/*'Throwable' argument 'x' to 'System.out.println()' call*/x<caret>/**/);
}
}
""");
checkQuickFix("Convert 'System.out' call to call of 'Slf4j'",
"""
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class Test{
private static final Logger logger = LoggerFactory.getLogger(Test.class);
void foo() {
final RuntimeException x = new RuntimeException();
logger.error("e: ", x);
}
}
""");
}
@Override
protected InspectionProfileEntry getInspection() {
return new ThrowablePrintedToSystemOutInspection();