001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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; 023 024import com.puppycrawl.tools.checkstyle.StatelessCheck; 025import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 026import com.puppycrawl.tools.checkstyle.api.DetailAST; 027import com.puppycrawl.tools.checkstyle.utils.CommonUtil; 028import com.puppycrawl.tools.checkstyle.xpath.AbstractNode; 029import com.puppycrawl.tools.checkstyle.xpath.RootNode; 030import net.sf.saxon.Configuration; 031import net.sf.saxon.om.Item; 032import net.sf.saxon.sxpath.XPathDynamicContext; 033import net.sf.saxon.sxpath.XPathEvaluator; 034import net.sf.saxon.sxpath.XPathExpression; 035import net.sf.saxon.trans.XPathException; 036 037/** 038 * <div> 039 * Evaluates Xpath query and report violation on all matching AST nodes. This check allows 040 * user to implement custom checks using Xpath. If Xpath query is not specified explicitly, 041 * then the check does nothing. 042 * </div> 043 * 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 * 051 * <p> 052 * Please read more about Xpath syntax at 053 * <a href="https://www.saxonica.com/html/documentation10/expressions/index.html">Xpath Syntax</a>. 054 * Information regarding Xpath functions can be found at 055 * <a href="https://www.saxonica.com/html/documentation10/functions/fn/index.html"> 056 * XSLT/XPath Reference</a>. 057 * Note, that <b>@text</b> attribute can be used only with token types that are listed in 058 * <a href="https://github.com/checkstyle/checkstyle/search?q=%22TOKEN_TYPES_WITH_TEXT_ATTRIBUTE+%3D+Arrays.asList%22"> 059 * XpathUtil</a>. 060 * </p> 061 * 062 * @since 8.39 063 */ 064@StatelessCheck 065public class MatchXpathCheck extends AbstractCheck { 066 067 /** 068 * A key is pointing to the warning message text provided by user. 069 */ 070 public static final String MSG_KEY = "matchxpath.match"; 071 072 /** Specify Xpath query. */ 073 private String query = ""; 074 075 /** Xpath expression. */ 076 private XPathExpression xpathExpression; 077 078 /** 079 * Creates a new {@code MatchXpathCheck} instance. 080 */ 081 public MatchXpathCheck() { 082 // no code by default 083 } 084 085 /** 086 * Setter to specify Xpath query. 087 * 088 * @param query Xpath query. 089 * @throws IllegalStateException if creation of xpath expression fails 090 * @since 8.39 091 */ 092 public void setQuery(String query) { 093 this.query = query; 094 if (!query.isEmpty()) { 095 try { 096 final XPathEvaluator xpathEvaluator = 097 new XPathEvaluator(Configuration.newConfiguration()); 098 xpathExpression = xpathEvaluator.createExpression(query); 099 } 100 catch (XPathException exc) { 101 throw new IllegalStateException("Creating Xpath expression failed: " + query, exc); 102 } 103 } 104 } 105 106 @Override 107 public int[] getDefaultTokens() { 108 return getRequiredTokens(); 109 } 110 111 @Override 112 public int[] getAcceptableTokens() { 113 return getRequiredTokens(); 114 } 115 116 @Override 117 public int[] getRequiredTokens() { 118 return CommonUtil.EMPTY_INT_ARRAY; 119 } 120 121 @Override 122 public boolean isCommentNodesRequired() { 123 return true; 124 } 125 126 @Override 127 public void beginTree(DetailAST rootAST) { 128 if (!query.isEmpty()) { 129 final List<DetailAST> matchingNodes = findMatchingNodesByXpathQuery(rootAST); 130 matchingNodes.forEach(node -> log(node, MSG_KEY)); 131 } 132 } 133 134 /** 135 * Find nodes that match query. 136 * 137 * @param rootAST root node 138 * @return list of matching nodes 139 * @throws IllegalStateException if evaluation of xpath query fails 140 */ 141 private List<DetailAST> findMatchingNodesByXpathQuery(DetailAST rootAST) { 142 try { 143 final RootNode rootNode = new RootNode(rootAST); 144 final XPathDynamicContext xpathDynamicContext = 145 xpathExpression.createDynamicContext(rootNode); 146 final List<Item> matchingItems = xpathExpression.evaluate(xpathDynamicContext); 147 return matchingItems.stream() 148 .map(item -> (DetailAST) ((AbstractNode) item).getUnderlyingNode()) 149 .toList(); 150 } 151 catch (XPathException exc) { 152 throw new IllegalStateException("Evaluation of Xpath query failed: " + query, exc); 153 } 154 } 155 156}