Skip to content

Commit

Permalink
Issue checkstyle#4422: Implement XpathFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
timurt authored and Timur Tibeyev committed Aug 20, 2017
1 parent dd3e7bc commit 2fc31d2
Show file tree
Hide file tree
Showing 9 changed files with 515 additions and 15 deletions.
2 changes: 2 additions & 0 deletions config/import-control.xml
Expand Up @@ -143,11 +143,13 @@
</subpackage>

<subpackage name="filters">
<allow pkg="net.sf.saxon"/>
<allow class="java.lang.ref.WeakReference" local-only="true"/>
<allow class="com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent" local-only="true"/>
<allow class="com.puppycrawl.tools.checkstyle.TreeWalkerFilter" local-only="true"/>
<disallow pkg="com\.puppycrawl\.tools\.checkstyle\.checks\.[^.]+" regex="true"/>
<allow pkg="com.puppycrawl.tools.checkstyle.utils"/>
<allow pkg="com.puppycrawl.tools.checkstyle.xpath"/>
</subpackage>

<subpackage name="gui" strategyOnMismatch="disallowed">
Expand Down
3 changes: 2 additions & 1 deletion config/pmd-test.xml
Expand Up @@ -69,7 +69,7 @@
<!-- pmd cannot find assert if it is located in private method of this class called from the test
method or method of another class -->
<properties>
<!-- in SuppressionCommentFilterTest, SuppressWithNearbyCommentFilterTest, SuppressionFilterTest
<!-- in SuppressionCommentFilterTest, SuppressWithNearbyCommentFilterTest, SuppressionFilterTest, XpathFilterTest
pmd does not accept nl.jqno.equalsverifier.EqualsVerifier#.verify() as correctness check method -->
<!-- in AbstractJavadocCheckTest pmd does not find asserts in another class methods called from the test method -->
<!-- in ImportControlCheckTest, DetailASTTest
Expand All @@ -88,6 +88,7 @@
| //ClassOrInterfaceDeclaration[@Image='AstRegressionTest']//MethodDeclarator[@Image='testImpossibleValid']
| //ClassOrInterfaceDeclaration[@Image='AllChecksTest']//MethodDeclarator[@Image='testAllModulesAreReferencedInConfigFile']
| //ClassOrInterfaceDeclaration[@Image='DetailASTTest']//MethodDeclarator[@Image='testTreeStructure']
| //ClassOrInterfaceDeclaration[@Image='XpathFilterTest']//MethodDeclarator[@Image='testEqualsAndHashCode']
"/>
</properties>
</rule>
Expand Down
18 changes: 12 additions & 6 deletions src/main/java/com/puppycrawl/tools/checkstyle/TreeWalker.java
Expand Up @@ -209,9 +209,14 @@ protected void processFiltered(File file, FileText fileText) throws CheckstyleEx

