diff --git a/core/src/main/java/com/google/errorprone/bugpatterns/DoNotUseRuleChain.java b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotUseRuleChain.java new file mode 100644 index 00000000000..e4b0e8f582a --- /dev/null +++ b/core/src/main/java/com/google/errorprone/bugpatterns/DoNotUseRuleChain.java @@ -0,0 +1,203 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * 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.bugpatterns; + +import static com.google.common.collect.Streams.stream; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Matchers.anyMethod; +import static java.util.stream.Collectors.joining; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.errorprone.BugPattern; +import com.google.errorprone.BugPattern.StandardTags; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker.VariableTreeMatcher; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Description; +import com.google.errorprone.matchers.Matcher; +import com.google.errorprone.matchers.Matchers; +import com.google.errorprone.util.ASTHelpers; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.ExpressionTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree.Kind; +import com.sun.source.tree.VariableTree; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.Type; +import java.util.List; +import java.util.Optional; +import javax.lang.model.element.ElementKind; + +/** + * Identifies {@code RuleChain} class fields and proposes refactoring to ordered {@code @Rule(order + * = n)}. + */ +@BugPattern( + tags = {StandardTags.REFACTORING}, + summary = "Do not use RuleChain", + severity = WARNING) +public class DoNotUseRuleChain extends BugChecker implements VariableTreeMatcher { + + private static final String TEST_RULE_VAR_PREFIX = "testRule"; + + private static final String JUNIT_RULE_CHAIN_IMPORT_PATH = "org.junit.rules.RuleChain"; + private static final String JUNIT_CLASS_RULE_IMPORT_PATH = "org.junit.ClassRule"; + private static final String JUNIT_RULE_IMPORT_PATH = "org.junit.Rule"; + + private static final Matcher RULE_CHAIN_METHOD_MATCHER = + anyMethod().onDescendantOf(JUNIT_RULE_CHAIN_IMPORT_PATH).namedAnyOf("around", "outerRule"); + + @Override + public Description matchVariable(VariableTree tree, VisitorState state) { + VarSymbol symbol = ASTHelpers.getSymbol(tree); + if (symbol.getKind() != ElementKind.FIELD + || !isRuleChainExpression(tree, state) + || !isClassWithSingleRule(state) + || isChainedRuleChain(tree, state)) { + return Description.NO_MATCH; + } + return describeMatch(tree, refactor(tree, state)); + } + + private static boolean isRuleChainExpression(VariableTree tree, VisitorState state) { + ExpressionTree expression = tree.getInitializer(); + if (expression == null) { + return false; + } + return RULE_CHAIN_METHOD_MATCHER.matches(expression, state); + } + + /** + * This refactoring only matches classes that feature a single occurrence of a {@code @Rule}. This + * is done to avoid breaking some edge cases where more than one RuleChain might be declared or a + * mixture of {@code @Rule} and {@code @RuleChain}. + */ + private static boolean isClassWithSingleRule(VisitorState state) { + Optional classTree = getClassTree(state); + if (classTree.isEmpty()) { + return false; + } + return classTree.get().getMembers().stream() + .filter(tree -> ASTHelpers.hasAnnotation(tree, JUNIT_RULE_IMPORT_PATH, state)) + .count() + == 1; + } + + private static Optional getClassTree(VisitorState state) { + return stream(state.getPath().iterator()) + .filter(parent -> parent.getKind() == Kind.CLASS) + .map(ClassTree.class::cast) + .findFirst(); + } + + /** + * Don't evaluate if there is a {@code RuleChain} expression inside a {@code + * RuleChain.outerRule()} or {@code RuleChain.around()} method. + */ + private static boolean isChainedRuleChain(VariableTree tree, VisitorState state) { + return getOrderedExpressions(tree, state).stream() + .map(DoNotUseRuleChain::getArgumentExpression) + .anyMatch(ex -> Matchers.isSameType(JUNIT_RULE_CHAIN_IMPORT_PATH).matches(ex, state)); + } + + private static SuggestedFix refactor(VariableTree tree, VisitorState state) { + SuggestedFix.Builder fix = SuggestedFix.builder(); + ImmutableList expressions = getOrderedExpressions(tree, state); + + String replacement = ""; + for (int i = 0; i < expressions.size(); i++) { + ExpressionTree expression = getArgumentExpression(expressions.get(i)); + addImportIfNecessary(expression, fix); + replacement += extractRuleFromExpression(expression, i, tree, state); + } + + fix.replace(tree, replacement); + return fix.build(); + } + + /** + * Since {@code ASTHelpers.getReceiver()} goes into reverse order, the expression list must be + * reversed in order for them to follow the required ordering from {@code @Rule(order = n)}. + */ + private static ImmutableList getOrderedExpressions( + VariableTree tree, VisitorState state) { + ImmutableList.Builder expressions = ImmutableList.builder(); + for (ExpressionTree ex = tree.getInitializer(); + RULE_CHAIN_METHOD_MATCHER.matches(ex, state); + ex = ASTHelpers.getReceiver(ex)) { + expressions.add(ex); + } + return expressions.build().reverse(); + } + + // Gets the only argument from {@code RuleChain.outerRule()} or {@code RuleChain.around()} to use + // as the variable value from the new ordered {@code @Rule(order = n)} + private static ExpressionTree getArgumentExpression(ExpressionTree ex) { + MethodInvocationTree methodInvocation = (MethodInvocationTree) ex; + return methodInvocation.getArguments().get(0); + } + + private static void addImportIfNecessary(ExpressionTree expression, SuggestedFix.Builder fix) { + Type originalType = ASTHelpers.getResultType(expression); + if (ImmutableSet.of(Kind.METHOD_INVOCATION, Kind.LAMBDA_EXPRESSION) + .contains(expression.getKind())) { + fix.addImport(originalType.tsym.getQualifiedName().toString()); + } + } + + private static String extractRuleFromExpression( + ExpressionTree expression, int order, VariableTree tree, VisitorState state) { + String className = className(expression); + + return String.format( + "%s(order = %d)\npublic %sfinal %s %s = %s;\n", + annotationName(tree, state), + order, + ASTHelpers.getSymbol(tree).isStatic() ? "static " : "", + className, + classToVariableName(className), + state.getSourceForNode(expression)); + } + + private static String className(ExpressionTree expression) { + Type originalType = ASTHelpers.getResultType(expression); + List arguments = originalType.getTypeArguments(); + String className = originalType.tsym.getSimpleName().toString(); + if (!arguments.isEmpty()) { + String argumentString = + arguments.stream().map(t -> t.tsym.getSimpleName().toString()).collect(joining(", ")); + return String.format("%s<%s>", className, argumentString); + } + return className; + } + + private static String annotationName(VariableTree tree, VisitorState state) { + if (ASTHelpers.hasAnnotation(tree, JUNIT_CLASS_RULE_IMPORT_PATH, state)) { + return "@ClassRule"; + } + return "@Rule"; + } + + private static String classToVariableName(String className) { + return String.format("%s%s", TEST_RULE_VAR_PREFIX, className) + .replace("<", "") + .replace(">", "") + .replace(",", "") + .replace(" ", ""); + } +} diff --git a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java index 37162310716..aaab3feeaf7 100644 --- a/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java +++ b/core/src/main/java/com/google/errorprone/scanner/BuiltInCheckerSuppliers.java @@ -112,6 +112,7 @@ import com.google.errorprone.bugpatterns.DoNotClaimAnnotations; import com.google.errorprone.bugpatterns.DoNotMockAutoValue; import com.google.errorprone.bugpatterns.DoNotMockChecker; +import com.google.errorprone.bugpatterns.DoNotUseRuleChain; import com.google.errorprone.bugpatterns.DoubleBraceInitialization; import com.google.errorprone.bugpatterns.DuplicateMapKeys; import com.google.errorprone.bugpatterns.EmptyCatch; @@ -1071,6 +1072,7 @@ public static ScannerSupplier errorChecks() { DeduplicateConstants.class, DepAnn.class, DifferentNameButSame.class, + DoNotUseRuleChain.class, EmptyIfStatement.class, EqualsBrokenForNull.class, EqualsMissingNullable.class, diff --git a/core/src/test/java/com/google/errorprone/bugpatterns/DoNotUseRuleChainTest.java b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotUseRuleChainTest.java new file mode 100644 index 00000000000..3c2a8a2f003 --- /dev/null +++ b/core/src/test/java/com/google/errorprone/bugpatterns/DoNotUseRuleChainTest.java @@ -0,0 +1,511 @@ +/* + * Copyright 2023 The Error Prone Authors. + * + * 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.bugpatterns; + +import com.google.errorprone.BugCheckerRefactoringTestHelper; +import com.google.errorprone.CompilationTestHelper; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Tests for {@link DoNotUseRuleChain} */ +@RunWith(JUnit4.class) +public class DoNotUseRuleChainTest { + + private final BugCheckerRefactoringTestHelper refactoringHelper = + BugCheckerRefactoringTestHelper.newInstance(DoNotUseRuleChain.class, getClass()); + + private final CompilationTestHelper compilationHelper = + CompilationTestHelper.newInstance(DoNotUseRuleChain.class, getClass()); + + @Test + public void negativeEmptyFile() { + compilationHelper.addSourceLines("EmptyFile.java", "// Empty file").doTest(); + } + + @Test + public void negativeRuleChainNoInitializer() { + compilationHelper + .addSourceLines( + "RuleChainNoInitializer.java", + "package rulechainnoinitializer;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class RuleChainNoInitializer {", + "@Rule", + "public RuleChain notInitializedRule;", + "}") + .doTest(); + } + + @Test + public void negativeEmptyRuleChain() { + compilationHelper + .addSourceLines( + "EmptyRuleChain.java", + "package emptyrulechain;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class EmptyRuleChain {", + "@Rule", + "public final RuleChain emptyRuleChain = RuleChain.emptyRuleChain();", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeNullRuleChain() { + compilationHelper + .addSourceLines( + "NullRuleChain.java", + "package nullrulechain;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class NullRuleChain {", + "@Rule", + "public final RuleChain nullRuleChain = null;", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeLocalVariable() { + compilationHelper + .addSourceLines( + "LocalVariable.java", + "package localvariable;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class LocalVariable {", + "public void doNothing() {", + "RuleChain ruleChain = RuleChain.outerRule(new Rule2()).around(new Rule3());", + "}", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeSingleRules() { + compilationHelper + .addSourceLines( + "SingleRule.java", + "package singlerule;", + "import org.junit.Rule;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class SingleRule {", + "@Rule", + "public final Rule1 ruleOne = new Rule1(null, null);", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeOrderedRules() { + compilationHelper + .addSourceLines( + "OrderedRules.java", + "package orderedrules;", + "import org.junit.Rule;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class OrderedRules {", + "@Rule(order = 0)", + "public final Rule1 ruleOne = new Rule1(\"unused\", new Rule2());", + "@Rule(order = 1)", + "public final Rule2 ruleTwo = new Rule2();", + "@Rule(order = 2)", + "public final Rule3 ruleThree = new Rule3();", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeOrderedRulesClassRules() { + compilationHelper + .addSourceLines( + "OrderedRulesClassRules.java", + "package orderedrulesclassRules;", + "import org.junit.Rule;", + "import org.junit.ClassRule;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class OrderedRulesClassRules {", + "@Rule(order = 0)", + "public final Rule1 ruleOne = new Rule1(\"unused\", new Rule2());", + "@Rule(order = 1)", + "public final Rule2 ruleTwo = new Rule2();", + "@ClassRule(order = 0)", + "public static final Rule4 ruleFour = new Rule4();", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeChainedRulesChain() { + compilationHelper + .addSourceLines( + "OrderedRuleChain.java", + "package orderedrulechain;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class OrderedRuleChain {", + "@Rule(order = 0)", + "public final RuleChain orderedRuleChain = RuleChain.outerRule(new Rule3())", + ".around(RuleChain.outerRule(new Rule2()).around(new Rule3()));", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void negativeMultipleRuleChains() { + compilationHelper + .addSourceLines( + "MultipleRuleChains.java", + "package multiplerulechains;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class MultipleRuleChains {", + "public final Rule5 varRule52 = new Rule5();", + "@Rule", + "public final RuleChain ruleChain = RuleChain.outerRule(", + "new Rule1(\"unused\", new Rule2()))", + ".around(new Rule2()).around(new Rule5());", + "@Rule", + "public final RuleChain ruleChain2 = RuleChain.outerRule(new Rule3()).around(", + "varRule52);", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringOrderedRulesChain() { + refactoringHelper + .addInputLines( + "OrderedRuleChain.java", + "package orderedrulechain;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class OrderedRuleChain {", + "@Rule(order = 0)", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain orderedRuleChain = RuleChain.outerRule(new Rule3())", + ".around(new Rule2());", + customRuleClasses(), + "}") + .addOutputLines( + "OrderedRuleChain.java", + "package orderedrulechain;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class OrderedRuleChain {", + "@Rule(order = 0)", + "public final Rule3 testRuleRule3 = new Rule3();", + "@Rule(order = 1)", + "public final Rule2 testRuleRule2 = new Rule2();", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringUnorderedRuleChainWithNewObjects() { + refactoringHelper + .addInputLines( + "UnorderedRuleChainWithNewObjects.java", + "package orderedrulechainwithnewobjects;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithNewObjects {", + "@Rule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain ruleChain = RuleChain.outerRule(", + "new Rule1(\"really big string so that the new falls into another line\"", + ", new Rule2()))", + ".around(new Rule2()).around(new Rule5()).around((base, description) ->", + "new Statement() {", + "@Override", + "public void evaluate() throws Throwable {", + "// Do nothing", + "}", + "});", + customRuleClasses(), + "}") + .addOutputLines( + "UnorderedRuleChainWithNewObjects.java", + "package orderedrulechainwithnewobjects;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithNewObjects {", + "@Rule(order = 0)", + "public final Rule1 testRuleRule1 =", + "new Rule1(\"really big string so that the new falls into another line\"", + ", new Rule2());", + "@Rule(order = 1)", + "public final Rule2 testRuleRule2 = new Rule2();", + "@Rule(order = 2)", + "public final Rule5 testRuleRule5 = new Rule5();", + "@Rule(order = 3)", + "public final TestRule testRuleTestRule = (base, description) ->", + "new Statement() {", + "@Override", + "public void evaluate() throws Throwable {", + "// Do nothing", + "}", + "};", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringUnorderedRuleChainWithExistingObjects() { + refactoringHelper + .addInputLines( + "UnorderedRuleChainWithExistingObjects.java", + "package orderedrulechainwithexistingobjects;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithExistingObjects {", + "public final Rule1 varRule1 = new Rule1(\"unused\", new Rule2());", + "public final Rule2 varRule2 = new Rule2();", + "@Rule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain ruleChain = RuleChain.outerRule(varRule1).around(varRule2);", + customRuleClasses(), + "}") + .addOutputLines( + "UnorderedRuleChainWithExistingObjects.java", + "package orderedrulechainwithexistingobjects;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithExistingObjects {", + "public final Rule1 varRule1 = new Rule1(\"unused\", new Rule2());", + "public final Rule2 varRule2 = new Rule2();", + "@Rule(order = 0)", + "public final Rule1 testRuleRule1 = varRule1;", + "@Rule(order = 1)", + "public final Rule2 testRuleRule2 = varRule2;", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringUnorderedRuleChainWithGenericClass() { + refactoringHelper + .addInputLines( + "UnorderedRuleChainWithGenericClass.java", + "package orderedrulechainwithgenericclass;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithGenericClass {", + "public final Rule5 varRule52 = new Rule5();", + "public final Rule6 varRule532 = new Rule6();", + "@Rule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain ruleChain = RuleChain.outerRule(varRule52).around(varRule532);", + customRuleClasses(), + "}") + .addOutputLines( + "UnorderedRuleChainWithGenericClass.java", + "package orderedrulechainwithgenericclass;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithGenericClass {", + "public final Rule5 varRule52 = new Rule5();", + "public final Rule6 varRule532 = new Rule6();", + "@Rule(order = 0)", + "public final Rule5 testRuleRule5Rule2 = varRule52;", + "@Rule(order = 1)", + "public final Rule6 testRuleRule6Rule3Rule2 = varRule532;", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringUnorderedTwoRuleChainWithClassRule() { + refactoringHelper + .addInputLines( + "UnorderedRuleChainWithClassRule.java", + "package orderedrulechainwithclassrule;", + "import org.junit.ClassRule;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithClassRule {", + "@Rule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain ruleChain = RuleChain.outerRule(", + "new Rule2()).around(new Rule3());", + "@ClassRule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public static final RuleChain classRuleChain = RuleChain.outerRule(new Rule4());", + customRuleClasses(), + "}") + .addOutputLines( + "UnorderedRuleChainWithClassRule.java", + "package orderedrulechainwithclassrule;", + "import org.junit.ClassRule;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runner.Description;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithClassRule {", + "@Rule(order = 0)", + "public final Rule2 testRuleRule2 = new Rule2();", + "@Rule(order = 1)", + "public final Rule3 testRuleRule3 = new Rule3();", + "@ClassRule(order = 0)", + "public static final Rule4 testRuleRule4 = new Rule4();", + customRuleClasses(), + "}") + .doTest(); + } + + @Test + public void refactoringUnorderedRuleChainWithoutTestRuleImport() { + refactoringHelper + .addInputLines( + "UnorderedRuleChainWithoutTestRuleImport.java", + "package orderedrulechainwithouttestruleimport;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithoutTestRuleImport {", + "@Rule", + "// BUG: Diagnostic contains: Do not use RuleChain", + "public final RuleChain ruleChain = RuleChain.outerRule((base, description) ->", + "new Statement() {", + "@Override", + "public void evaluate() throws Throwable {", + "// Do nothing", + "}", + "});", + "}") + .addOutputLines( + "UnorderedRuleChainWithoutTestRuleImport.java", + "package orderedrulechainwithouttestruleimport;", + "import org.junit.Rule;", + "import org.junit.rules.RuleChain;", + "import org.junit.rules.TestRule;", + "import org.junit.runners.model.Statement;", + "public class UnorderedRuleChainWithoutTestRuleImport {", + "@Rule(order = 0)", + "public final TestRule testRuleTestRule =", + "(base, description) ->", + "new Statement() {", + "@Override", + "public void evaluate() throws Throwable {", + "// Do nothing", + "}", + "};", + "}") + .doTest(); + } + + private static String customRuleClasses() { + return "private class BaseCustomRule implements TestRule {\n" + + "@Override\n" + + "public Statement apply(Statement base, Description description) {\n" + + "return new Statement() {\n" + + "@Override\n" + + "public void evaluate() throws Throwable {\n" + + "// Do nothing\n" + + "}\n" + + "};\n" + + "}\n" + + "}\n" + + "private class Rule1 extends BaseCustomRule {\n" + + "private Rule1(String unusedString, TestRule unusedRule) {\n" + + " // Example with parameter\n" + + "}\n" + + "}\n" + + "private class Rule2 extends BaseCustomRule {}\n" + + "private class Rule3 extends BaseCustomRule {}\n" + + "private static class Rule4 implements TestRule {\n" + + "@Override\n" + + "public Statement apply(Statement base, Description description) {\n" + + "return new Statement() {\n" + + "@Override\n" + + "public void evaluate() throws Throwable {\n" + + "// Do nothing\n" + + "}\n" + + "};\n" + + "}\n" + + "}\n" + + "private class Rule5 extends BaseCustomRule {}\n" + + "private class Rule6 extends BaseCustomRule {}\n"; + } +}