diff --git a/java-checks-test-sources/default/pom.xml b/java-checks-test-sources/default/pom.xml index fc00b39f4a7..d9024753560 100644 --- a/java-checks-test-sources/default/pom.xml +++ b/java-checks-test-sources/default/pom.xml @@ -43,6 +43,12 @@ 3) Several plugins are disabled bellow to not generate jars --> + + org.jetbrains.kotlin + kotlin-stdlib + 1.8.0 + provided + org.jspecify diff --git a/java-checks-test-sources/default/src/main/java/checks/ControlCharacterInLiteralCheck.java b/java-checks-test-sources/default/src/main/java/checks/ControlCharacterInLiteralCheck.java index 43cf5f1e0e6..0cbf28d2fc0 100644 Binary files a/java-checks-test-sources/default/src/main/java/checks/ControlCharacterInLiteralCheck.java and b/java-checks-test-sources/default/src/main/java/checks/ControlCharacterInLiteralCheck.java differ diff --git a/java-checks/src/main/java/org/sonar/java/checks/ControlCharacterInLiteralCheck.java b/java-checks/src/main/java/org/sonar/java/checks/ControlCharacterInLiteralCheck.java index 27c8cc90bea..6e661db797a 100644 --- a/java-checks/src/main/java/org/sonar/java/checks/ControlCharacterInLiteralCheck.java +++ b/java-checks/src/main/java/org/sonar/java/checks/ControlCharacterInLiteralCheck.java @@ -22,6 +22,7 @@ import java.util.regex.Pattern; import org.sonar.check.Rule; import org.sonar.check.RuleProperty; +import org.sonar.java.checks.helpers.GeneratedStringLiteralRecognizer; import org.sonar.java.model.LiteralUtils; import org.sonar.plugins.java.api.IssuableSubscriptionVisitor; import org.sonar.plugins.java.api.tree.LiteralTree; @@ -70,14 +71,14 @@ public List nodesToVisit() { public void visitNode(Tree tree) { LiteralTree literal = (LiteralTree) tree; String literalValue = LiteralUtils.getAsStringValue(literal); - Matcher matcher = null; + Matcher matcher; if (allowTabsInTextBlocks && tree.is(Tree.Kind.TEXT_BLOCK)) { matcher = CONTROL_CHARACTERS_WITHOUT_TABS_PATTERN.matcher(literalValue); } else { matcher = CONTROL_CHARACTERS_PATTERN.matcher(literalValue); } - if (matcher.find()) { - reportIssue(literal, String.format(MESSAGE_FORMAT, literalValue.codePointAt(matcher.start()))); + if (matcher.find() && !GeneratedStringLiteralRecognizer.isGenerated(literal)) { + reportIssue(literal, String.format(MESSAGE_FORMAT, literalValue.codePointAt(matcher.start()))); } } diff --git a/java-checks/src/main/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizer.java b/java-checks/src/main/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizer.java new file mode 100644 index 00000000000..877d2688852 --- /dev/null +++ b/java-checks/src/main/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizer.java @@ -0,0 +1,82 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.helpers; + +import org.sonar.java.model.ExpressionUtils; +import org.sonar.plugins.java.api.semantic.Type; +import org.sonar.plugins.java.api.tree.AnnotationTree; +import org.sonar.plugins.java.api.tree.ExpressionTree; +import org.sonar.plugins.java.api.tree.LiteralTree; +import org.sonar.plugins.java.api.tree.Tree; + +/** + * Recognizes strings that are most likely not hand-written and maintained by developers. + * + * Currently we ignore: + * + *
    + *
  • {@code d1} strings in {@code kotlin.Metadata} annotations
  • + *
  • All strings in {@code kotlin.jvm.internal.SourceDebugExtension} annotations
  • + *
  • All strings in {@code kotlin.coroutines.jvm.internal.DebugMetadata} annotations
  • + *
+ */ +public final class GeneratedStringLiteralRecognizer { + + private static final String KOTLIN_METADATA = "kotlin.Metadata"; + private static final String KOTLIN_SOURCE_DEBUG_EXTENSION = "kotlin.jvm.internal.SourceDebugExtension"; + private static final String KOTLIN_DEBUG_METADATA = "kotlin.coroutines.jvm.internal.DebugMetadata"; + + private GeneratedStringLiteralRecognizer() { + } + + /** + * Checks whether a string or text block literal is (most likely) generated by a known tool. + * and not hand-written by developers. Useful for rule suppression for rules that raise on such literals. + * + * @param literal the literal to check + * @return {@code true} when the literal is recognized as generated + */ + public static boolean isGenerated(LiteralTree literal) { + if (!literal.is(Tree.Kind.STRING_LITERAL, Tree.Kind.TEXT_BLOCK)) { + return false; + } + + Tree annotationArgument = literal; + while (annotationArgument.parent() != null && !annotationArgument.parent().is(Tree.Kind.ARGUMENTS)) { + annotationArgument = annotationArgument.parent(); + } + + Tree arguments = annotationArgument.parent(); + if (arguments == null || !(arguments.parent() instanceof AnnotationTree annotation)) { + return false; + } + + return isAnnotationType(annotation, KOTLIN_SOURCE_DEBUG_EXTENSION) + || isAnnotationType(annotation, KOTLIN_DEBUG_METADATA) + || (annotationArgument instanceof ExpressionTree argument + && "d1".equals(ExpressionUtils.annotationAttributeName(argument)) + && isAnnotationType(annotation, KOTLIN_METADATA)); + } + + private static boolean isAnnotationType(AnnotationTree annotation, String fullyQualifiedName) { + Type annotationType = annotation.symbolType(); + String unqualifiedName = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf('.') + 1); + return annotationType.is(fullyQualifiedName) + || (annotationType.isUnknown() && annotationType.name().equals(unqualifiedName)); + } + +} diff --git a/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java b/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java new file mode 100644 index 00000000000..6a5cc4e18af --- /dev/null +++ b/java-checks/src/test/java/org/sonar/java/checks/helpers/GeneratedStringLiteralRecognizerTest.java @@ -0,0 +1,135 @@ +/* + * SonarQube Java + * Copyright (C) SonarSource Sàrl + * mailto:info AT sonarsource DOT com + * + * You can redistribute and/or modify this program under the terms of + * the Sonar Source-Available License Version 1, as published by SonarSource Sàrl. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the Sonar Source-Available License for more details. + * + * You should have received a copy of the Sonar Source-Available License + * along with this program; if not, see https://sonarsource.com/license/ssal/ + */ +package org.sonar.java.checks.helpers; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.sonar.plugins.java.api.tree.BaseTreeVisitor; +import org.sonar.plugins.java.api.tree.LiteralTree; + +import static org.assertj.core.api.Assertions.assertThat; + +class GeneratedStringLiteralRecognizerTest { + + @Test + void recognizes_generated_literals() { + assertGenerated(""" + package kotlin; + @interface Metadata { + String[] d1(); + } + @Metadata(d1 = {"generated"}) + class A {} + """); + + assertGenerated(""" + package kotlin.jvm.internal; + @interface SourceDebugExtension { + String[] value(); + } + @SourceDebugExtension({ + "generated", + \""" + generated text block + \"""}) + class A {} + """); + + assertGenerated(""" + package kotlin.coroutines.jvm.internal; + @interface DebugMetadata { + String c(); + String f(); + String m(); + String[] n(); + } + @DebugMetadata(c = "c", f = "f", m = "m", n = {"n"}) + class A {} + """); + } + + @Test + void recognizes_generated_literals_without_dependencies() { + assertGenerated(""" + @Metadata(d1 = {"generated"}) + class A {} + """); + + assertGenerated(""" + @kotlin.jvm.internal.SourceDebugExtension({"generated"}) + class A {} + """); + } + + @Test + void does_not_recognize_other_literals() { + assertNotGenerated(""" + package kotlin; + @interface Metadata { + String[] d2(); + } + @Metadata(d2 = {"not generated"}) + class A {} + """); + + assertNotGenerated(""" + @interface NotKotlinMetadata { + String[] d1(); + } + @NotKotlinMetadata(d1 = {"not generated"}) + class A { + String value = "not generated"; + char character = 'a'; + } + """); + } + + @Test + void does_not_recognize_shadowed_kotlin_metadata() { + assertNotGenerated(""" + import kotlin.Metadata; + class A { + @interface Metadata { + String[] d1(); + } + @Metadata(d1 = {"not generated"}) + class B {} + } + """); + } + + private static void assertGenerated(String source) { + assertThat(literals(source)).allMatch(GeneratedStringLiteralRecognizer::isGenerated); + } + + private static void assertNotGenerated(String source) { + assertThat(literals(source)).noneMatch(GeneratedStringLiteralRecognizer::isGenerated); + } + + private static List literals(String source) { + List literals = new ArrayList<>(); + JParserTestUtils.parse(source).accept(new BaseTreeVisitor() { + @Override + public void visitLiteral(LiteralTree tree) { + literals.add(tree); + } + }); + assertThat(literals).isNotEmpty(); + return literals; + } +}