Merge pull request #3667 from adangel:issue-3644-invalidlogmessageformat

[java] InvalidLogMessageFormat - fix false positive with logstash #3667

* pr-3667:
  Use Set instead of array
  [java] InvalidLogMessageFormat - fix false positive with logstash
This commit is contained in:
Andreas Dangel 2022-01-20 10:58:32 +01:00
commit eadc469c39
No known key found for this signature in database
GPG Key ID: 93450DF2DF9A3FA3
3 changed files with 123 additions and 9 deletions

View File

@ -49,6 +49,7 @@ not support all features of the latest EcmaScript standard.
* java-design
* [#3679](https://github.com/pmd/pmd/issues/3679): \[java] Make FinalFieldCouldBeStatic detect constant variable
* java-errorprone
* [#3644](https://github.com/pmd/pmd/issues/3644): \[java] InvalidLogMessageFormat: false positives with logstash structured logging
* [#3686](https://github.com/pmd/pmd/issues/3686): \[java] ReturnEmptyCollectionRatherThanNull - false negative with conditioned returns
* [#3701](https://github.com/pmd/pmd/issues/3701): \[java] MissingStaticMethodInNonInstantiatableClass false positive with method inner classes
* java-performance

View File

@ -9,6 +9,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -48,6 +49,14 @@ public class InvalidLogMessageFormatRule extends AbstractJavaRule {
private static final Map<String, Set<String>> LOGGERS;
/**
* Whitelisted methods of net.logstash.logback.argument.StructuredArguments
*/
private static final Set<String> STRUCTURED_ARGUMENTS_METHODS = Collections.unmodifiableSet(new HashSet<>(
Arrays.asList("a", "array", "defer", "e",
"entries", "f", "fields", "keyValue",
"kv", "r", "raw", "v", "value")));
static {
Map<String, Set<String>> loggersMap = new HashMap<>();
@ -116,16 +125,25 @@ public class InvalidLogMessageFormatRule extends AbstractJavaRule {
final List<ASTExpression> argumentList = parentNode.getFirstChildOfType(ASTPrimarySuffix.class)
.getFirstDescendantOfType(ASTArgumentList.class).findChildrenOfType(ASTExpression.class);
if (argumentList.get(0).getType() != null && !argumentList.get(0).getType().equals(String.class)) {
if (argumentList.size() == 1) {
// no need to check for message params in case no string and no params found
return data;
// remove any arguments before the string message - these might be method calls for
// logstash markers or structured arguments
// this also removes any non-string value, e.g. a slf4j-Marker
// if the type cannot be determined, it is considered not to be a string...
Iterator<ASTExpression> iterator = argumentList.iterator();
while (iterator.hasNext()) {
ASTExpression argument = iterator.next();
if (!TypeTestUtil.isA(String.class, argument)) {
iterator.remove();
} else {
// ignore the first argument if it is a known non-string value, e.g. a slf4j-Marker
argumentList.remove(0);
break;
}
}
if (argumentList.isEmpty()) {
// no need to check for message params in case no string message found
return data;
}
// remove the message parameter
final ASTExpression messageParam = argumentList.remove(0);
@ -145,6 +163,12 @@ public class InvalidLogMessageFormatRule extends AbstractJavaRule {
if (argumentList.size() > expectedArguments) {
removeThrowableParam(argumentList);
}
// remove any logstash structured arguments at the end
// but only, if there are not enough placeholders
if (argumentList.size() > expectedArguments) {
removePotentialStructuredArguments(argumentList.size() - expectedArguments, argumentList);
}
int providedArguments = argumentList.size();
@ -319,4 +343,50 @@ public class InvalidLogMessageFormatRule extends AbstractJavaRule {
}
return stringLiterals;
}
/**
* Removes up to {@code maxArgumentsToRemove} arguments from the end of the {@code argumentList},
* if the argument is a method call to one of the whitelisted StructuredArguments methods.
*
* @param maxArgumentsToRemove
* @param argumentList
*/
private void removePotentialStructuredArguments(int maxArgumentsToRemove, List<ASTExpression> argumentList) {
int removed = 0;
while (!argumentList.isEmpty() && removed < maxArgumentsToRemove) {
int lastIndex = argumentList.size() - 1;
ASTExpression argument = argumentList.get(lastIndex);
if (isStructuredArgumentMethodCall(argument)) {
argumentList.remove(lastIndex);
removed++;
} else {
// stop if something else is encountered
break;
}
}
}
/*
* Expression == argument
* PrimaryExpression
* PrimaryPrefix
* Name: eg. "keyValue" or "StructuredArguments.keyValue"
* PrimarySuffix
* Arguments
*/
private boolean isStructuredArgumentMethodCall(ASTExpression argument) {
if (argument.getNumChildren() == 1 && argument.getChild(0) instanceof ASTPrimaryExpression) {
ASTPrimaryExpression primary = (ASTPrimaryExpression) argument.getChild(0);
if (primary.getNumChildren() == 2 && primary.getChild(1) instanceof ASTPrimarySuffix) {
ASTPrimaryPrefix prefix = (ASTPrimaryPrefix) primary.getChild(0);
ASTPrimarySuffix suffix = (ASTPrimarySuffix) primary.getChild(1);
if (suffix.isArguments() && prefix.getNumChildren() == 1 && prefix.getChild(0) instanceof ASTName) {
ASTName name = (ASTName) prefix.getChild(0);
return name.getImage().startsWith("StructuredArguments.")
|| STRUCTURED_ARGUMENTS_METHODS.contains(name.getImage());
}
}
}
return false;
}
}

View File

@ -811,8 +811,8 @@ public class InvalidLogMessageFormatTest {
<test-code>
<description>ignore slf4j-Markers when detecting the number of arguments #2250</description>
<expected-problems>2</expected-problems>
<expected-linenumbers>11,17</expected-linenumbers>
<expected-problems>1</expected-problems>
<expected-linenumbers>11</expected-linenumbers>
<code><![CDATA[
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -830,7 +830,11 @@ public class InvalidLogMessageFormatTest {
logger.warn(marker, "foo {}", "bar"); // correct: marker and one argument
final var otherMarker = MarkerFactory.getMarker("OTHER_MARKER");
logger.warn(otherMarker, "foo"); // gets flagged as we can't statically determine the type of the "otherMarker" variable
// we can't statically determine the type of the "otherMarker" variable, so we assume it is not a string and ignore it
logger.warn(otherMarker, "foo");
final var message = "foo {} {}";
logger.warn(message, "a", "b"); // correct: first var is the message with expects two parameters
}
}
]]></code>
@ -1039,6 +1043,45 @@ public class Foo {
logger.trace(logMessage, param);
}
}
}
]]></code>
</test-code>
<test-code>
<description>[java] InvalidLogMessageFormat: false positives with logstash structured logging #3644</description>
<expected-problems>2</expected-problems>
<expected-linenumbers>27,29</expected-linenumbers>
<code><![CDATA[
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.logstash.logback.argument.StructuredArguments;
import static net.logstash.logback.marker.Markers.*;
import static net.logstash.logback.argument.StructuredArguments.*;
public class Foo {
private static final Logger LOGGER = LoggerFactory.getLogger(Foo.class);
public void withMarkers() {
// Add "foo:bar" to the JSON output
LOGGER.info(append("foo", "bar"), "Some log message");
// Add "foo:bar,name:value" to the JSON output
LOGGER.info(append("foo", "bar").and(append("name", "value")), "Some log message");
}
public void withStructuredArguments() {
// Add "foo:bar" to JSON output and "bar" to log message
LOGGER.info("log message {}", value("foo", "bar"));
// Add "foo:bar" to JSON output only
LOGGER.info("log message", keyValue("foo", "bar"));
// Add "foo:bar" to JSON output only
LOGGER.info("log message", StructuredArguments.keyValue("foo", "bar"));
// Add "foo:bar" to JSON output only, but one parameter
LOGGER.info("log message: {}", "used parameter", keyValue("foo", "bar"));
// Add "foo:bar" to JSON output only - too many arguments!
LOGGER.info("log message", "unused parameter", keyValue("foo", "bar"));
// Add "foo:bar" to JSON output only - too less arguments!
LOGGER.info("log message {} {}", keyValue("foo", "bar"));
}
}
]]></code>
</test-code>