// ===================================================================== // // Copyright (C) 2012 - 2016, Philip Graf // // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // which accompanies this distribution, and is available at // http://www.eclipse.org/legal/epl-v10.html // // ===================================================================== package ch.acanda.eclipse.pmd.java.resolution; import static com.google.common.base.Preconditions.checkState; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.io.StringReader; import java.util.Collection; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.osgi.framework.Version; import com.google.common.base.Optional; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableCollection; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; import com.google.common.collect.Iterators; import ch.acanda.eclipse.pmd.java.resolution.QuickFixTestData.TestParameters; import ch.acanda.eclipse.pmd.marker.MarkerUtil; import ch.acanda.eclipse.pmd.marker.MarkerUtil.Range; import ch.acanda.eclipse.pmd.marker.PMDMarker; import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.PMDConfiguration; import net.sourceforge.pmd.PMDException; import net.sourceforge.pmd.RuleContext; import net.sourceforge.pmd.RuleSetFactory; import net.sourceforge.pmd.RuleSetNotFoundException; import net.sourceforge.pmd.RuleSets; import net.sourceforge.pmd.RuleViolation; import net.sourceforge.pmd.SourceCodeProcessor; import net.sourceforge.pmd.lang.Language; import net.sourceforge.pmd.lang.LanguageRegistry; import net.sourceforge.pmd.lang.LanguageVersion; /** * Quick fixes usually depend on an exact location of a violation to be able to work correctly. This regression test * verifies that the range values of PMD violations (begin/end line, begin/end column) are as expected. * * @author Philip Graf */ @RunWith(value = Parameterized.class) public class PMDIntegrationTest { private static final ImmutableCollection<String> TEST_DATA_XML = ImmutableList.of( "basic/ExtendsObject.xml", "design/DefaultLabelNotLastInSwitchStmt.xml", "design/EqualsNull.xml", "design/SingularField.xml", "design/UseCollectionIsEmpty.xml", "design/UseNotifyAllInsteadOfNotify.xml", "design/UseUtilityClass.xml", "design/UseVarargs.xml", "emptycode/EmptyFinallyBlock.xml", "emptycode/EmptyIfStmt.xml", "emptycode/EmptyInitializer.xml", "emptycode/EmptyStatementBlock.xml", "emptycode/EmptyStatementNotInLoop.xml", "emptycode/EmptyStaticInitializer.xml", "emptycode/EmptySwitchStatements.xml", "emptycode/EmptySynchronizedBlock.xml", "emptycode/EmptyTryBlock.xml", "emptycode/EmptyWhileStmt.xml", "migration/IntegerInstantiationAutoboxing.xml", "migration/IntegerInstantiationValueOf.xml", "migration/ByteInstantiationAutoboxing.xml", "migration/ByteInstantiationValueOf.xml", "migration/ShortInstantiationAutoboxing.xml", "migration/ShortInstantiationValueOf.xml", "migration/LongInstantiationAutoboxing.xml", "migration/LongInstantiationValueOf.xml", "naming/SuspiciousHashcodeMethodName.xml", "optimization/AddEmptyString.xml", "optimization/LocalVariableCouldBeFinal.xml", "optimization/MethodArgumentCouldBeFinal.xml", "optimization/RedundantFieldInitializer.xml", "optimization/SimplifyStartsWith.xml", "stringandstringbuffer/AppendCharacterWithChar.xml", "stringandstringbuffer/UseIndexOfChar.xml", "stringandstringbuffer/StringToString.xml", "stringandstringbuffer/UnnecessaryCaseChange.xml", "sunsecure/MethodReturnsInternalArray.xml", "unnecessary/UselessOverridingMethod.xml", "unnecessary/UnnecessaryReturn.xml"); private final String testDataXml; private final TestParameters params; public PMDIntegrationTest(final String testDataXml, final TestParameters params) { this.testDataXml = testDataXml; this.params = params; } @Parameters public static Collection<Object[]> getTestData() { final Builder<Object[]> testData = ImmutableList.builder(); for (final String tests : TEST_DATA_XML) { try (final InputStream stream = QuickFixTestData.class.getResourceAsStream(tests)) { final Collection<TestParameters> data = QuickFixTestData.createTestData(stream); for (final TestParameters params : data) { testData.add(new Object[] { tests, params }); } } catch (final IOException e) { fail(e.getMessage()); } } return testData.build(); } @Test public void violationRangeAndRuleId() throws IOException, RuleSetNotFoundException, PMDException { checkState(params.language.isPresent(), "%s: language is missing", testDataXml); checkState(params.pmdReferenceId.isPresent(), "%s: pmdReferenceId is missing", testDataXml); final Matcher languageMatcher = Pattern.compile("(.*)\\s+(\\d+[\\.\\d]+)").matcher(params.language.get()); checkState(languageMatcher.matches(), "%s: language must be formated '<terse-name> <version>'", testDataXml); final Language language = LanguageRegistry.findLanguageByTerseName(languageMatcher.group(1)); if (language == null) { failDueToInvalidTerseName(languageMatcher.group(1)); } final LanguageVersion languageVersion = language.getVersion(languageMatcher.group(2)); final String fileExtension = "." + languageVersion.getLanguage().getExtensions().get(0); final File sourceFile = File.createTempFile(getFilePrefix(params), fileExtension); try { final PMDConfiguration configuration = new PMDConfiguration(); final Reader reader = new StringReader(params.source); final RuleContext context = PMD.newRuleContext(sourceFile.getName(), sourceFile); context.setLanguageVersion(languageVersion); final RuleSets ruleSets = new RuleSetFactory().createRuleSets(params.pmdReferenceId.get()); new SourceCodeProcessor(configuration).processSourceCode(reader, ruleSets, context); // Only verify when PMD actually reports an error. There are a few test cases where the quick fix can // handle violations that PMD does not (yet) find. if (!context.getReport().isEmpty()) { // PMD might find more than one violation. If there is one with a matching range then the test passes. final Optional<RuleViolation> violation = Iterators.tryFind(context.getReport().iterator(), new Predicate<RuleViolation>() { @Override public boolean apply(final RuleViolation violation) { final Range range = MarkerUtil.getAbsoluteRange(params.source, violation); return params.offset == range.getStart() && params.length == range.getEnd() - range.getStart(); } }); assertTrue(testDataXml + " > " + params.name + ": couldn't find violation with expected range (offset = " + params.offset + ", length = " + params.length + ")", violation.isPresent()); final JavaQuickFixGenerator generator = new JavaQuickFixGenerator(); final PMDMarker marker = mock(PMDMarker.class); final String ruleId = MarkerUtil.createRuleId(violation.get().getRule()); when(marker.getRuleId()).thenReturn(ruleId); when(marker.getMarkerText()).thenReturn(""); final JavaQuickFixContext quickFixContext = new JavaQuickFixContext(new Version(languageVersion.getVersion())); assertTrue("The Java quick fix generator should have quick fixes for " + ruleId, generator.hasQuickFixes(marker, quickFixContext)); assertTrue("The Java quick fix generator should have at least one quick fix besides the SuppressWarningsQuickFix for " + ruleId, generator.getQuickFixes(marker, quickFixContext).size() > 1); } } finally { sourceFile.delete(); } } private static String getFilePrefix(final TestParameters params) { return params.pmdReferenceId.transform(PMDIntegrationTest::getRuleId).or("") + '-' + params.name + '-'; } private static String getRuleId(final String pmdReferenceId) { final Matcher matcher = Pattern.compile(".*/([^/]+)").matcher(pmdReferenceId); if (matcher.matches()) { return matcher.group(1); } return ""; } private void failDueToInvalidTerseName(final String languageTerseName) { final String msg; if (LanguageRegistry.getLanguages().isEmpty()) { msg = String.format("Cannot find the language for terse name '%s'" + " as there aren't any registered languages in the PMD language registry.", languageTerseName); } else { final String knownLanguages = LanguageRegistry.getLanguages().stream() .map(l -> l.getTerseName()) .collect(Collectors.joining(", ")); msg = String.format("The language terse name '%s' is not supported by PMD. The supported language terse names are: %s.", languageTerseName, knownLanguages); } fail(msg); } }