cleanup IDEA-382376: move com.siyeh.ig.logging package from java-impl to java-impl-inspections

GitOrigin-RevId: 58a24da795ae1b8a3a9875cd687a8c47abb67a27
This commit is contained in:
Bartek Pacia
2026-02-12 12:49:55 +00:00
committed by intellij-monorepo-bot
parent b924d3dcc4
commit 2a72828cba
6 changed files with 0 additions and 0 deletions
@@ -0,0 +1,107 @@
/*
* Copyright 2003-2021 Dave Griffith, Bas Leijdekkers
*
* 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.logging;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.PsiVariable;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.JavaLoggingUtils;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.options.OptPane.stringList;
public final class ClassWithMultipleLoggersInspection extends BaseInspection {
private final List<String> loggerNames = new ArrayList<>();
/**
* @noinspection PublicField
*/
public @NonNls String loggerNamesString = StringUtil.join(JavaLoggingUtils.DEFAULT_LOGGERS, ",");
public ClassWithMultipleLoggersInspection() {
parseString(loggerNamesString, loggerNames);
}
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
stringList("loggerNames", InspectionGadgetsBundle.message("logger.class.name"),
new JavaClassValidator()
.withTitle(InspectionGadgetsBundle.message("choose.logger.class")))
);
}
@Override
public @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("multiple.loggers.problem.descriptor");
}
@Override
public void readSettings(@NotNull Element element) throws InvalidDataException {
super.readSettings(element);
parseString(loggerNamesString, loggerNames);
}
@Override
public void writeSettings(@NotNull Element element) throws WriteExternalException {
loggerNamesString = formatString(loggerNames);
super.writeSettings(element);
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new ClassWithMultipleLoggersVisitor();
}
private class ClassWithMultipleLoggersVisitor extends BaseInspectionVisitor {
@Override
public void visitClass(@NotNull PsiClass aClass) {
if (aClass instanceof PsiTypeParameter) {
return;
}
int numLoggers = 0;
for (PsiField field : aClass.getFields()) {
if (isLogger(field)) {
numLoggers++;
}
}
if (numLoggers <= 1) {
return;
}
registerClassError(aClass);
}
private boolean isLogger(PsiVariable variable) {
return loggerNames.contains(variable.getType().getCanonicalText());
}
}
}
@@ -0,0 +1,142 @@
/*
* Copyright 2003-2021 Dave Griffith, Bas Leijdekkers
*
* 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.logging;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.JavaLoggingUtils;
import com.siyeh.ig.ui.ExternalizableStringSet;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import static com.intellij.codeInspection.options.OptPane.checkbox;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.options.OptPane.stringList;
import static com.intellij.codeInspection.options.OptPane.tab;
import static com.intellij.codeInspection.options.OptPane.tabs;
public final class ClassWithoutLoggerInspection extends BaseInspection {
private final List<String> loggerNames = new ArrayList<>();
/**
* @noinspection PublicField
*/
public @NonNls String loggerNamesString = StringUtil.join(JavaLoggingUtils.DEFAULT_LOGGERS, ",");
/**
* @noinspection PublicField
*/
public boolean ignoreSuperLoggers = false;
@SuppressWarnings("PublicField") public final ExternalizableStringSet annotations = new ExternalizableStringSet();
@SuppressWarnings("PublicField") public final ExternalizableStringSet ignoredClasses = new ExternalizableStringSet(CommonClassNames.JAVA_LANG_THROWABLE);
public ClassWithoutLoggerInspection() {
parseString(loggerNamesString, loggerNames);
}
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
tabs(
tab(InspectionGadgetsBundle.message("class.without.logger.loggers.tab"),
stringList("loggerNames", InspectionGadgetsBundle.message("logger.class.name"),
new JavaClassValidator().withTitle(InspectionGadgetsBundle.message("choose.logger.class")))),
tab(InspectionGadgetsBundle.message("options.title.ignored.classes"),
stringList("ignoredClasses", InspectionGadgetsBundle.message("ignored.class.hierarchies.border.title"),
new JavaClassValidator().withTitle(InspectionGadgetsBundle.message("choose.class.hierarchy.to.ignore.title"))),
checkbox("ignoreSuperLoggers", InspectionGadgetsBundle.message("super.class.logger.option"))),
tab(InspectionGadgetsBundle.message("class.without.logger.annotations.tab"),
stringList("annotations", InspectionGadgetsBundle.message("ignore.classes.annotated.by"),
new JavaClassValidator().annotationsOnly()))
)
);
}
@Override
public @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("no.logger.problem.descriptor");
}
@Override
public void readSettings(@NotNull Element element) throws InvalidDataException {
super.readSettings(element);
parseString(loggerNamesString, loggerNames);
}
@Override
public void writeSettings(@NotNull Element element) throws WriteExternalException {
loggerNamesString = formatString(loggerNames);
defaultWriteSettings(element, "annotations", "ignoredClasses");
annotations.writeSettings(element, "annotations");
ignoredClasses.writeSettings(element, "ignoredClasses");
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new ClassWithoutLoggerVisitor();
}
private class ClassWithoutLoggerVisitor extends BaseInspectionVisitor {
@Override
public void visitClass(@NotNull PsiClass aClass) {
if (aClass.isInterface() || aClass.isEnum() || aClass.isAnnotationType() || aClass.getContainingClass() != null) {
return;
}
if (aClass instanceof PsiTypeParameter || aClass instanceof PsiAnonymousClass) {
return;
}
if (ignoredClasses.stream().anyMatch(ignoredClass -> InheritanceUtil.isInheritor(aClass, ignoredClass))) {
return;
}
if (AnnotationUtil.isAnnotated(aClass, annotations, AnnotationUtil.CHECK_EXTERNAL | AnnotationUtil.CHECK_HIERARCHY)) {
return;
}
final PsiField[] fields = ignoreSuperLoggers ? aClass.getAllFields() : aClass.getFields();
if (Stream.of(fields).anyMatch(field -> isLogger(field) && PsiUtil.isAccessible(field, aClass, aClass))) {
return;
}
registerClassError(aClass);
}
private boolean isLogger(PsiVariable variable) {
final PsiType type = variable.getType();
final String text = type.getCanonicalText();
return loggerNames.contains(text);
}
}
}
@@ -0,0 +1,288 @@
// 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.logging;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInsight.options.JavaIdentifierValidator;
import com.intellij.codeInspection.CommonQuickFixBundle;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiClassObjectAccessExpression;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiTypeElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.xmlb.Accessor;
import com.intellij.util.xmlb.SerializationFilterBase;
import com.intellij.util.xmlb.XmlSerializer;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.psiutils.CommentTracker;
import one.util.streamex.StreamEx;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.codeInspection.options.OptPane.checkbox;
import static com.intellij.codeInspection.options.OptPane.column;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.options.OptPane.table;
public final class LoggerInitializedWithForeignClassInspection extends BaseInspection {
private static final @NonNls String DEFAULT_FACTORY_CLASS_NAMES =
// Log4J 1
"org.apache.log4j.Logger," +
// SLF4J
"org.slf4j.LoggerFactory," +
// Apache Commons Logging
"org.apache.commons.logging.LogFactory," +
// Java Util Logging
"java.util.logging.Logger," +
// Log4J 2
"org.apache.logging.log4j.LogManager";
private static final @NonNls String DEFAULT_FACTORY_METHOD_NAMES =
//Log4J 1
"getLogger," +
// SLF4J
"getLogger," +
// Apache Commons Logging
"getLog," +
// Java Util Logging
"getLogger," +
// Log4J 2
"getLogger";
private final List<String> loggerFactoryClassNames = new ArrayList<>();
private final List<String> loggerFactoryMethodNames = new ArrayList<>();
@SuppressWarnings("PublicField")
public String loggerClassName = DEFAULT_FACTORY_CLASS_NAMES;
@SuppressWarnings("PublicField")
public @NonNls String loggerFactoryMethodName = DEFAULT_FACTORY_METHOD_NAMES;
public boolean ignoreSuperClass = false;
public boolean ignoreNonPublicClasses = false;
public boolean ignoreNotFinalField = true;
{
parseString(loggerClassName, loggerFactoryClassNames);
parseString(loggerFactoryMethodName, loggerFactoryMethodNames);
}
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
table("",
column("loggerFactoryClassNames", InspectionGadgetsBundle.message("logger.factory.class.name"),
new JavaClassValidator()),
column("loggerFactoryMethodNames", InspectionGadgetsBundle.message("logger.factory.method.name"),
new JavaIdentifierValidator())),
checkbox("ignoreSuperClass", InspectionGadgetsBundle.message("logger.initialized.with.foreign.class.ignore.super.class.option")),
checkbox("ignoreNonPublicClasses",
InspectionGadgetsBundle.message("logger.initialized.with.foreign.class.ignore.non.public.classes.option")),
checkbox("ignoreNotFinalField",
InspectionGadgetsBundle.message("logger.initialized.with.foreign.class.ignore.not.final.field"))
);
}
@Override
protected @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("logger.initialized.with.foreign.class.problem.descriptor");
}
@Override
protected LocalQuickFix buildFix(Object... infos) {
return new LoggerInitializedWithForeignClassFix((String)infos[0]);
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new LoggerInitializedWithForeignClassVisitor();
}
@Override
public void readSettings(@NotNull Element element) throws InvalidDataException {
super.readSettings(element);
parseString(loggerClassName, loggerFactoryClassNames);
parseString(loggerFactoryMethodName, loggerFactoryMethodNames);
if (loggerFactoryClassNames.size() != loggerFactoryMethodNames.size() || loggerFactoryClassNames.isEmpty()) {
parseString(DEFAULT_FACTORY_CLASS_NAMES, loggerFactoryClassNames);
parseString(DEFAULT_FACTORY_METHOD_NAMES, loggerFactoryMethodNames);
}
}
@Override
public void writeSettings(@NotNull Element element) throws WriteExternalException {
loggerClassName = formatString(loggerFactoryClassNames);
loggerFactoryMethodName = formatString(loggerFactoryMethodNames);
if (loggerFactoryMethodName.equals(DEFAULT_FACTORY_METHOD_NAMES) && loggerClassName.equals(DEFAULT_FACTORY_CLASS_NAMES)) {
// to prevent changing inspection profile with new default, which is mistakenly always written because of bug in serialization below.
loggerFactoryMethodName = "getLogger," +
"getLogger," +
"getLog," +
"getLogger";
// these broken settings are restored correctly in readSettings()
}
XmlSerializer.serializeInto(this, element, new SerializationFilterBase() {
@Override
protected boolean accepts(@NotNull Accessor accessor, @NotNull Object bean, @Nullable Object beanValue) {
final @NonNls String factoryName = accessor.getName();
if ("loggerClassName".equals(factoryName) && DEFAULT_FACTORY_CLASS_NAMES.equals(beanValue)) return false;
if ("loggerFactoryMethodNames".equals(factoryName) && DEFAULT_FACTORY_METHOD_NAMES.equals(beanValue)) return false;
if ("ignoreSuperClass".equals(factoryName) && !ignoreSuperClass) return false;
if ("ignoreNonPublicClasses".equals(factoryName) && !ignoreNonPublicClasses) return false;
if ("ignoreNotFinalField".equals(factoryName) && ignoreNotFinalField) return false;
return true;
}
});
}
private static final class LoggerInitializedWithForeignClassFix extends PsiUpdateModCommandQuickFix {
private final String newClassName;
private LoggerInitializedWithForeignClassFix(String newClassName) {
this.newClassName = newClassName;
}
@Override
public @NotNull String getName() {
return CommonQuickFixBundle.message("fix.replace.with.x", newClassName+".class");
}
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("logger.initialized.with.foreign.class.fix.family.name");
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
if (!(element instanceof PsiClassObjectAccessExpression classObjectAccessExpression)) {
return;
}
PsiReplacementUtil.replaceExpression(classObjectAccessExpression, newClassName + ".class", new CommentTracker());
}
}
private class LoggerInitializedWithForeignClassVisitor extends BaseInspectionVisitor {
@Override
public void visitClassObjectAccessExpression(@NotNull PsiClassObjectAccessExpression expression) {
super.visitClassObjectAccessExpression(expression);
PsiElement parent = expression.getParent();
if (parent instanceof PsiReferenceExpression referenceExpression) {
if (!expression.equals(referenceExpression.getQualifierExpression())) {
return;
}
final @NonNls String name = referenceExpression.getReferenceName();
if (!"getName".equals(name)) {
return;
}
final PsiElement grandParent = referenceExpression.getParent();
if (!(grandParent instanceof PsiMethodCallExpression methodCallExpression)) {
return;
}
final PsiExpressionList list = methodCallExpression.getArgumentList();
if (!list.isEmpty()) {
return;
}
parent = methodCallExpression.getParent();
}
if (!(parent instanceof PsiExpressionList)) {
return;
}
final PsiElement grandParent = parent.getParent();
if (!(grandParent instanceof PsiMethodCallExpression methodCallExpression)) {
return;
}
final PsiExpressionList argumentList = methodCallExpression.getArgumentList();
final PsiExpression[] expressions = argumentList.getExpressions();
if (expressions.length != 1) {
return;
}
PsiClass containingClass = PsiUtil.getContainingClass(expression);
while (containingClass instanceof PsiAnonymousClass) {
containingClass = PsiUtil.getContainingClass(containingClass);
}
if (containingClass == null) {
return;
}
if (ignoreNonPublicClasses && !containingClass.hasModifierProperty(PsiModifier.PUBLIC)) {
return;
}
final String containingClassName = containingClass.getName();
if (containingClassName == null) {
return;
}
final PsiReferenceExpression methodExpression = methodCallExpression.getMethodExpression();
final String referenceName = methodExpression.getReferenceName();
final int index = loggerFactoryMethodNames.indexOf(referenceName);
if (index < 0) {
return;
}
final PsiMethod method = methodCallExpression.resolveMethod();
if (method == null) {
return;
}
final PsiClass aClass = method.getContainingClass();
if (aClass == null) {
return;
}
final String className = aClass.getQualifiedName();
if (className == null) {
return;
}
if(StreamEx.zip(loggerFactoryClassNames, loggerFactoryMethodNames, (cl, m)-> new Pair<>(cl, m))
.noneMatch(expected->expected.first.equals(className) && expected.second.equals(referenceName))) {
return;
}
if (ignoreNotFinalField) {
PsiField field = PsiTreeUtil.getParentOfType(methodCallExpression, PsiField.class);
if (field == null) return;
if (!field.hasModifierProperty(PsiModifier.FINAL)) return;
}
final PsiTypeElement operand = expression.getOperand();
final PsiClass initializerClass = PsiUtil.resolveClassInClassTypeOnly(operand.getType());
if (initializerClass == null) {
return;
}
if (containingClass.equals(initializerClass)) {
return;
}
if (ignoreSuperClass && containingClass.isInheritor(initializerClass, true) ||
PsiTreeUtil.isAncestor(initializerClass, containingClass, true)) {
if (isOnTheFly()) {
registerError(expression, ProblemHighlightType.INFORMATION, containingClassName);
}
return;
}
registerError(expression, containingClassName);
}
}
}
@@ -0,0 +1,125 @@
/*
* Copyright 2003-2019 Dave Griffith, Bas Leijdekkers
*
* 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.logging;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.PsiVariable;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.fixes.MakeFieldStaticFinalFix;
import com.siyeh.ig.psiutils.JavaLoggingUtils;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.options.OptPane.stringList;
public final class NonStaticFinalLoggerInspection extends BaseInspection {
private final List<String> loggerClassNames = new ArrayList<>();
@SuppressWarnings("PublicField")
public String loggerClassName = StringUtil.join(JavaLoggingUtils.DEFAULT_LOGGERS, ",");
public NonStaticFinalLoggerInspection() {
parseString(loggerClassName, loggerClassNames);
}
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
stringList("loggerClassNames", InspectionGadgetsBundle.message("logger.class.name"),
new JavaClassValidator().withTitle(InspectionGadgetsBundle.message("choose.logger.class"))));
}
@Override
public @NotNull String getID() {
return "NonConstantLogger";
}
@Override
protected @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("non.constant.logger.problem.descriptor");
}
@Override
protected LocalQuickFix buildFix(Object... infos) {
final PsiField field = (PsiField)infos[0];
return MakeFieldStaticFinalFix.buildFixUnconditional(field);
}
@Override
public void readSettings(@NotNull Element element) throws InvalidDataException {
super.readSettings(element);
parseString(loggerClassName, loggerClassNames);
}
@Override
public void writeSettings(@NotNull Element element) throws WriteExternalException {
loggerClassName = formatString(loggerClassNames);
super.writeSettings(element);
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new NonStaticFinalLoggerVisitor();
}
private class NonStaticFinalLoggerVisitor extends BaseInspectionVisitor {
@Override
public void visitClass(@NotNull PsiClass aClass) {
if (aClass.isInterface() || aClass.isEnum() || aClass.isAnnotationType()) {
return;
}
if (aClass instanceof PsiTypeParameter) {
return;
}
if (aClass.getContainingClass() != null) {
return;
}
final PsiField[] fields = aClass.getFields();
for (final PsiField field : fields) {
if (!isLogger(field)) {
continue;
}
if (field.hasModifierProperty(PsiModifier.STATIC) && field.hasModifierProperty(PsiModifier.FINAL)) {
continue;
}
registerFieldError(field, field);
}
}
private boolean isLogger(PsiVariable variable) {
final PsiType type = variable.getType();
final String text = type.getCanonicalText();
return loggerClassNames.contains(text);
}
}
}
@@ -0,0 +1,155 @@
/*
* Copyright 2003-2019 Dave Griffith, Bas Leijdekkers
*
* 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.logging;
import com.intellij.codeInsight.options.JavaClassValidator;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.openapi.util.InvalidDataException;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaRecursiveElementWalkingVisitor;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.util.PropertyUtil;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.psiutils.JavaLoggingUtils;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.intellij.codeInspection.options.OptPane.stringList;
public final class PublicMethodWithoutLoggingInspection extends BaseInspection {
final List<String> loggerClassNames = new ArrayList<>();
@SuppressWarnings("PublicField")
public String loggerClassName = StringUtil.join(JavaLoggingUtils.DEFAULT_LOGGERS, ",");
public PublicMethodWithoutLoggingInspection() {
parseString(loggerClassName, loggerClassNames);
}
@Override
public @NotNull OptPane getOptionsPane() {
return pane(
stringList("loggerClassNames", InspectionGadgetsBundle.message("logger.class.name"),
new JavaClassValidator().withTitle(InspectionGadgetsBundle.message("choose.logger.class"))));
}
@Override
public @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("public.method.without.logging.problem.descriptor");
}
@Override
public void readSettings(@NotNull Element element) throws InvalidDataException {
super.readSettings(element);
parseString(loggerClassName, loggerClassNames);
}
@Override
public void writeSettings(@NotNull Element element) throws WriteExternalException {
loggerClassName = formatString(loggerClassNames);
super.writeSettings(element);
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new PublicMethodWithoutLoggingVisitor();
}
private class PublicMethodWithoutLoggingVisitor extends BaseInspectionVisitor {
@Override
public void visitMethod(@NotNull PsiMethod method) {
//no drilldown
if (method.getNameIdentifier() == null) {
return;
}
if (!method.hasModifierProperty(PsiModifier.PUBLIC)) {
return;
}
final PsiCodeBlock body = method.getBody();
if (body == null) {
return;
}
if (method.isConstructor()) {
return;
}
if (PropertyUtil.isSimpleGetter(method) || PropertyUtil.isSimpleSetter(method)) {
return;
}
if (containsLoggingCall(body)) {
return;
}
registerMethodError(method);
}
private boolean containsLoggingCall(PsiCodeBlock block) {
final ContainsLoggingCallVisitor visitor = new ContainsLoggingCallVisitor();
block.accept(visitor);
return visitor.containsLoggingCall();
}
}
private class ContainsLoggingCallVisitor extends JavaRecursiveElementWalkingVisitor {
private boolean containsLoggingCall;
@Override
public void visitElement(@NotNull PsiElement element) {
if (containsLoggingCall) {
return;
}
super.visitElement(element);
}
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
if (containsLoggingCall) {
return;
}
super.visitMethodCallExpression(expression);
final PsiMethod method = expression.resolveMethod();
if (method == null) {
return;
}
final PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
final String containingClassName = containingClass.getQualifiedName();
if (containingClassName == null) {
return;
}
if (loggerClassNames.contains(containingClassName)) {
containsLoggingCall = true;
}
}
boolean containsLoggingCall() {
return containsLoggingCall;
}
}
}
@@ -0,0 +1,813 @@
// 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.logging;
import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.options.OptDropdown;
import com.intellij.codeInspection.options.OptPane;
import com.intellij.modcommand.ModPsiUpdater;
import com.intellij.modcommand.PsiUpdateModCommandQuickFix;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.WriteExternalException;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.CommonClassNames;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.JavaTokenType;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiConstantEvaluationHelper;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiExpression;
import com.intellij.psi.PsiExpressionList;
import com.intellij.psi.PsiLiteralExpression;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiParenthesizedExpression;
import com.intellij.psi.PsiPolyadicExpression;
import com.intellij.psi.PsiReferenceExpression;
import com.intellij.psi.PsiReturnStatement;
import com.intellij.psi.PsiTypes;
import com.intellij.psi.PsiVariable;
import com.intellij.psi.util.InheritanceUtil;
import com.intellij.psi.util.PsiLiteralUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.TypeConversionUtil;
import com.intellij.util.ThreeState;
import com.siyeh.InspectionGadgetsBundle;
import com.siyeh.ig.BaseInspection;
import com.siyeh.ig.BaseInspectionVisitor;
import com.siyeh.ig.PsiReplacementUtil;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.format.FormatDecode;
import com.siyeh.ig.format.MessageFormatUtil;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import one.util.streamex.EntryStream;
import one.util.streamex.StreamEx;
import org.jdom.Element;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import static com.intellij.codeInspection.options.OptPane.checkbox;
import static com.intellij.codeInspection.options.OptPane.dropdown;
import static com.intellij.codeInspection.options.OptPane.option;
import static com.intellij.codeInspection.options.OptPane.pane;
import static com.siyeh.ig.callMatcher.CallMatcher.anyOf;
import static com.siyeh.ig.callMatcher.CallMatcher.staticCall;
/**
* @author Bas Leijdekkers
*/
public final class StringConcatenationArgumentToLogCallInspection extends BaseInspection {
private static final @NonNls Set<String> logNames = Set.of(
"trace",
"debug",
"info",
"warn",
"error",
"fatal",
"log"
);
private static final String LOG4J_LOGGER = "org.apache.logging.log4j.Logger";
private static final String LOG4J_BUILDER = "org.apache.logging.log4j.LogBuilder";
private static final CallMatcher GET_FORMATTER_LOGGER = staticCall("org.apache.logging.log4j.LogManager", "getFormatterLogger") ;
private static final CallMatcher GET_LOGGER = staticCall("org.apache.logging.log4j.LogManager", "getLogger");
private static final CallMatcher MESSAGE_FORMAT_FORMAT = anyOf(
staticCall("java.text.MessageFormat", "format").parameterCount(2)
);
private static final String SLF4J_LOGGER = "org.slf4j.Logger";
@SuppressWarnings("PublicField") public int warnLevel = 0;
/**
* @noinspection PublicField
*/
public boolean isLog4JParameterizedLogger = true;
@Override
public @NotNull OptPane getOptionsPane() {
@Nls String[] options = {
InspectionGadgetsBundle.message("all.levels.option"),
InspectionGadgetsBundle.message("warn.level.and.lower.option"),
InspectionGadgetsBundle.message("info.level.and.lower.option"),
InspectionGadgetsBundle.message("debug.level.and.lower.option"),
InspectionGadgetsBundle.message("trace.level.option")
};
return pane(
dropdown("warnLevel", InspectionGadgetsBundle.message("warn.on.label"),
EntryStream.of(options).mapKeyValue((idx, name) -> option(String.valueOf(idx), name))
.toArray(OptDropdown.Option.class)),
checkbox("isLog4JParameterizedLogger",
InspectionGadgetsBundle.message("log4j.use.parameterized.logger"))
.description(InspectionGadgetsBundle.message("log4j.use.parameterized.logger.description"))
);
}
@Override
protected @NotNull String buildErrorString(Object... infos) {
return InspectionGadgetsBundle.message("string.concatenation.argument.to.log.call.problem.descriptor");
}
@Override
public void writeSettings(@NotNull Element node) throws WriteExternalException {
if (warnLevel != 0) {
node.addContent(new Element("option").setAttribute("name", "warnLevel").setAttribute("value", String.valueOf(warnLevel)));
}
if (!isLog4JParameterizedLogger) {
node.addContent(new Element("option").setAttribute("name", "isLog4JParameterizedLogger")
.setAttribute("value", "false"));
}
}
@Override
protected @Nullable LocalQuickFix buildFix(Object... infos) {
if (!(infos[0] instanceof ProblemType problemType)) {
return null;
}
if (!(infos[1] instanceof PsiMethodCallExpression logCall)) {
return null;
}
if (!(infos[2] instanceof PsiExpression targetExpression)) {
return null;
}
ThreeState formattedLog4J = isFormattedLog4J(logCall);
if (formattedLog4J == ThreeState.YES) return null;
if (!isLog4JParameterizedLogger && formattedLog4J == ThreeState.UNSURE) 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;
case STRING_FORMAT -> StringFormatArgumentToLogCallFix.create(targetExpression);
case MESSAGE_FORMAT -> MessageFormatArgumentToLogCallFix.create(targetExpression);
};
}
private static ThreeState isFormattedLog4J(@NotNull PsiMethodCallExpression logCall) {
PsiExpression qualifierExpression = logCall.getMethodExpression().getQualifierExpression();
if (qualifierExpression == null) {
return ThreeState.NO;
}
boolean isLogBuilder = InheritanceUtil.isInheritor(qualifierExpression.getType(), LOG4J_BUILDER);
if (isLogBuilder || InheritanceUtil.isInheritor(qualifierExpression.getType(), LOG4J_LOGGER)) {
if (isLogBuilder) {
while (qualifierExpression != null &&
!InheritanceUtil.isInheritor(qualifierExpression.getType(), LOG4J_LOGGER)) {
if (qualifierExpression instanceof PsiMethodCallExpression nextCall) {
qualifierExpression = PsiUtil.skipParenthesizedExprDown(nextCall.getMethodExpression().getQualifierExpression());
}
else {
qualifierExpression = null;
}
}
}
if (qualifierExpression != null) {
if (qualifierExpression instanceof PsiMethodCallExpression callExpression) {
PsiMethod method = callExpression.resolveMethod();
if (method != null &&
method.getContainingFile() == qualifierExpression.getContainingFile() &&
(method.hasModifierProperty(PsiModifier.PRIVATE) ||
method.hasModifierProperty(PsiModifier.STATIC))) {
PsiReturnStatement[] statements = PsiUtil.findReturnStatements(method);
if (statements.length == 1) {
PsiReturnStatement statement = statements[0];
qualifierExpression = statement.getReturnValue();
}
}
}
if (qualifierExpression instanceof PsiReferenceExpression referenceExpression &&
referenceExpression.resolve() instanceof PsiVariable loggerVariable) {
if (loggerVariable.getInitializer() instanceof PsiMethodCallExpression callExpression) {
if (GET_FORMATTER_LOGGER.test(callExpression)) {
return ThreeState.YES;
}
if (GET_LOGGER.test(callExpression)) {
return ThreeState.NO;
}
}
}
if (qualifierExpression instanceof PsiMethodCallExpression callExpression) {
if (GET_FORMATTER_LOGGER.test(callExpression)) {
return ThreeState.YES;
}
if (GET_LOGGER.test(callExpression)) {
return ThreeState.NO;
}
}
}
return ThreeState.UNSURE;
}
return ThreeState.NO;
}
@Override
public @NotNull BaseInspectionVisitor buildVisitor() {
return new StringConcatenationArgumentToLogCallVisitor();
}
public interface EvaluatedStringFix {
void fix(@NotNull PsiMethodCallExpression logCall);
}
private static class StringConcatenationArgumentToLogCallFix extends PsiUpdateModCommandQuickFix implements EvaluatedStringFix {
StringConcatenationArgumentToLogCallFix() { }
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("string.concatenation.argument.to.log.call.quickfix");
}
@Override
protected void applyFix(@NotNull Project project, @NotNull PsiElement element, @NotNull ModPsiUpdater updater) {
final PsiElement grandParent = element.getParent().getParent();
if (!(grandParent instanceof PsiMethodCallExpression methodCallExpression)) {
return;
}
fix(methodCallExpression);
}
@Override
public void fix(@NotNull PsiMethodCallExpression methodCallExpression) {
final PsiExpressionList argumentList = methodCallExpression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length == 0) {
return;
}
final @NonNls StringBuilder newMethodCall = new StringBuilder(methodCallExpression.getMethodExpression().getText());
newMethodCall.append('(');
PsiExpression argument = arguments[0];
int usedArguments;
if (!(argument instanceof PsiPolyadicExpression)) {
if (!TypeUtils.expressionHasTypeOrSubtype(argument, "org.slf4j.Marker") || arguments.length < 2) {
return;
}
newMethodCall.append(argument.getText()).append(',');
argument = arguments[1];
usedArguments = 2;
if (!(argument instanceof PsiPolyadicExpression)) {
return;
}
}
else {
usedArguments = 1;
}
final PsiPolyadicExpression polyadicExpression = (PsiPolyadicExpression)argument;
final PsiMethod method = methodCallExpression.resolveMethod();
if (method == null) {
return;
}
final String methodName = method.getName();
final PsiClass containingClass = method.getContainingClass();
if (containingClass == null) {
return;
}
final PsiMethod[] methods = containingClass.findMethodsByName(methodName, false);
boolean varArgs = false;
for (PsiMethod otherMethod : methods) {
if (otherMethod.isVarArgs()) {
varArgs = true;
break;
}
}
final List<PsiExpression> newArguments = new ArrayList<>();
final PsiExpression[] operands = polyadicExpression.getOperands();
boolean addPlus = false;
boolean inStringLiteral = false;
boolean isStringBlock = false;
StringBuilder logText = new StringBuilder();
int indent = 0;
for (PsiExpression operand : operands) {
if (ExpressionUtils.isEvaluatedAtCompileTime(operand)) {
final String text = operand.getText();
if (ExpressionUtils.hasStringType(operand) && operand instanceof PsiLiteralExpression literalExpression) {
final int count = StringUtil.getOccurrenceCount(text, "{}");
for (int i = 0; i < count && usedArguments + i < arguments.length; i++) {
newArguments.add(PsiUtil.skipParenthesizedExprDown((PsiExpression)arguments[i + usedArguments].copy()));
}
usedArguments += count;
if (!inStringLiteral) {
if (addPlus) {
newMethodCall.append('+');
}
inStringLiteral = true;
}
if (!isStringBlock && literalExpression.isTextBlock()) {
indent = PsiLiteralUtil.getTextBlockIndent(literalExpression);
}
isStringBlock = isStringBlock || literalExpression.isTextBlock();
logText.append(literalExpression.getValue());
}
else if (operand instanceof PsiLiteralExpression && PsiTypes.charType().equals(operand.getType()) && inStringLiteral) {
final Object value = ((PsiLiteralExpression)operand).getValue();
if (value instanceof Character) {
logText.append(value);
}
}
else {
if (inStringLiteral) {
addLogStrings(newMethodCall, logText, isStringBlock, indent);
isStringBlock = false;
inStringLiteral = false;
}
if (addPlus) {
newMethodCall.append('+');
}
newMethodCall.append(text);
}
}
else {
newArguments.add(PsiUtil.skipParenthesizedExprDown((PsiExpression)operand.copy()));
if (!inStringLiteral) {
if (addPlus) {
newMethodCall.append('+');
}
inStringLiteral = true;
}
logText.append("{}");
}
addPlus = true;
}
while (usedArguments < arguments.length) {
newArguments.add(arguments[usedArguments++]);
}
if (inStringLiteral) {
addLogStrings(newMethodCall, logText, isStringBlock, indent);
}
if (!varArgs && newArguments.size() > 2) {
newMethodCall.append(", new Object[]{");
boolean comma = false;
for (PsiExpression newArgument : newArguments) {
if (comma) {
newMethodCall.append(',');
}
else {
comma = true;
}
if (newArgument != null) {
newMethodCall.append(newArgument.getText());
}
}
newMethodCall.append('}');
}
else {
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());
}
}
}
}
newMethodCall.append(')');
PsiReplacementUtil.replaceExpression(methodCallExpression, newMethodCall.toString());
}
private static void addLogStrings(StringBuilder methodCall, StringBuilder logText, boolean isStringBlock, int indent) {
if (!isStringBlock) {
methodCall.append('"')
.append(StringUtil.escapeStringCharacters(logText.toString()))
.append('"');
logText.delete(0, logText.length());
return;
}
String delimiters = "\n" + " ".repeat(indent);
String preparedText = StreamEx.of(logText.toString().split("\n", -1))
.map(line -> line.endsWith(" ") ? line.substring(0, line.length() - 1) + "\\s" : line)
.joining(delimiters, delimiters, "");
preparedText = PsiLiteralUtil.escapeTextBlockCharacters(preparedText, true, true, false);
methodCall.append("\"\"\"")
.append(preparedText)
.append("\"\"\"");
logText.delete(0, logText.length());
}
public static boolean isAvailable(PsiExpression expression) {
if (!(expression instanceof PsiPolyadicExpression polyadicExpression)) {
return false;
}
final PsiExpression[] operands = polyadicExpression.getOperands();
for (PsiExpression operand : operands) {
if (!ExpressionUtils.isEvaluatedAtCompileTime(operand)) {
return true;
}
}
return false;
}
}
private abstract static class FormatArgumentToLogCallFix extends PsiUpdateModCommandQuickFix implements EvaluatedStringFix {
private final @NotNull Map<TextRange, Integer> myTextMapping;
private final @NotNull String myFormat;
private final @NotNull Function<String, String> myTransformer;
private FormatArgumentToLogCallFix(@NotNull Map<TextRange, Integer> textMapping,
@NotNull String format,
@NotNull Function<String, String> transformer) {
myTextMapping = textMapping;
myFormat = format;
myTransformer = transformer;
}
@Override
public void fix(@NotNull PsiMethodCallExpression callExpression) {
PsiExpression[] expressions = callExpression.getArgumentList().getExpressions();
if (expressions.length < 1 || expressions.length > 2) {
return;
}
PsiExpression expression = expressions[0];
if (!(expression instanceof PsiMethodCallExpression formatCallExpression)) {
return;
}
StringBuilder builder = new StringBuilder();
CommentTracker tracker = new CommentTracker();
for (PsiElement child : callExpression.getChildren()) {
if (child instanceof PsiExpressionList expressionList) {
builder.append(createNewArgumentsFromCall(formatCallExpression, tracker, expressionList.getExpressions()));
}
else {
builder.append(tracker.text(child));
}
}
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(callExpression);
}
private @NotNull String createNewArgumentsFromCall(@NotNull PsiMethodCallExpression formatCallExpression,
@NotNull CommentTracker tracker,
PsiExpression @NotNull [] allArguments) {
List<String> arguments = new ArrayList<>();
List<Map.Entry<TextRange, Integer>> placeholders =
myTextMapping.entrySet()
.stream()
.sorted(Comparator.<Map.Entry<TextRange, Integer>>comparingInt(t -> t.getKey().getStartOffset()).reversed())
.toList();
String formatWithPlaceholders = myFormat;
PsiExpression[] expressions = formatCallExpression.getArgumentList().getExpressions();
for (Map.Entry<TextRange, Integer> placeholder : placeholders) {
formatWithPlaceholders = formatWithPlaceholders.substring(0, placeholder.getKey().getStartOffset()) + "{}" +
formatWithPlaceholders.substring(placeholder.getKey().getEndOffset());
arguments.add(tracker.text(expressions[placeholder.getValue()]));
}
arguments.add(myTransformer.apply(formatWithPlaceholders));
Collections.reverse(arguments);
if (allArguments.length > 1) {
for (int i = 1; i < allArguments.length; i++) {
arguments.add(tracker.text(allArguments[i]));
}
}
return "(" + String.join(", ", arguments) + ")";
}
}
private static class MessageFormatArgumentToLogCallFix extends FormatArgumentToLogCallFix {
private MessageFormatArgumentToLogCallFix(@NotNull Map<TextRange, Integer> result,
@NotNull String format) {
super(result, format, (s)->s);
}
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("string.concatenation.argument.to.log.message.format.call.quickfix");
}
static @Nullable PsiUpdateModCommandQuickFix create(@NotNull PsiExpression expression) {
if (!(expression instanceof PsiMethodCallExpression callExpression)) {
return null;
}
PsiExpression[] arguments = callExpression.getArgumentList().getExpressions();
if (arguments.length == 0) return null;
PsiExpression firstArgument = arguments[0];
if (firstArgument == null) return null;
TextInfo textInfo = getTextInfo(firstArgument);
if (textInfo == null) return null;
String pattern = textInfo.formattedString();
String text = textInfo.text();
if (pattern == null || text.isEmpty()) return null;
MessageFormatUtil.MessageFormatResult result = MessageFormatUtil.checkFormat(pattern);
if (!result.valid()) {
return null;
}
Map<TextRange, Integer> mapping = new HashMap<>();
List<MessageFormatUtil.MessageFormatPlaceholder> placeholders = result.placeholders();
for (MessageFormatUtil.MessageFormatPlaceholder placeholder : placeholders) {
if (!placeholder.isString()) {
return null;
}
if (placeholder.index() + 1 >= arguments.length) {
return null;
}
TextRange actualRange = ExpressionUtils.findStringLiteralRange(firstArgument, placeholder.range().getStartOffset(),
placeholder.range().getEndOffset());
if (actualRange == null) {
return null;
}
mapping.put(actualRange, placeholder.index() + 1);
}
Set<Integer> argumentIndexes = new HashSet<>(mapping.values());
if (argumentIndexes.size() != arguments.length - 1) {
return null;
}
return new MessageFormatArgumentToLogCallFix(mapping, text);
}
}
private record TextInfo(String text, String formattedString) {
}
private static @Nullable TextInfo getTextInfo(@NotNull PsiExpression expression) {
String text = null;
String formattedString = null;
if (expression instanceof PsiLiteralExpression literalExpression) {
if (!(literalExpression.getValue() instanceof String value)) {
return null;
}
formattedString = value;
text = literalExpression.getText();
}
if (expression instanceof PsiPolyadicExpression polyadicExpression && ExpressionUtils.hasStringType(polyadicExpression)) {
PsiConstantEvaluationHelper constantEvaluationHelper =
JavaPsiFacade.getInstance(expression.getProject()).getConstantEvaluationHelper();
Object o = constantEvaluationHelper.computeConstantExpression(polyadicExpression);
if (o instanceof String value) {
formattedString = value;
text = polyadicExpression.getText();
}
}
return new TextInfo(text, formattedString);
}
private static class StringFormatArgumentToLogCallFix extends FormatArgumentToLogCallFix {
@Override
public @NotNull String getFamilyName() {
return InspectionGadgetsBundle.message("string.concatenation.argument.to.log.string.format.call.quickfix");
}
private StringFormatArgumentToLogCallFix(@NotNull Map<TextRange, Integer> result,
@NotNull String format,
@NotNull Function<String, String> transformer) {
super(result, format, transformer);
}
static @Nullable PsiUpdateModCommandQuickFix create(@NotNull PsiExpression originalExpression) {
if (!(originalExpression instanceof PsiMethodCallExpression callExpression)) {
return null;
}
FormatDecode.FormatArgument formatArgument =
FormatDecode.FormatArgument.extract(callExpression, List.of("format"), List.of("String"), true);
if (formatArgument == null || formatArgument.getIndex() != 1) {
return null;
}
PsiExpression expression = formatArgument.getExpression();
if (expression == null) return null;
TextInfo textInfo = getTextInfo(expression);
if (textInfo == null) return null;
String formattedString = textInfo.formattedString();
String text = textInfo.text();
if (formattedString == null || text == null) {
return null;
}
PsiExpression[] arguments = Objects.requireNonNull(callExpression.getArgumentList()).getExpressions();
int argumentCount = arguments.length - formatArgument.getIndex();
FormatDecode.Validator[] validators;
try {
validators = FormatDecode.decodeNoVerify(formattedString, argumentCount);
}
catch (FormatDecode.IllegalFormatException e) {
return null;
}
if (argumentCount != validators.length) return null;
Map<TextRange, Integer> result = new HashMap<>();
for (int i = 0; i < validators.length; i++) {
int index = formatArgument.getIndex() + i;
if (index >= arguments.length) return null;
FormatDecode.Validator metaValidator = validators[i];
if (metaValidator == null) continue;
Collection<FormatDecode.Validator> unpacked = metaValidator instanceof FormatDecode.MultiValidator multi ?
multi.getValidators() : List.of(metaValidator);
if (unpacked.size() != 1) return null;
FormatDecode.Validator validator = unpacked.iterator().next();
if (validator == null) return null;
PsiExpression argument = arguments[index];
if (!possibleToConvert(validator, argument)) return null;
TextRange stringRange = validator.getRange();
if (stringRange == null) return null;
TextRange range = ExpressionUtils.findStringLiteralRange(expression, stringRange.getStartOffset(),
stringRange.getEndOffset());
if (range == null) return null;
result.put(range, index);
}
int start = 0;
while ((start = formattedString.indexOf("%n", start)) != -1) {
int escaped = 0;
while (true) {
if (start - escaped == 0) {
break;
}
if(formattedString.charAt(start - escaped - 1) == '%') {
escaped++;
continue;
}
break;
}
if (escaped % 2 == 1) {
start++;
continue;
}
TextRange range = ExpressionUtils.findStringLiteralRange(expression, start, start + 2);
if (range == null) {
return null;
}
text = StringUtil.replaceSubstring(text, range, "\\n");
start++;
}
return new StringFormatArgumentToLogCallFix(result, text, s -> s.replaceAll("%%", "%"));
}
private static boolean possibleToConvert(@NotNull FormatDecode.Validator validator, PsiExpression argument) {
FormatDecode.Spec spec = validator.getSpec();
if (spec == null) return false;
if (spec.conversion() == null ||
!StringUtil.isEmpty(spec.width()) ||
!StringUtil.isEmpty(spec.dateSpec()) ||
!StringUtil.isEmpty(spec.flags()) ||
!StringUtil.isEmpty(spec.precision())) {
return false;
}
return switch (spec.conversion()) {
case "s" -> true;
case "b" -> argument.getType() != null && TypeConversionUtil.isBooleanType(argument.getType());
case "d" -> argument.getType() != null && TypeConversionUtil.isIntegralNumberType(argument.getType());
default -> false;
};
}
}
public enum ProblemType {
CONCATENATION, STRING_FORMAT, MESSAGE_FORMAT
}
private class StringConcatenationArgumentToLogCallVisitor extends BaseInspectionVisitor {
@Override
public void visitMethodCallExpression(@NotNull PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
final PsiReferenceExpression methodExpression = expression.getMethodExpression();
final String referenceName = methodExpression.getReferenceName();
if (!logNames.contains(referenceName)) {
return;
}
switch (warnLevel) {
case 4:
if ("debug".equals(referenceName)) return;
case 3:
if ("info".equals(referenceName)) return;
case 2:
if ("warn".equals(referenceName)) return;
case 1:
if ("error".equals(referenceName) || "fatal".equals(referenceName)) return;
}
final PsiMethod method = expression.resolveMethod();
if (method == null) {
return;
}
final PsiClass containingClass = method.getContainingClass();
if (!InheritanceUtil.isInheritor(containingClass, SLF4J_LOGGER) &&
!InheritanceUtil.isInheritor(containingClass, LOG4J_LOGGER) &&
!InheritanceUtil.isInheritor(containingClass, LOG4J_BUILDER)) {
return;
}
final PsiExpressionList argumentList = expression.getArgumentList();
final PsiExpression[] arguments = argumentList.getExpressions();
if (arguments.length == 0) {
return;
}
LogConcatenationContext result = getLogConcatenationContext(arguments);
if (result == null) return;
registerMethodCallError(expression, result.problemType(), expression, result.argument());
}
}
public record LogConcatenationContext(@NotNull PsiExpression argument, @NotNull ProblemType problemType) {
}
public static @Nullable LogConcatenationContext getLogConcatenationContext(PsiExpression @NotNull [] arguments) {
PsiExpression argument = arguments[0];
ProblemType problemType = null;
if (argument instanceof PsiMethodCallExpression callExpression && (
arguments.length == 1 ||
(arguments.length == 2 && arguments[1] != null &&
InheritanceUtil.isInheritor(arguments[1].getType(), CommonClassNames.JAVA_LANG_THROWABLE))
)) {
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;
}
}