001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.coding;
021
022import java.util.List;
023import java.util.stream.Collectors;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
027import com.puppycrawl.tools.checkstyle.api.DetailAST;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
030import com.puppycrawl.tools.checkstyle.xpath.RootNode;
031import net.sf.saxon.Configuration;
032import net.sf.saxon.om.Item;
033import net.sf.saxon.sxpath.XPathDynamicContext;
034import net.sf.saxon.sxpath.XPathEvaluator;
035import net.sf.saxon.sxpath.XPathExpression;
036import net.sf.saxon.trans.XPathException;
037
038/**
039 * <p>
040 * Evaluates Xpath query and report violation on all matching AST nodes. This check allows
041 * user to implement custom checks using Xpath. If Xpath query is not specified explicitly,
042 * then the check does nothing.
043 * </p>
044 * <p>
045 * It is recommended to define custom message for violation to explain what is not allowed and what
046 * to use instead, default message might be too abstract. To customize a message you need to
047 * add {@code message} element with <b>matchxpath.match</b> as {@code key} attribute and
048 * desired message as {@code value} attribute.
049 * </p>
050 * <p>
051 * Please read more about Xpath syntax at
052 * <a href="https://www.saxonica.com/html/documentation10/expressions/">Xpath Syntax</a>.
053 * Information regarding Xpath functions can be found at
054 * <a href="https://www.saxonica.com/html/documentation10/functions/fn/">XSLT/XPath Reference</a>.
055 * Note, that <b>@text</b> attribute can used only with token types that are listed in
056 * <a href="https://github.com/checkstyle/checkstyle/search?q=%22TOKEN_TYPES_WITH_TEXT_ATTRIBUTE+%3D+Arrays.asList%22">
057 *     XpathUtil</a>.
058 * </p>
059 * <ul>
060 * <li>
061 * Property {@code query} - Specify Xpath query.
062 * Type is {@code java.lang.String}.
063 * Default value is {@code ""}.
064 * </li>
065 * </ul>
066 * <p>
067 * Checkstyle provides <a href="https://checkstyle.org/cmdline.html">command line tool</a>
068 * and <a href="https://checkstyle.org/writingchecks.html#The_Checkstyle_SDK_Gui">GUI
069 * application</a> with options to show AST and to ease usage of Xpath queries.
070 * </p>
071 * <p><b>-T</b> option prints AST tree of the checked file.</p>
072 * <pre>
073 * $ java -jar checkstyle-X.XX-all.jar -T Main.java
074 * CLASS_DEF -&gt; CLASS_DEF [1:0]
075 * |--MODIFIERS -&gt; MODIFIERS [1:0]
076 * |   `--LITERAL_PUBLIC -&gt; public [1:0]
077 * |--LITERAL_CLASS -&gt; class [1:7]
078 * |--IDENT -&gt; Main [1:13]
079 * `--OBJBLOCK -&gt; OBJBLOCK [1:18]
080 * |--LCURLY -&gt; { [1:18]
081 * |--METHOD_DEF -&gt; METHOD_DEF [2:4]
082 * |   |--MODIFIERS -&gt; MODIFIERS [2:4]
083 * |   |   `--LITERAL_PUBLIC -&gt; public [2:4]
084 * |   |--TYPE -&gt; TYPE [2:11]
085 * |   |   `--IDENT -&gt; String [2:11]
086 * |   |--IDENT -&gt; sayHello [2:18]
087 * |   |--LPAREN -&gt; ( [2:26]
088 * |   |--PARAMETERS -&gt; PARAMETERS [2:27]
089 * |   |   `--PARAMETER_DEF -&gt; PARAMETER_DEF [2:27]
090 * |   |       |--MODIFIERS -&gt; MODIFIERS [2:27]
091 * |   |       |--TYPE -&gt; TYPE [2:27]
092 * |   |       |   `--IDENT -&gt; String [2:27]
093 * |   |       `--IDENT -&gt; name [2:34]
094 * |   |--RPAREN -&gt; ) [2:38]
095 * |   `--SLIST -&gt; { [2:40]
096 * |       |--LITERAL_RETURN -&gt; return [3:8]
097 * |       |   |--EXPR -&gt; EXPR [3:25]
098 * |       |   |   `--PLUS -&gt; + [3:25]
099 * |       |   |       |--STRING_LITERAL -&gt; "Hello, " [3:15]
100 * |       |   |       `--IDENT -&gt; name [3:27]
101 * |       |   `--SEMI -&gt; ; [3:31]
102 * |       `--RCURLY -&gt; } [4:4]
103 * `--RCURLY -&gt; } [5:0]
104 * </pre>
105 * <p><b>-b</b> option shows AST nodes that match given Xpath query. This command can be used to
106 * validate accuracy of Xpath query against given file.</p>
107 * <pre>
108 * $ java -jar checkstyle-X.XX-all.jar Main.java -b "//METHOD_DEF[./IDENT[@text='sayHello']]"
109 * CLASS_DEF -&gt; CLASS_DEF [1:0]
110 * `--OBJBLOCK -&gt; OBJBLOCK [1:18]
111 * |--METHOD_DEF -&gt; METHOD_DEF [2:4]
112 * </pre>
113 * <p>
114 * The following example demonstrates validation of methods order, so that public methods should
115 * come before the private ones:
116 * </p>
117 * <pre>
118 * &lt;module name="MatchXpath"&gt;
119 *  &lt;property name="query" value="//METHOD_DEF[.//LITERAL_PRIVATE and
120 *  following-sibling::METHOD_DEF[.//LITERAL_PUBLIC]]"/&gt;
121 *  &lt;message key=&quot;matchxpath.match&quot;
122 *  value=&quot;Private methods must appear after public methods&quot;/&gt;
123 * &lt;/module&gt;
124 * </pre>
125 * <p>
126 * Example:
127 * </p>
128 * <pre>
129 * public class Test {
130 *  public void method1() { }
131 *  private void method2() { } // violation
132 *  public void method3() { }
133 *  private void method4() { } // violation
134 *  public void method5() { }
135 *  private void method6() { } // ok
136 * }
137 * </pre>
138 * <p>
139 * To violate if there are any parametrized constructors
140 * </p>
141 * <pre>
142 * &lt;module name="MatchXpath"&gt;
143 *  &lt;property name="query" value="//CTOR_DEF[count(./PARAMETERS/*) &gt; 0]"/&gt;
144 *  &lt;message key=&quot;matchxpath.match&quot;
145 *  value=&quot;Parameterized constructors are not allowed, there should be only default
146 *  ctor&quot;/&gt;
147 * &lt;/module&gt;
148 * </pre>
149 * <p>
150 * Example:
151 * </p>
152 * <pre>
153 * public class Test {
154 *  public Test(Object c) { } // violation
155 *  public Test(int a, HashMap&lt;String, Integer&gt; b) { } // violation
156 *  public Test() { } // ok
157 * }
158 * </pre>
159 * <p>
160 * To violate if method name is 'test' or 'foo'
161 * </p>
162 * <pre>
163 * &lt;module name="MatchXpath"&gt;
164 *  &lt;property name="query" value="//METHOD_DEF[./IDENT[@text='test' or @text='foo']]"/&gt;
165 *  &lt;message key=&quot;matchxpath.match&quot;
166 *  value=&quot;Method name should not be 'test' or 'foo'&quot;/&gt;
167 * &lt;/module&gt;
168 * </pre>
169 * <p>
170 * Example:
171 * </p>
172 * <pre>
173 * public class Test {
174 *  public void test() {} // violation
175 *  public void getName() {} // ok
176 *  public void foo() {} // violation
177 *  public void sayHello() {} // ok
178 * }
179 * </pre>
180 * <p>
181 * To violate if new instance creation was done without <b>var</b> type
182 * </p>
183 * <pre>
184 * &lt;module name=&quot;MatchXpath&quot;&gt;
185 *  &lt;property name=&quot;query&quot; value=&quot;//VARIABLE_DEF[./ASSIGN/EXPR/LITERAL_NEW
186 *  and not(./TYPE/IDENT[@text='var'])]&quot;/&gt;
187 *  &lt;message key=&quot;matchxpath.match&quot;
188 *  value=&quot;New instances should be created via 'var' keyword to avoid duplication of type
189 *  reference in statement&quot;/&gt;
190 * &lt;/module&gt;
191 * </pre>
192 * <p>
193 * Example:
194 * </p>
195 * <pre>
196 * public class Test {
197 *   public void foo() {
198 *     SomeObject a = new SomeObject(); // violation
199 *     var b = new SomeObject(); // OK
200 *   }
201 * }
202 * </pre>
203 * <p>
204 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
205 * </p>
206 * <p>
207 * Violation Message Keys:
208 * </p>
209 * <ul>
210 * <li>
211 * {@code matchxpath.match}
212 * </li>
213 * </ul>
214 *
215 * @since 8.39
216 */
217@StatelessCheck
218public class MatchXpathCheck extends AbstractCheck {
219
220    /**
221     * A key is pointing to the warning message text provided by user.
222     */
223    public static final String MSG_KEY = "matchxpath.match";
224
225    /** Specify Xpath query. */
226    private String query = "";
227
228    /** Xpath expression. */
229    private XPathExpression xpathExpression;
230
231    /**
232     * Setter to specify Xpath query.
233     *
234     * @param query Xpath query.
235     * @throws IllegalStateException if creation of xpath expression fails
236     */
237    public void setQuery(String query) {
238        this.query = query;
239        if (!query.isEmpty()) {
240            try {
241                final XPathEvaluator xpathEvaluator =
242                        new XPathEvaluator(Configuration.newConfiguration());
243                xpathExpression = xpathEvaluator.createExpression(query);
244            }
245            catch (XPathException ex) {
246                throw new IllegalStateException("Creating Xpath expression failed: " + query, ex);
247            }
248        }
249    }
250
251    @Override
252    public int[] getDefaultTokens() {
253        return getRequiredTokens();
254    }
255
256    @Override
257    public int[] getAcceptableTokens() {
258        return getRequiredTokens();
259    }
260
261    @Override
262    public int[] getRequiredTokens() {
263        return CommonUtil.EMPTY_INT_ARRAY;
264    }
265
266    @Override
267    public boolean isCommentNodesRequired() {
268        return true;
269    }
270
271    @Override
272    public void beginTree(DetailAST rootAST) {
273        if (xpathExpression != null) {
274            final List<DetailAST> matchingNodes = findMatchingNodesByXpathQuery(rootAST);
275            matchingNodes.forEach(node -> log(node, MSG_KEY));
276        }
277    }
278
279    /**
280     * Find nodes that match query.
281     *
282     * @param rootAST root node
283     * @return list of matching nodes
284     * @throws IllegalStateException if evaluation of xpath query fails
285     */
286    private List<DetailAST> findMatchingNodesByXpathQuery(DetailAST rootAST) {
287        try {
288            final RootNode rootNode = new RootNode(rootAST);
289            final XPathDynamicContext xpathDynamicContext =
290                    xpathExpression.createDynamicContext(rootNode);
291            final List<Item> matchingItems = xpathExpression.evaluate(xpathDynamicContext);
292            return matchingItems.stream()
293                    .map(item -> ((AbstractNode) item).getUnderlyingNode())
294                    .collect(Collectors.toList());
295        }
296        catch (XPathException ex) {
297            throw new IllegalStateException("Evaluation of Xpath query failed: " + query, ex);
298        }
299    }
300}