/* * Copyright 2014 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; import static com.google.common.truth.Truth.assertThat; import static com.google.errorprone.BugPattern.Category.JDK; import static com.google.errorprone.BugPattern.SeverityLevel.ERROR; import static com.google.errorprone.BugPattern.Suppressibility.UNSUPPRESSIBLE; import static com.google.errorprone.DiagnosticTestHelper.diagnosticMessage; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasItem; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.errorprone.bugpatterns.ArrayEquals; import com.google.errorprone.bugpatterns.BadShiftAmount; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.ClassTreeMatcher; import com.google.errorprone.bugpatterns.ChainingConstructorIgnoresParameter; import com.google.errorprone.bugpatterns.Finally; import com.google.errorprone.fixes.SuggestedFix; import com.google.errorprone.matchers.Description; import com.google.errorprone.scanner.ScannerSupplier; import com.google.errorprone.util.ASTHelpers; import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodTree; import java.io.ByteArrayOutputStream; import java.io.IOError; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import javax.lang.model.SourceVersion; import javax.tools.Diagnostic; import javax.tools.DiagnosticListener; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import org.hamcrest.Matcher; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * @author cushon@google.com (Liam Miller-Cushon) */ @RunWith(JUnit4.class) public class ErrorProneJavaCompilerTest { @Rule public final TemporaryFolder tempDir = new TemporaryFolder(); @Test public void testIsSupportedOption() { ErrorProneJavaCompiler compiler = new ErrorProneJavaCompiler(); // javac options should be passed through assertThat(compiler.isSupportedOption("-source")).isEqualTo(1); // error-prone options should be handled assertThat(compiler.isSupportedOption("-Xep:")).isEqualTo(0); assertThat(compiler.isSupportedOption("-XepIgnoreUnknownCheckNames")).isEqualTo(0); assertThat(compiler.isSupportedOption("-XepDisableWarningsInGeneratedCode")).isEqualTo(0); // old-style error-prone options are not supported assertThat(compiler.isSupportedOption("-Xepdisable:")).isEqualTo(-1); } interface JavaFileObjectDiagnosticListener extends DiagnosticListener<JavaFileObject> {} @Test public void testGetStandardJavaFileManager() { JavaCompiler mockCompiler = mock(JavaCompiler.class); ErrorProneJavaCompiler compiler = new ErrorProneJavaCompiler(mockCompiler); JavaFileObjectDiagnosticListener listener = mock(JavaFileObjectDiagnosticListener.class); Locale locale = Locale.CANADA; compiler.getStandardFileManager(listener, locale, null); verify(mockCompiler).getStandardFileManager(listener, locale, null); } @Test public void testRun() { JavaCompiler mockCompiler = mock(JavaCompiler.class); ErrorProneJavaCompiler compiler = new ErrorProneJavaCompiler(mockCompiler); InputStream in = mock(InputStream.class); OutputStream out = mock(OutputStream.class); OutputStream err = mock(OutputStream.class); String[] arguments = {"-source", "8", "-target", "8"}; compiler.run(in, out, err, arguments); verify(mockCompiler).run(in, out, err, arguments); } @Test public void testSourceVersion() { ErrorProneJavaCompiler compiler = new ErrorProneJavaCompiler(); assertThat(compiler.getSourceVersions()).contains(SourceVersion.latest()); assertThat(compiler.getSourceVersions()).doesNotContain(SourceVersion.RELEASE_5); } @Test public void fileWithErrorIntegrationTest() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Collections.<String>emptyList(), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isFalse(); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[SelfAssignment]"))); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); } @Test public void testWithDisabledCheck() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Collections.<String>emptyList(), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isFalse(); result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Arrays.asList("-Xep:SelfAssignment:OFF"), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isTrue(); } @Test public void testWithCheckPromotedToError() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/WaitNotInLoopPositiveCases.java"), Collections.<String>emptyList(), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isTrue(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[WaitNotInLoop]"))); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); result = doCompile( Arrays.asList("bugpatterns/testdata/WaitNotInLoopPositiveCases.java"), Arrays.asList("-Xep:WaitNotInLoop:ERROR"), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isFalse(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); } @Test public void testWithCheckDemotedToWarning() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Collections.<String>emptyList(), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isFalse(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[SelfAssignment]"))); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Arrays.asList("-Xep:SelfAssignment:WARN"), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isTrue(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); } @Test public void testWithNonDefaultCheckOn() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/EmptyIfStatementPositiveCases.java"), Collections.<String>emptyList(), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isTrue(); assertThat(result.diagnosticHelper.getDiagnostics()).isEmpty(); result = doCompile( Arrays.asList("bugpatterns/testdata/EmptyIfStatementPositiveCases.java"), Arrays.asList("-Xep:EmptyIf"), Collections.<Class<? extends BugChecker>>emptyList()); assertThat(result.succeeded).isFalse(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[EmptyIf]"))); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); } @Test public void testBadFlagThrowsException() throws Exception { try { doCompile( Arrays.asList("bugpatterns/testdata/EmptyIfStatementPositiveCases.java"), Arrays.asList("-Xep:foo:bar:baz"), Collections.<Class<? extends BugChecker>>emptyList()); fail(); } catch (RuntimeException expected) { assertThat(expected.getMessage()).contains("invalid flag"); } } @BugPattern( name = "ArrayEquals", summary = "Reference equality used to compare arrays", explanation = "", category = JDK, severity = ERROR, suppressibility = UNSUPPRESSIBLE ) public static class UnsuppressibleArrayEquals extends ArrayEquals {} @Test public void testCantDisableNonDisableableCheck() throws Exception { try { doCompile( Arrays.asList("bugpatterns/testdata/ArrayEqualsPositiveCases.java"), Arrays.asList("-Xep:ArrayEquals:OFF"), ImmutableList.<Class<? extends BugChecker>>of(UnsuppressibleArrayEquals.class)); fail(); } catch (RuntimeException expected) { assertThat(expected.getMessage()).contains("ArrayEquals may not be disabled"); } } @Test public void testWithCustomCheckPositive() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/BadShiftAmountPositiveCases.java"), Collections.<String>emptyList(), Arrays.<Class<? extends BugChecker>>asList(BadShiftAmount.class)); assertThat(result.succeeded).isFalse(); assertThat(result.diagnosticHelper.getDiagnostics().size()).isGreaterThan(0); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[BadShiftAmount]"))); assertTrue(matcher.matches(result.diagnosticHelper.getDiagnostics())); } @Test public void testWithCustomCheckNegative() throws Exception { CompilationResult result = doCompile( Arrays.asList("bugpatterns/testdata/SelfAssignmentPositiveCases1.java"), Collections.<String>emptyList(), Arrays.<Class<? extends BugChecker>>asList(Finally.class)); assertThat(result.succeeded).isTrue(); assertThat(result.diagnosticHelper.getDiagnostics()).isEmpty(); } @Test public void testSeverityResetsAfterOverride() throws Exception { DiagnosticTestHelper diagnosticHelper = new DiagnosticTestHelper(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, UTF_8), true); ErrorProneInMemoryFileManager fileManager = new ErrorProneInMemoryFileManager(); JavaCompiler errorProneJavaCompiler = new ErrorProneJavaCompiler(); List<String> args = Lists.newArrayList( "-d", tempDir.getRoot().getAbsolutePath(), "-proc:none", "-Xep:ChainingConstructorIgnoresParameter:WARN"); List<JavaFileObject> sources = fileManager.forResources( ChainingConstructorIgnoresParameter.class, "testdata/ChainingConstructorIgnoresParameterPositiveCases.java"); fileManager.close(); JavaCompiler.CompilationTask task = errorProneJavaCompiler.getTask( printWriter, fileManager, diagnosticHelper.collector, args, null, sources); boolean succeeded = task.call(); assertThat(succeeded).isTrue(); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[ChainingConstructorIgnoresParameter]"))); assertTrue(matcher.matches(diagnosticHelper.getDiagnostics())); // reset state between compilations diagnosticHelper.clearDiagnostics(); fileManager = new ErrorProneInMemoryFileManager(); sources = fileManager.forResources( ChainingConstructorIgnoresParameter.class, "testdata/ChainingConstructorIgnoresParameterPositiveCases.java"); fileManager.close(); args.remove("-Xep:ChainingConstructorIgnoresParameter:WARN"); task = errorProneJavaCompiler.getTask( printWriter, fileManager, diagnosticHelper.collector, args, null, sources); succeeded = task.call(); assertThat(succeeded).isFalse(); assertTrue(matcher.matches(diagnosticHelper.getDiagnostics())); } @Test public void testMaturityResetsAfterOverride() throws Exception { DiagnosticTestHelper diagnosticHelper = new DiagnosticTestHelper(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, UTF_8), true); ErrorProneInMemoryFileManager fileManager = new ErrorProneInMemoryFileManager(); JavaCompiler errorProneJavaCompiler = new ErrorProneJavaCompiler(); List<String> args = Lists.newArrayList( "-d", tempDir.getRoot().getAbsolutePath(), "-proc:none", "-Xep:EmptyIf"); List<JavaFileObject> sources = fileManager.forResources( BadShiftAmount.class, "testdata/EmptyIfStatementPositiveCases.java"); fileManager.close(); JavaCompiler.CompilationTask task = errorProneJavaCompiler.getTask( printWriter, null, diagnosticHelper.collector, args, null, sources); boolean succeeded = task.call(); assertThat(succeeded).isFalse(); Matcher<? super Iterable<Diagnostic<? extends JavaFileObject>>> matcher = hasItem(diagnosticMessage(containsString("[EmptyIf]"))); assertTrue(matcher.matches(diagnosticHelper.getDiagnostics())); diagnosticHelper.clearDiagnostics(); args.remove("-Xep:EmptyIf"); task = errorProneJavaCompiler.getTask( printWriter, null, diagnosticHelper.collector, args, null, sources); fileManager.close(); succeeded = task.call(); assertThat(succeeded).isTrue(); assertThat(diagnosticHelper.getDiagnostics()).isEmpty(); } @BugPattern( name = "DeleteMethod", summary = "You appear to be using methods; prefer to implement all program logic inside the main" + " function by flipping bits in a single long[].", explanation = "", category = JDK, severity = ERROR, suppressibility = UNSUPPRESSIBLE ) public static class DeleteMethod extends BugChecker implements ClassTreeMatcher { @Override public Description matchClass(ClassTree tree, VisitorState state) { MethodTree ctor = (MethodTree) Iterables.getOnlyElement(tree.getMembers()); Preconditions.checkArgument(ASTHelpers.isGeneratedConstructor(ctor)); return describeMatch(tree, SuggestedFix.delete(ctor)); } } @Test public void testFixGeneratedConstructor() throws Exception { CompilationResult result = doCompile( Arrays.asList("testdata/DeleteGeneratedConstructorTestCase.java"), Collections.<String>emptyList(), ImmutableList.<Class<? extends BugChecker>>of(DeleteMethod.class)); assertThat(result.succeeded).isFalse(); assertThat(result.diagnosticHelper.getDiagnostics()).hasSize(1); assertThat( Iterables.getOnlyElement(result.diagnosticHelper.getDiagnostics()) .getMessage(Locale.ENGLISH)) .contains("AssertionError: Cannot edit synthetic AST nodes"); } private static class CompilationResult { public final boolean succeeded; public final DiagnosticTestHelper diagnosticHelper; public CompilationResult(boolean succeeded, DiagnosticTestHelper diagnosticHelper) { this.succeeded = succeeded; this.diagnosticHelper = diagnosticHelper; } } private CompilationResult doCompile( List<String> fileNames, List<String> extraArgs, List<Class<? extends BugChecker>> customCheckers) { DiagnosticTestHelper diagnosticHelper = new DiagnosticTestHelper(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(outputStream, UTF_8), true); ErrorProneInMemoryFileManager fileManager = new ErrorProneInMemoryFileManager(); List<String> args = Lists.newArrayList( "-d", tempDir.getRoot().getAbsolutePath(), "-proc:none"); args.addAll(extraArgs); JavaCompiler errorProneJavaCompiler = (customCheckers.isEmpty()) ? new ErrorProneJavaCompiler() : new ErrorProneJavaCompiler(ScannerSupplier.fromBugCheckerClasses(customCheckers)); JavaCompiler.CompilationTask task = errorProneJavaCompiler.getTask( printWriter, fileManager, diagnosticHelper.collector, args, null, fileManager.forResources(getClass(), fileNames.toArray(new String[0]))); try { fileManager.close(); } catch (IOException e) { throw new IOError(e); } return new CompilationResult(task.call(), diagnosticHelper); } }