/* * Copyright 2017 Google Inc. All Rights Reserved. * * 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.google.errorprone.util; import static com.google.common.collect.ImmutableList.toImmutableList; import com.google.common.collect.ImmutableList; import com.google.errorprone.BugPattern; import com.google.errorprone.BugPattern.Category; import com.google.errorprone.BugPattern.SeverityLevel; import com.google.errorprone.CompilationTestHelper; import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; import com.google.errorprone.bugpatterns.BugChecker.NewClassTreeMatcher; import com.google.errorprone.matchers.Description; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; import com.sun.source.tree.NewClassTree; import com.sun.tools.javac.parser.Tokens.Comment; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** Test code for {@code Comments} */ @RunWith(JUnit4.class) public class CommentsTest { /** * A {@link BugChecker} that calls computeEndPosition for each invocation and prints the final * line of source ending at that position */ @BugPattern( name = "ComputeEndPosition", category = Category.ONE_OFF, severity = SeverityLevel.ERROR, summary = "Calls computeEndPosition and prints results" ) public static class ComputeEndPosition extends BugChecker implements MethodInvocationTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { CharSequence sourceCode = state.getSourceCode(); Optional<Integer> endPosition = Comments.computeEndPosition(tree, sourceCode, state); if (!endPosition.isPresent()) { return Description.NO_MATCH; } int startPosition = endPosition.get(); do { startPosition--; } while (sourceCode.charAt(startPosition) != '\n'); return buildDescription(tree) .setMessage(sourceCode.subSequence(startPosition + 1, endPosition.get()).toString()) .build(); } } @Test public void computeEndPosition_returnsEndOfInvocation_whenNoTrailingComment() { CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: target(param)", " target(param);", " }", "}") .doTest(); } @Test public void computeEndPosition_returnsEndOfNextStatement_whenTrailingComment() { CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: int i;", " target(param); // 1", " int i;", " }", "}") .doTest(); } @Test public void computeEndPosition_returnsEndOfBlock_whenLastWithTrailingComment() { CompilationTestHelper.newInstance(ComputeEndPosition.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: }", " target(param); // 1", " }", "}") .doTest(); } /** A {@link BugChecker} that prints the contents of comments around arguments */ @BugPattern( name = "PrintCommentsForArguments", category = Category.ONE_OFF, severity = SeverityLevel.ERROR, summary = "Prints comments occurring around arguments. Matches calls to methods named " + "'target' and all constructors" ) public static class PrintCommentsForArguments extends BugChecker implements MethodInvocationTreeMatcher, NewClassTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { if (!ASTHelpers.getSymbol(tree).getSimpleName().contentEquals("target")) { return Description.NO_MATCH; } return buildDescription(tree) .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state))) .build(); } @Override public Description matchNewClass(NewClassTree tree, VisitorState state) { return buildDescription(tree) .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state))) .build(); } private static String commentsToString(ImmutableList<Commented<ExpressionTree>> comments) { return comments .stream() .map( c -> Stream.of( String.valueOf(getTextFromCommentList(c.beforeComments())), String.valueOf(c.tree()), String.valueOf(getTextFromCommentList(c.afterComments()))) .collect(Collectors.joining(" "))) .collect(toImmutableList()) .toString(); } private static ImmutableList<String> getTextFromCommentList(ImmutableList<Comment> comments) { return comments.stream().map(Comments::getTextFromComment).collect(toImmutableList()); } } @Test public void findCommentsForArguments_findsBothComments_beforeAndAfter() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: [[1] param [2]]", " target(/* 1 */ param /* 2 */);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_findsBothComments_before() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: [[1, 2] param []]", " target(/* 1 */ /* 2 */ param);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_findsBothComments_after() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param);", " private void test(Object param) {", " // BUG: Diagnostic contains: [[] param [1, 2]]", " target(param /* 1 */ /* 2 */);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignsToCorrectParameter_adjacentComments() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [1], [2] param2 []]", " target(param1 /* 1 */, /* 2 */ param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_findsNoComments_whenNoComments() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [], [] param2 []]", " target(param1, param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_findsNothing_whenNoArguments() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target();", " private void test() {", " // BUG: Diagnostic contains: []", " target();", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignToFirstParameter_withLineCommentAfterComma() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [1], [] param2 []]", " target(param1, // 1", " param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignToFirstParameter_withBlockAfterComma() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [1], [] param2 []]", " target(param1, /* 1 */", " param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignToSecondParameter_withLineCommentAfterMethod() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [], [] param2 [2]]", " target(param1,", " param2); // 2", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignToSecondParameter_withLineCommentAfterMethodMidBlock() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " private void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [], [] param2 [2]]", " target(param1,", " param2); // 2", " int i = 1;", " }", "}") .doTest(); } @Test public void findCommentsForArguments_assignToSecondParameter_withLineCommentAfterField() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract Object target(Object param);", " // BUG: Diagnostic contains: [[] null [1]]", " private Object test = target(null); // 1", "}") .doTest(); } @Test public void findCommentsForArguments_findsComments_onConstructor() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "class Test {", " Test(Object param1, Object param2) {}", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[1] param1 [], [] param2 [2]]", " new Test(/* 1 */ param1, param2 /* 2 */);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_attachesCommentToSecondArgument_whenCommentOnOwnLine() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "class Test {", " Test(Object param1, Object param2) {}", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [], [1] param2 []]", " new Test(param1, ", " // 1", " param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_attachesCommentToArgument_whenCommentOnFollowingLineWithinCall() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "class Test {", " Test(Object param1) {}", " void test(Object param1) {", " // BUG: Diagnostic contains: [[] param1 [1]]", " new Test(param1", " // 1", " );", " }", "}") .doTest(); } @Test public void findCommentsForArguments_ignoresNextLineComment_withLineCommentAfterInvocation() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract Object target(Object param);", " void test(Object param) {", " // BUG: Diagnostic contains: [[] param [1]]", " target(param); // 1", " /* 2 */ int i;", " }", "}") .doTest(); } @Test public void findCommentsForArguments_attachesCommentToSecondArgument_whenFollowedByTreeContainingComma() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract void target(Object param1, Object param2);", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param1 [], [] param2 [1]]", " target(param1, param2); // 1", " // BUG: Diagnostic contains: [[] param1 [], [] param2 []]", " target(param1, param2);", " }", "}") .doTest(); } @Test public void findCommentsForArguments_attachesCommentToFirstCall_whenMethodIsChained() { CompilationTestHelper.newInstance(PrintCommentsForArguments.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract Test chain(Object param1);", " abstract void target(Object param2);", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[] param2 []]", " chain(/* 1 */ param1).target(param2);", " }", "}") .doTest(); } /** A {@link BugChecker} that prints the source code at comment positions */ @BugPattern( name = "PrintTextAtCommentPosition", category = Category.ONE_OFF, severity = SeverityLevel.ERROR, summary = "Prints the source code text which is under the comment position. Matches calls to " + "methods called target and constructors only" ) public static class PrintTextAtCommentPosition extends BugChecker implements MethodInvocationTreeMatcher, NewClassTreeMatcher { @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { if (!ASTHelpers.getSymbol(tree).getSimpleName().contentEquals("target")) { return Description.NO_MATCH; } return buildDescription(tree) .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state), state)) .build(); } @Override public Description matchNewClass(NewClassTree tree, VisitorState state) { return buildDescription(tree) .setMessage(commentsToString(Comments.findCommentsForArguments(tree, state), state)) .build(); } private static String commentsToString( ImmutableList<Commented<ExpressionTree>> comments, VisitorState state) { return comments .stream() .map( c -> Stream.of( String.valueOf(getSourceAtComment(c.beforeComments(), state)), String.valueOf(c.tree()), String.valueOf(getSourceAtComment(c.afterComments(), state))) .collect(Collectors.joining(" "))) .collect(toImmutableList()) .toString(); } private static ImmutableList<String> getSourceAtComment( ImmutableList<Comment> comments, VisitorState state) { return comments .stream() .map( c -> state .getSourceCode() .subSequence(c.getSourcePos(0), c.getSourcePos(0) + c.getText().length()) .toString()) .collect(toImmutableList()); } } @Test public void printTextAtCommentPosition_isCorrect_whenMethodIsChained() { CompilationTestHelper.newInstance(PrintTextAtCommentPosition.class, getClass()) .addSourceLines( "Test.java", "abstract class Test {", " abstract Test chain(Object param1);", " abstract void target(Object param2);", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[/* 1 */] param2 []]", " chain(param1).target(/* 1 */ param2);", " }", "}") .doTest(); } @Test public void printTextAtCommentPosition_isCorrect_onConstructor() { CompilationTestHelper.newInstance(PrintTextAtCommentPosition.class, getClass()) .addSourceLines( "Test.java", "class Test {", " Test(Object param1, Object param2) {}", " void test(Object param1, Object param2) {", " // BUG: Diagnostic contains: [[/* 1 */] param1 [], [] param2 [/* 2 */]]", " new Test(/* 1 */ param1, param2 /* 2 */);", " }", "}") .doTest(); } }