walk(astWithComments, contents, AstState.WITH_COMMENTS);
}
final SortedSet<LocalizedMessage> filteredMessages =
getFilteredMessages(fileName, contents);
addMessages(filteredMessages);
if (filters.isEmpty()) {
addMessages(messages);
}
else {
final SortedSet<LocalizedMessage> filteredMessages =
getFilteredMessages(fileName, contents, rootAST);
addMessages(filteredMessages);
}
messages.clear();
}
}
Expand All @@ -232,14 +237,15 @@ protected void processFiltered(File file, FileText fileText) throws CheckstyleEx
* Returns filtered set of {@link LocalizedMessage}.
* @param fileName path to the file
* @param fileContents the contents of the file
* @param rootAST root AST element {@link DetailAST} of the file
* @return filtered set of messages
*/
private SortedSet<LocalizedMessage> getFilteredMessages(String fileName,
FileContents fileContents) {
private SortedSet<LocalizedMessage> getFilteredMessages(
String fileName, FileContents fileContents, DetailAST rootAST) {
final SortedSet<LocalizedMessage> result = new TreeSet<>(messages);
for (LocalizedMessage element : messages) {
final TreeWalkerAuditEvent event =
new TreeWalkerAuditEvent(fileContents, fileName, element);
new TreeWalkerAuditEvent(fileContents, fileName, element, rootAST);
for (TreeWalkerFilter filter : filters) {
if (!filter.accept(event)) {
result.remove(element);
Expand Down
Expand Up @@ -19,6 +19,7 @@

package com.puppycrawl.tools.checkstyle;

import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FileContents;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;

Expand All @@ -34,19 +35,23 @@ public class TreeWalkerAuditEvent {
private final FileContents fileContents;
/** Message associated with the event. **/
private final LocalizedMessage localizedMessage;
/** Root ast element. **/
private final DetailAST rootAst;

/**
* Creates a new {@code TreeWalkerAuditEvent} instance.
*
* @param fileContents contents of the file associated with the event
* @param fileName file associated with the event
* @param localizedMessage the actual message
* @param rootAst root AST element {@link DetailAST} of the file
*/
public TreeWalkerAuditEvent(FileContents fileContents, String fileName,
LocalizedMessage localizedMessage) {
LocalizedMessage localizedMessage, DetailAST rootAst) {
this.fileContents = fileContents;
this.fileName = fileName;
this.localizedMessage = localizedMessage;
this.rootAst = rootAst;
}

/**
Expand Down Expand Up @@ -115,4 +120,20 @@ public String getModuleId() {
public String getSourceName() {
return localizedMessage.getSourceName();
}

/**
* Gets the token type of the message.
* @return the token type of the message
*/
public int getTokenType() {
return localizedMessage.getTokenType();
}

/**
* Gets the root element of the AST tree.
* @return the root element of the AST tree
*/
public DetailAST getRootAst() {
return rootAst;
}
}
184 changes: 184 additions & 0 deletions src/main/java/com/puppycrawl/tools/checkstyle/filters/XpathFilter.java
@@ -0,0 +1,184 @@
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle.filters;

import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;

import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
import com.puppycrawl.tools.checkstyle.xpath.RootNode;
import net.sf.saxon.om.Item;
import net.sf.saxon.sxpath.XPathDynamicContext;
import net.sf.saxon.sxpath.XPathEvaluator;
import net.sf.saxon.sxpath.XPathExpression;
import net.sf.saxon.trans.XPathException;

/**
* This filter processes {@link TreeWalkerAuditEvent}
* objects based on the criteria of file, check, module id, xpathQuery.
*
* @author Timur Tibeyev.
*/
public class XpathFilter implements TreeWalkerFilter {
/** The regexp to match file names against. */
private final Pattern fileRegexp;

/** The pattern for file names. */
private final String filePattern;

/** The regexp to match check names against. */
private final Pattern checkRegexp;

/** The pattern for check class names. */
private final String checkPattern;

/** Module id filter. */
private final String moduleId;

/** Xpath expression. */
private final XPathExpression xpathExpression;

/** Xpath query. */
private final String xpathQuery;

/**
* Creates a {@code XpathElement} instance.
* @param files regular expression for names of filtered files
* @param checks regular expression for filtered check classes
* @param moduleId the module id
* @param query the xpath query
*/
public XpathFilter(String files, String checks,
String moduleId, String query) {
filePattern = files;
fileRegexp = Pattern.compile(files);
checkPattern = checks;
if (checks == null) {
checkRegexp = null;
}
else {
checkRegexp = CommonUtils.createPattern(checks);
}
this.moduleId = moduleId;
xpathQuery = query;
if (xpathQuery == null) {
xpathExpression = null;
}
else {
final XPathEvaluator xpathEvaluator = new XPathEvaluator();
try {
xpathExpression = xpathEvaluator.createExpression(xpathQuery);
}
catch (XPathException ex) {
throw new IllegalStateException("Unexpected xpath query", ex);
}
}
}

@Override
public boolean accept(TreeWalkerAuditEvent event) {
return !isFileNameAndModuleAndCheckNameMatching(event)
|| !isXpathQueryMatching(event);
}

/**
* Is matching by file name, moduleId and Check name.
* @param event event
* @return true if it is matching
*/
private boolean isFileNameAndModuleAndCheckNameMatching(TreeWalkerAuditEvent event) {
return event.getFileName() != null
&& fileRegexp.matcher(event.getFileName()).find()
&& event.getLocalizedMessage() != null
&& (moduleId == null || moduleId.equals(event.getModuleId()))
&& (checkRegexp == null || checkRegexp.matcher(event.getSourceName()).find());
}

/**
* Is matching by xpath query.
* @param event event
* @return true is matching
*/
private boolean isXpathQueryMatching(TreeWalkerAuditEvent event) {
boolean isMatching = false;
if (xpathExpression != null) {
final List<Item> items = getItems(event);
for (Item item : items) {
final AbstractNode abstractNode = (AbstractNode) item;
isMatching = abstractNode.getTokenType() == event.getTokenType()
&& abstractNode.getLineNumber() == event.getLine()
&& abstractNode.getColumnNumber() == event.getColumn();
if (isMatching) {
break;
}
}
}
return isMatching;
}

/**
* Returns list of nodes matching xpath expression given event.
* @param event {@code TreeWalkerAuditEvent} object
* @return list of nodes matching xpath expression given event
*/
private List<Item> getItems(TreeWalkerAuditEvent event) {
final RootNode rootNode;
if (event.getRootAst() == null) {
rootNode = null;
}
else {
rootNode = new RootNode(event.getRootAst());
}
final List<Item> items;
try {
final XPathDynamicContext xpathDynamicContext =
xpathExpression.createDynamicContext(rootNode);
items = xpathExpression.evaluate(xpathDynamicContext);
}
catch (XPathException ex) {
throw new IllegalStateException(ex);
}
return items;
}

@Override
public int hashCode() {
return Objects.hash(filePattern, checkPattern, moduleId, xpathQuery);
}

@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
final XpathFilter xpathFilter = (XpathFilter) other;
return Objects.equals(filePattern, xpathFilter.filePattern)
&& Objects.equals(checkPattern, xpathFilter.checkPattern)
&& Objects.equals(moduleId, xpathFilter.moduleId)
&& Objects.equals(xpathQuery, xpathFilter.xpathQuery);
}
}
Expand Up @@ -297,7 +297,7 @@ public void testInvalidCheckFormat() throws Exception {
@Test
public void testAcceptNullLocalizedMessage() {
final SuppressWithNearbyCommentFilter filter = new SuppressWithNearbyCommentFilter();
final TreeWalkerAuditEvent auditEvent = new TreeWalkerAuditEvent(null, null, null);
final TreeWalkerAuditEvent auditEvent = new TreeWalkerAuditEvent(null, null, null, null);
assertTrue("Filter should accept null localized message", filter.accept(auditEvent));
}

Expand Down Expand Up @@ -353,13 +353,13 @@ public void testTagsAreClearedEachRun() {
new FileContents("filename", "//SUPPRESS CHECKSTYLE ignore", "line2");
contents.reportSingleLineComment(1, 0);
final TreeWalkerAuditEvent dummyEvent = new TreeWalkerAuditEvent(contents, "filename",
new LocalizedMessage(1, null, null, null, null, Object.class, null));
new LocalizedMessage(1, null, null, null, null, Object.class, null), null);
suppressionCommentFilter.accept(dummyEvent);
final FileContents contents2 =
new FileContents("filename2", "some line", "//SUPPRESS CHECKSTYLE ignore");
contents2.reportSingleLineComment(2, 0);
final TreeWalkerAuditEvent dummyEvent2 = new TreeWalkerAuditEvent(contents2, "filename",
new LocalizedMessage(1, null, null, null, null, Object.class, null));
new LocalizedMessage(1, null, null, null, null, Object.class, null), null);
suppressionCommentFilter.accept(dummyEvent2);
final List<SuppressionCommentFilter.Tag> tags =
Whitebox.getInternalState(suppressionCommentFilter, "tags");
Expand Down
Expand Up @@ -297,7 +297,7 @@ public void testInvalidMessageFormat() throws Exception {
@Test
public void testAcceptNullLocalizedMessage() {
final SuppressionCommentFilter filter = new SuppressionCommentFilter();
final TreeWalkerAuditEvent auditEvent = new TreeWalkerAuditEvent(null, null, null);
final TreeWalkerAuditEvent auditEvent = new TreeWalkerAuditEvent(null, null, null, null);
Assert.assertTrue("Filter should accept audit event", filter.accept(auditEvent));
Assert.assertNull("File name should not be null", auditEvent.getFileName());
}
Expand Down Expand Up @@ -332,7 +332,7 @@ public void testFindNearestMatchDontAllowSameColumn() {
new FileContents("filename", "//CHECKSTYLE:OFF: ConstantNameCheck", "line2");
contents.reportSingleLineComment(1, 0);
final TreeWalkerAuditEvent dummyEvent = new TreeWalkerAuditEvent(contents, "filename",
new LocalizedMessage(1, null, null, null, null, Object.class, null));
new LocalizedMessage(1, null, null, null, null, Object.class, null), null);
final boolean result = suppressionCommentFilter.accept(dummyEvent);
assertFalse("Fileter should not accept event", result);
}
Expand All @@ -344,13 +344,13 @@ public void testTagsAreClearedEachRun() {
new FileContents("filename", "//CHECKSTYLE:OFF", "line2");
contents.reportSingleLineComment(1, 0);
final TreeWalkerAuditEvent dummyEvent = new TreeWalkerAuditEvent(contents, "filename",
new LocalizedMessage(1, null, null, null, null, Object.class, null));
new LocalizedMessage(1, null, null, null, null, Object.class, null), null);
suppressionCommentFilter.accept(dummyEvent);
final FileContents contents2 =
new FileContents("filename2", "some line", "//CHECKSTYLE:OFF");
contents2.reportSingleLineComment(2, 0);
final TreeWalkerAuditEvent dummyEvent2 = new TreeWalkerAuditEvent(contents2, "filename",
new LocalizedMessage(1, null, null, null, null, Object.class, null));
new LocalizedMessage(1, null, null, null, null, Object.class, null), null);
suppressionCommentFilter.accept(dummyEvent2);
final List<SuppressionCommentFilter.Tag> tags =
Whitebox.getInternalState(suppressionCommentFilter, "tags");
Expand Down

0 comments on commit 2fc31d2

Please sign in to comment.