MatchXpathCheck.java
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2021 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.checks.coding;
import java.util.List;
import java.util.stream.Collectors;
import com.puppycrawl.tools.checkstyle.StatelessCheck;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
import com.puppycrawl.tools.checkstyle.xpath.RootNode;
import net.sf.saxon.Configuration;
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;
/**
* <p>
* Evaluates Xpath query and report violation on all matching AST nodes. This check allows
* user to implement custom checks using Xpath. If Xpath query is not specified explicitly,
* then the check does nothing.
* </p>
* <p>
* It is recommended to define custom message for violation to explain what is not allowed and what
* to use instead, default message might be too abstract. To customize a message you need to
* add {@code message} element with <b>matchxpath.match</b> as {@code key} attribute and
* desired message as {@code value} attribute.
* </p>
* <p>
* Please read more about Xpath syntax at
* <a href="https://www.saxonica.com/html/documentation10/expressions/">Xpath Syntax</a>.
* Information regarding Xpath functions can be found at
* <a href="https://www.saxonica.com/html/documentation10/functions/fn/">XSLT/XPath Reference</a>.
* Note, that <b>@text</b> attribute can used only with token types that are listed in
* <a href="https://github.com/checkstyle/checkstyle/search?q=%22TOKEN_TYPES_WITH_TEXT_ATTRIBUTE+%3D+Arrays.asList%22">
* XpathUtil</a>.
* </p>
* <ul>
* <li>
* Property {@code query} - Specify Xpath query.
* Type is {@code java.lang.String}.
* Default value is {@code ""}.
* </li>
* </ul>
* <p>
* Checkstyle provides <a href="https://checkstyle.org/cmdline.html">command line tool</a>
* and <a href="https://checkstyle.org/writingchecks.html#The_Checkstyle_SDK_Gui">GUI
* application</a> with options to show AST and to ease usage of Xpath queries.
* </p>
* <p><b>-T</b> option prints AST tree of the checked file.</p>
* <pre>
* $ java -jar checkstyle-X.XX-all.jar -T Main.java
* CLASS_DEF -> CLASS_DEF [1:0]
* |--MODIFIERS -> MODIFIERS [1:0]
* | `--LITERAL_PUBLIC -> public [1:0]
* |--LITERAL_CLASS -> class [1:7]
* |--IDENT -> Main [1:13]
* `--OBJBLOCK -> OBJBLOCK [1:18]
* |--LCURLY -> { [1:18]
* |--METHOD_DEF -> METHOD_DEF [2:4]
* | |--MODIFIERS -> MODIFIERS [2:4]
* | | `--LITERAL_PUBLIC -> public [2:4]
* | |--TYPE -> TYPE [2:11]
* | | `--IDENT -> String [2:11]
* | |--IDENT -> sayHello [2:18]
* | |--LPAREN -> ( [2:26]
* | |--PARAMETERS -> PARAMETERS [2:27]
* | | `--PARAMETER_DEF -> PARAMETER_DEF [2:27]
* | | |--MODIFIERS -> MODIFIERS [2:27]
* | | |--TYPE -> TYPE [2:27]
* | | | `--IDENT -> String [2:27]
* | | `--IDENT -> name [2:34]
* | |--RPAREN -> ) [2:38]
* | `--SLIST -> { [2:40]
* | |--LITERAL_RETURN -> return [3:8]
* | | |--EXPR -> EXPR [3:25]
* | | | `--PLUS -> + [3:25]
* | | | |--STRING_LITERAL -> "Hello, " [3:15]
* | | | `--IDENT -> name [3:27]
* | | `--SEMI -> ; [3:31]
* | `--RCURLY -> } [4:4]
* `--RCURLY -> } [5:0]
* </pre>
* <p><b>-b</b> option shows AST nodes that match given Xpath query. This command can be used to
* validate accuracy of Xpath query against given file.</p>
* <pre>
* $ java -jar checkstyle-X.XX-all.jar Main.java -b "//METHOD_DEF[./IDENT[@text='sayHello']]"
* CLASS_DEF -> CLASS_DEF [1:0]
* `--OBJBLOCK -> OBJBLOCK [1:18]
* |--METHOD_DEF -> METHOD_DEF [2:4]
* </pre>
* <p>
* The following example demonstrates validation of methods order, so that public methods should
* come before the private ones:
* </p>
* <pre>
* <module name="MatchXpath">
* <property name="query" value="//METHOD_DEF[.//LITERAL_PRIVATE and
* following-sibling::METHOD_DEF[.//LITERAL_PUBLIC]]"/>
* <message key="matchxpath.match"
* value="Private methods must appear after public methods"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* public class Test {
* public void method1() { }
* private void method2() { } // violation
* public void method3() { }
* private void method4() { } // violation
* public void method5() { }
* private void method6() { } // ok
* }
* </pre>
* <p>
* To violate if there are any parametrized constructors
* </p>
* <pre>
* <module name="MatchXpath">
* <property name="query" value="//CTOR_DEF[count(./PARAMETERS/*) > 0]"/>
* <message key="matchxpath.match"
* value="Parameterized constructors are not allowed, there should be only default
* ctor"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* public class Test {
* public Test(Object c) { } // violation
* public Test(int a, HashMap<String, Integer> b) { } // violation
* public Test() { } // ok
* }
* </pre>
* <p>
* To violate if method name is 'test' or 'foo'
* </p>
* <pre>
* <module name="MatchXpath">
* <property name="query" value="//METHOD_DEF[./IDENT[@text='test' or @text='foo']]"/>
* <message key="matchxpath.match"
* value="Method name should not be 'test' or 'foo'"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* public class Test {
* public void test() {} // violation
* public void getName() {} // ok
* public void foo() {} // violation
* public void sayHello() {} // ok
* }
* </pre>
* <p>
* To violate if new instance creation was done without <b>var</b> type
* </p>
* <pre>
* <module name="MatchXpath">
* <property name="query" value="//VARIABLE_DEF[./ASSIGN/EXPR/LITERAL_NEW
* and not(./TYPE/IDENT[@text='var'])]"/>
* <message key="matchxpath.match"
* value="New instances should be created via 'var' keyword to avoid duplication of type
* reference in statement"/>
* </module>
* </pre>
* <p>
* Example:
* </p>
* <pre>
* public class Test {
* public void foo() {
* SomeObject a = new SomeObject(); // violation
* var b = new SomeObject(); // OK
* }
* }
* </pre>
* <p>
* Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
* </p>
* <p>
* Violation Message Keys:
* </p>
* <ul>
* <li>
* {@code matchxpath.match}
* </li>
* </ul>
*
* @since 8.39
*/
@StatelessCheck
public class MatchXpathCheck extends AbstractCheck {
/**
* A key is pointing to the warning message text provided by user.
*/
public static final String MSG_KEY = "matchxpath.match";
/** Specify Xpath query. */
private String query = "";
/** Xpath expression. */
private XPathExpression xpathExpression;
/**
* Setter to specify Xpath query.
*
* @param query Xpath query.
* @throws IllegalStateException if creation of xpath expression fails
*/
public void setQuery(String query) {
this.query = query;
if (!query.isEmpty()) {
try {
final XPathEvaluator xpathEvaluator =
new XPathEvaluator(Configuration.newConfiguration());
xpathExpression = xpathEvaluator.createExpression(query);
}
catch (XPathException ex) {
throw new IllegalStateException("Creating Xpath expression failed: " + query, ex);
}
}
}
@Override
public int[] getDefaultTokens() {
return getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return CommonUtil.EMPTY_INT_ARRAY;
}
@Override
public void beginTree(DetailAST rootAST) {
if (xpathExpression != null) {
final List<DetailAST> matchingNodes = findMatchingNodesByXpathQuery(rootAST);
matchingNodes.forEach(node -> log(node, MSG_KEY));
}
}
/**
* Find nodes that match query.
*
* @param rootAST root node
* @return list of matching nodes
* @throws IllegalStateException if evaluation of xpath query fails
*/
private List<DetailAST> findMatchingNodesByXpathQuery(DetailAST rootAST) {
try {
final RootNode rootNode = new RootNode(rootAST);
final XPathDynamicContext xpathDynamicContext =
xpathExpression.createDynamicContext(rootNode);
final List<Item> matchingItems = xpathExpression.evaluate(xpathDynamicContext);
return matchingItems.stream()
.map(item -> ((AbstractNode) item).getUnderlyingNode())
.collect(Collectors.toList());
}
catch (XPathException ex) {
throw new IllegalStateException("Evaluation of Xpath query failed: " + query, ex);
}
}
}