// ===================================================================== // // 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.builder; import static org.mockito.Matchers.any; import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.Properties; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Test; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import net.sourceforge.pmd.PMD; import net.sourceforge.pmd.Rule; 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; /** * Unit tests for {@link Analyzer}. * * @author Philip Graf */ public class AnalyzerTest { /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze Java files. */ @Test public void analyzeJava() { analyze("class A extends Object {}", "UTF-8", "java", "rulesets/java/basic.xml/ExtendsObject", "ExtendsObject"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all Java rules. */ @Test public void analyzeJavaAllRules() throws IOException { final String content = "/** */\n" + "package a;\n" + "class FooBar {\n" + " /** */\n" + " public FooBar() {\n" + " baz();\n" + " }\n" + " private void baz() {\n" + " /* */\n" + " }\n" + "}\n"; analyze(content, "UTF-8", "java", getAllRuleSetRefIds("java")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze xml files. */ @Test public void analyzeXML() { analyze("<cdata><![CDATA[[bar]]></cdata>", "UTF-8", "xml", "rulesets/xml/basic.xml/MistypedCDATASection", "MistypedCDATASection"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all xml rules. */ @Test public void analyzeXMLAllRules() throws IOException { analyze("<a/>", "UTF-8", "xml", getAllRuleSetRefIds("xml")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze jsp files. */ @Test public void analyzeJSP() { analyze("<jsp:forward page='a.jsp'/>", "UTF-8", "jsp", "rulesets/jsp/basic.xml/NoJspForward", "NoJspForward"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all jsp rules. */ @Test public void analyzeJSPAllRules() throws IOException { analyze("<%@ page contentType=\"text/html; charset=UTF-8\" pageEncoding=\"UTF-8\" %>", "UTF-8", "jsp", getAllRuleSetRefIds("jsp")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze xsl files. */ @Test public void analyzeXSL() { analyze("<variable name=\"var\" select=\"//item/descendant::child\"/>", "UTF-8", "xsl", "rulesets/xsl/xpath.xml/AvoidAxisNavigation", "AvoidAxisNavigation"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all jsp rules. */ @Test public void analyzeXSLAllRules() throws IOException { analyze("<a/>", "UTF-8", "xsl", getAllRuleSetRefIds("xsl")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze Ecmascript files. */ @Test public void analyzeEcmascript() { analyze("var z = 1.12345678901234567", "UTF-8", "js", "rulesets/ecmascript/basic.xml/InnaccurateNumericLiteral", "InnaccurateNumericLiteral"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all ecmascript rules. */ @Test public void analyzeEcmascriptAllRules() throws IOException { analyze("var i = 0", "UTF-8", "js", getAllRuleSetRefIds("ecmascript")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze Velocity files. */ @Test public void analyzeVelocity() { analyze("<script type=\"text/javascript\">$s</script>", "UTF-8", "vm", "rulesets/vm/basic.xml/NoInlineJavaScript", "NoInlineJavaScript"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all Velocity rules. */ @Test public void analyzeVelocityAllRules() throws IOException { analyze("<a/>", "UTF-8", "vm", getAllRuleSetRefIds("vm")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can analyze PLSQL files. */ @Test public void analyzePLSQL() { final String content = "CREATE OR REPLACE PACKAGE BODY date_utilities\n" + "IS\n" + "FUNCTION to_date_single_parameter (p_date_string IN VARCHAR2) RETURN DATE\n" + "IS\n" + "BEGIN\n" + " RETURN TO_DATE(p_date_string);\n" + "END to_date_single_parameter;\n" + "END date_utilities;"; analyze(content, "UTF-8", "sql", "rulesets/plsql/dates.xml/TO_DATEWithoutDateFormat", "TO_DATEWithoutDateFormat"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} can run all PLSQL rules. */ @Test public void analyzePLSQLAllRules() throws IOException { analyze("select * from a", "UTF-8", "sql", getAllRuleSetRefIds("plsql")); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} doesn't throw a NullPointerException * when the file to analyze does not have a file extension. */ @Test public void analyzeFileWithoutExtension() { analyze("Hello World", "UTF-8", null, "rulesets/java/basic.xml/ExtendsObject"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} doesn't throw a NullPointerException * when trying to analyze a class file. */ @Test public void analyzeClassFile() { analyze("", "UTF-8", "class", "rulesets/java/basic.xml/ExtendsObject"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} doesn't analyze a derived file. */ @Test public void analyzeDerivedFile() throws UnsupportedEncodingException, CoreException { final IFile file = mockFile("class A extends Object {}", "UTF-8", "java", true, true); analyze(file, "rulesets/java/basic.xml/ExtendsObject"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} doesn't analyze an inaccessible file. */ @Test public void analyzeInaccessibleFile() throws UnsupportedEncodingException, CoreException { final IFile file = mockFile("", "UTF-8", "java", false, false); analyze(file, "rulesets/java/basic.xml/ExtendsObject"); } /** * Verifies that {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor)} works around PMD's <a * href="http://sourceforge.net/p/pmd/bugs/1076/">bug #1076</a> and reports two violations instead of only one. */ @Test public void analyzePMDBug1076() throws UnsupportedEncodingException, CoreException { final IFile file = mockFile("class Foo { void bar(int a, int b) { } }", "UTF-8", "java", false, true); analyze(file, "rulesets/java/optimizations.xml/MethodArgumentCouldBeFinal", "MethodArgumentCouldBeFinal", "MethodArgumentCouldBeFinal"); } /** * Prepares the arguments, calls {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor), and verifies that it * invokes {@link ViolationProcessor#annotate(IFile, Iterable) with the correct rule violations. */ public void analyze(final String content, final String charset, final String fileExtension, final String ruleSetRefId, final String... violatedRules) { try { final IFile file = mockFile(content, charset, fileExtension, false, true); analyze(file, ruleSetRefId, violatedRules); } catch (CoreException | IOException e) { throw new AssertionError("Failed to mock file", e); } } /** * Prepares the arguments, calls {@link Analyzer#analyze(IFile, RuleSets, ViolationProcessor), and verifies that it * invokes {@link ViolationProcessor#annotate(IFile, Iterable) with the correct rule violations and that it invokes * {@link RuleSets#start(RuleContext)} as well as {@link RuleSets#end(RuleContext)} if the file is valid. */ public void analyze(final IFile file, final String ruleSetRefId, final String... violatedRules) { try { final ViolationProcessor violationProcessor = mock(ViolationProcessor.class); final RuleSets ruleSets = spy(new RuleSetFactory().createRuleSets(ruleSetRefId)); new Analyzer().analyze(file, ruleSets, violationProcessor); verify(violationProcessor).annotate(same(file), violations(violatedRules)); final boolean isValidFile = violatedRules.length > 0; if (isValidFile) { verify(ruleSets).start(any(RuleContext.class)); verify(ruleSets).end(any(RuleContext.class)); } } catch (final RuleSetNotFoundException e) { throw new AssertionError("Failed to create rule sets", e); } catch (CoreException | IOException e) { throw new AssertionError("Failed to annotate file", e); } } private IFile mockFile(final String content, final String charset, final String fileExtension, final boolean isDerived, final boolean isAccessible) throws CoreException, UnsupportedEncodingException { final IFile file = mock(IFile.class); when(file.isDerived(IResource.CHECK_ANCESTORS)).thenReturn(isDerived); when(file.isAccessible()).thenReturn(isAccessible); when(file.getFileExtension()).thenReturn(fileExtension); when(file.getCharset()).thenReturn(charset); when(file.getContents()).thenReturn(new ByteArrayInputStream(content.getBytes(charset))); final IPath path = mock(IPath.class); when(file.getRawLocation()).thenReturn(path); when(path.toFile()).thenReturn(new File("test." + fileExtension)); return file; } private String getAllRuleSetRefIds(final String language) throws IOException { try (final InputStream in = PMD.class.getResourceAsStream("/rulesets/" + language + "/rulesets.properties")) { final Properties properties = new Properties(); properties.load(in); return properties.getProperty("rulesets.filenames"); } } private Iterable<RuleViolation> violations(final String... ruleNames) { return argThat(new RuleViolationIteratorMatcher(ruleNames)); } private static class RuleViolationIteratorMatcher extends BaseMatcher<Iterable<RuleViolation>> { private final Iterable<String> expectedRuleNames; public RuleViolationIteratorMatcher(final String... ruleNames) { expectedRuleNames = Lists.newArrayList(ruleNames); } @Override public boolean matches(final Object item) { if (item instanceof Iterable) { @SuppressWarnings("unchecked") final Iterable<RuleViolation> violations = (Iterable<RuleViolation>) item; final Iterable<String> actualRuleNames = Iterables.transform(violations, new RuleNameExtractor()); return Iterables.elementsEqual(expectedRuleNames, actualRuleNames); } return false; } @Override public void describeTo(final Description description) { description.appendText("Iterable containing the following violations " + Iterables.toString(expectedRuleNames)); } private static class RuleNameExtractor implements Function<RuleViolation, String> { @Override public String apply(final RuleViolation violation) { final Rule rule = violation.getRule(); if (rule != null) { return rule.getName(); } return null; } } } }