View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.filters;
21  
22  import java.util.List;
23  import java.util.Objects;
24  import java.util.Optional;
25  import java.util.regex.Pattern;
26  
27  import com.puppycrawl.tools.checkstyle.TreeWalkerAuditEvent;
28  import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
29  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
30  import com.puppycrawl.tools.checkstyle.xpath.AbstractNode;
31  import com.puppycrawl.tools.checkstyle.xpath.RootNode;
32  import net.sf.saxon.Configuration;
33  import net.sf.saxon.om.Item;
34  import net.sf.saxon.sxpath.XPathDynamicContext;
35  import net.sf.saxon.sxpath.XPathEvaluator;
36  import net.sf.saxon.sxpath.XPathExpression;
37  import net.sf.saxon.trans.XPathException;
38  
39  /**
40   * This filter element is immutable and processes {@link TreeWalkerAuditEvent}
41   * objects based on the criteria of file, check, module id, xpathQuery.
42   *
43   */
44  public class XpathFilterElement implements TreeWalkerFilter {
45  
46      /** The regexp to match file names against. */
47      private final Pattern fileRegexp;
48  
49      /** The regexp to match check names against. */
50      private final Pattern checkRegexp;
51  
52      /** The regexp to match message names against. */
53      private final Pattern messageRegexp;
54  
55      /** Module id filter. */
56      private final String moduleId;
57  
58      /** Xpath expression. */
59      private final XPathExpression xpathExpression;
60  
61      /** Xpath query. */
62      private final String xpathQuery;
63  
64      /** Indicates if all properties are set to null. */
65      private final boolean isEmptyConfig;
66  
67      /**
68       * Creates a {@code XpathElement} instance.
69       *
70       * @param files regular expression for names of filtered files
71       * @param checks regular expression for filtered check classes
72       * @param message regular expression for messages.
73       * @param moduleId the module id
74       * @param query the xpath query
75       * @throws IllegalArgumentException if the xpath query is not expected.
76       */
77      public XpathFilterElement(String files, String checks,
78                         String message, String moduleId, String query) {
79          this(Optional.ofNullable(files).map(Pattern::compile).orElse(null),
80               Optional.ofNullable(checks).map(CommonUtil::createPattern).orElse(null),
81               Optional.ofNullable(message).map(Pattern::compile).orElse(null),
82               moduleId,
83               query);
84      }
85  
86      /**
87       * Creates a {@code XpathElement} instance.
88       *
89       * @param files regular expression for names of filtered files
90       * @param checks regular expression for filtered check classes
91       * @param message regular expression for messages.
92       * @param moduleId the module id
93       * @param query the xpath query
94       * @throws IllegalArgumentException if the xpath query is not correct.
95       */
96      public XpathFilterElement(Pattern files, Pattern checks, Pattern message,
97                             String moduleId, String query) {
98          fileRegexp = files;
99          checkRegexp = checks;
100         messageRegexp = message;
101         this.moduleId = moduleId;
102         xpathQuery = query;
103         if (xpathQuery == null) {
104             xpathExpression = null;
105         }
106         else {
107             final XPathEvaluator xpathEvaluator = new XPathEvaluator(
108                     Configuration.newConfiguration());
109             try {
110                 xpathExpression = xpathEvaluator.createExpression(xpathQuery);
111             }
112             catch (XPathException exc) {
113                 throw new IllegalArgumentException("Incorrect xpath query: " + xpathQuery, exc);
114             }
115         }
116         isEmptyConfig = fileRegexp == null
117                              && checkRegexp == null
118                              && messageRegexp == null
119                              && moduleId == null
120                              && xpathExpression == null;
121     }
122 
123     @Override
124     public boolean accept(TreeWalkerAuditEvent event) {
125         return isEmptyConfig
126                 || !isFileNameAndModuleAndModuleNameMatching(event)
127                 || !isMessageNameMatching(event)
128                 || !isXpathQueryMatching(event);
129     }
130 
131     /**
132      * Is matching by file name, module id and Check name.
133      *
134      * @param event event
135      * @return true if it is matching
136      */
137     private boolean isFileNameAndModuleAndModuleNameMatching(TreeWalkerAuditEvent event) {
138         return event.getFileName() != null
139                 && (fileRegexp == null || fileRegexp.matcher(event.getFileName()).find())
140                 && event.getViolation() != null
141                 && (moduleId == null || moduleId.equals(event.getModuleId()))
142                 && (checkRegexp == null || checkRegexp.matcher(event.getSourceName()).find());
143     }
144 
145     /**
146      * Is matching by message.
147      *
148      * @param event event
149      * @return true if it is matching or not set.
150      */
151     private boolean isMessageNameMatching(TreeWalkerAuditEvent event) {
152         return messageRegexp == null || messageRegexp.matcher(event.getMessage()).find();
153     }
154 
155     /**
156      * Is matching by xpath query.
157      *
158      * @param event event
159      * @return true if it is matching or not set.
160      */
161     private boolean isXpathQueryMatching(TreeWalkerAuditEvent event) {
162         boolean isMatching;
163         if (xpathExpression == null) {
164             isMatching = true;
165         }
166         else {
167             isMatching = false;
168             final List<AbstractNode> nodes = getItems(event)
169                 .stream().map(AbstractNode.class::cast)
170                 .toList();
171             for (AbstractNode abstractNode : nodes) {
172                 isMatching = abstractNode.getTokenType() == event.getTokenType()
173                         && abstractNode.getLineNumber() == event.getLine()
174                         && abstractNode.getColumnNumber() == event.getColumnCharIndex();
175                 if (isMatching) {
176                     break;
177                 }
178             }
179         }
180         return isMatching;
181     }
182 
183     /**
184      * Returns list of nodes matching xpath expression given event.
185      *
186      * @param event {@code TreeWalkerAuditEvent} object
187      * @return list of nodes matching xpath expression given event
188      * @throws IllegalStateException if the xpath query could not be evaluated.
189      */
190     private List<Item> getItems(TreeWalkerAuditEvent event) {
191         final RootNode rootNode;
192         if (event.getRootAst() == null) {
193             rootNode = null;
194         }
195         else {
196             rootNode = new RootNode(event.getRootAst());
197         }
198         final List<Item> items;
199         try {
200             final XPathDynamicContext xpathDynamicContext =
201                     xpathExpression.createDynamicContext(rootNode);
202             items = xpathExpression.evaluate(xpathDynamicContext);
203         }
204         catch (XPathException exc) {
205             throw new IllegalStateException("Cannot initialize context and evaluate query: "
206                     + xpathQuery, exc);
207         }
208         return items;
209     }
210 
211     @Override
212     public int hashCode() {
213         return Objects.hash(getPatternSafely(fileRegexp), getPatternSafely(checkRegexp),
214                 getPatternSafely(messageRegexp), moduleId, xpathQuery);
215     }
216 
217     @Override
218     public boolean equals(Object other) {
219         if (this == other) {
220             return true;
221         }
222         if (other == null || getClass() != other.getClass()) {
223             return false;
224         }
225         final XpathFilterElement xpathFilter = (XpathFilterElement) other;
226         return Objects.equals(getPatternSafely(fileRegexp),
227                     getPatternSafely(xpathFilter.fileRegexp))
228                 && Objects.equals(getPatternSafely(checkRegexp),
229                     getPatternSafely(xpathFilter.checkRegexp))
230                 && Objects.equals(getPatternSafely(messageRegexp),
231                     getPatternSafely(xpathFilter.messageRegexp))
232                 && Objects.equals(moduleId, xpathFilter.moduleId)
233                 && Objects.equals(xpathQuery, xpathFilter.xpathQuery);
234     }
235 
236     /**
237      * Util method to get pattern String value from Pattern object safely, return null if
238      * pattern object is null.
239      *
240      * @param pattern pattern object
241      * @return value of pattern or null
242      */
243     private static String getPatternSafely(Pattern pattern) {
244         String result = null;
245         if (pattern != null) {
246             result = pattern.pattern();
247         }
248         return result;
249     }
250 }