View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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.internal.utils;
21  
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  import java.util.stream.Collectors;
34  import java.util.stream.Stream;
35  
36  import javax.xml.parsers.DocumentBuilder;
37  import javax.xml.parsers.DocumentBuilderFactory;
38  import javax.xml.parsers.ParserConfigurationException;
39  
40  import org.w3c.dom.Document;
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  import org.w3c.dom.NodeList;
44  import org.xml.sax.SAXException;
45  
46  /**
47   * XdocUtil.
48   *
49   * @noinspection ClassOnlyUsedInOnePackage
50   * @noinspectionreason ClassOnlyUsedInOnePackage - class is internal tool, and only used in testing
51   */
52  public final class XdocUtil {
53  
54      public static final String DIRECTORY_PATH = "src/site/xdoc";
55  
56      private XdocUtil() {
57      }
58  
59      /**
60       * Gets xdocs file paths.
61       *
62       * @return a set of xdocs file paths.
63       * @throws IOException if an I/O error occurs.
64       */
65      public static Set<Path> getXdocsFilePaths() throws IOException {
66          final Path directory = Path.of(DIRECTORY_PATH);
67          try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
68                  (path, attr) -> {
69                      return attr.isRegularFile()
70                              && (path.toString().endsWith(".xml")
71                              || path.toString().endsWith(".xml.vm"));
72                  })) {
73              return stream.collect(Collectors.toUnmodifiableSet());
74          }
75      }
76  
77      /**
78       * Gets xdocs template file paths. These are files ending with .xml.template.
79       *
80       * @return a set of xdocs template file paths.
81       * @throws IOException if an I/O error occurs.
82       */
83      public static Set<Path> getXdocsTemplatesFilePaths() throws IOException {
84          final Path directory = Path.of(DIRECTORY_PATH);
85          try (Stream<Path> stream = Files.find(directory, Integer.MAX_VALUE,
86                  (path, attr) -> {
87                      return attr.isRegularFile()
88                              && path.toString().endsWith(".xml.template");
89                  })) {
90              return stream.collect(Collectors.toUnmodifiableSet());
91          }
92      }
93  
94      /**
95       * Read the documented property names from a module's generated xdoc page.
96       *
97       * @param moduleName module class simple name
98       * @return documented property names
99       */
100     public static Set<String> getDocumentedProperties(String moduleName) {
101         String pageName = moduleName;
102         if (pageName.endsWith("Check")) {
103             pageName = pageName.substring(0, pageName.length() - "Check".length());
104         }
105         final String fileName = pageName.toLowerCase(Locale.ROOT) + ".xml";
106         final Set<String> result = new HashSet<>();
107 
108         try {
109             Path xdocPath = null;
110             for (Path path : getXdocsFilePaths()) {
111                 if (path.getFileName().toString().equals(fileName)) {
112                     xdocPath = path;
113                     break;
114                 }
115             }
116             if (xdocPath == null) {
117                 throw new IllegalStateException("Generated xdoc does not exist: " + fileName);
118             }
119             final String content = Files.readString(xdocPath);
120             final Document document = XmlUtil.getRawXml(fileName, content, content);
121             final NodeList subsections = document.getElementsByTagName("subsection");
122             for (int index = 0; index < subsections.getLength(); index++) {
123                 final Element subsection = (Element) subsections.item(index);
124                 if ("Properties".equals(subsection.getAttribute("name"))) {
125                     final NodeList rows = subsection.getElementsByTagName("tr");
126                     for (int rowIndex = 1; rowIndex < rows.getLength(); rowIndex++) {
127                         final NodeList columns = ((Element) rows.item(rowIndex))
128                                 .getElementsByTagName("td");
129                         if (columns.getLength() > 0) {
130                             result.add(columns.item(0).getTextContent().trim());
131                         }
132                     }
133                     break;
134                 }
135             }
136         }
137         catch (IOException | ParserConfigurationException exception) {
138             throw new IllegalStateException("Failed to read generated xdoc: " + fileName,
139                     exception);
140         }
141         return Set.copyOf(result);
142     }
143 
144     /**
145      * Gets xdocs documentation file paths.
146      *
147      * @param files set of all xdoc files
148      * @return a set of xdocs config file paths.
149      */
150     public static Set<Path> getXdocsConfigFilePaths(Set<Path> files) {
151         final Set<Path> xdocs = new HashSet<>();
152         for (Path entry : files) {
153             final String fileName = entry.getFileName().toString();
154             if (!entry.getParent().toString().matches("src[\\\\/]site[\\\\/]xdocs")
155                     && fileName.endsWith(".xml")) {
156                 xdocs.add(entry);
157             }
158         }
159         return xdocs;
160     }
161 
162     /**
163      * Gets xdocs style file paths.
164      *
165      * @param files set of all xdoc files
166      * @return a set of xdocs style file paths.
167      */
168     public static Set<Path> getXdocsStyleFilePaths(Set<Path> files) {
169         final Set<Path> xdocs = new HashSet<>();
170         for (Path entry : files) {
171             final String fileName = entry.getFileName().toString();
172             if (fileName.endsWith("_style.xml")) {
173                 xdocs.add(entry);
174             }
175         }
176         return xdocs;
177     }
178 
179     /**
180      * Gets names of checkstyle's modules which are documented in xdocs.
181      *
182      * @return a set of checkstyle's modules which have xdoc documentation.
183      * @throws IOException if any IO errors occur.
184      * @throws ParserConfigurationException if a DocumentBuilder cannot be created which satisfies
185      *              the configuration requested.
186      * @throws SAXException if any parse errors occur.
187      */
188     public static Set<String> getModulesNamesWhichHaveXdoc() throws Exception {
189         final DocumentBuilderFactory factory = DocumentBuilderFactory
190                 .newInstance();
191 
192         // Validations of XML file make parsing too slow, that is why we disable
193         // all validations.
194         factory.setNamespaceAware(false);
195         factory.setValidating(false);
196         factory.setFeature("http://xml.org/sax/features/namespaces", false);
197         factory.setFeature("http://xml.org/sax/features/validation", false);
198         factory.setFeature(
199                 "http://apache.org/xml/features/nonvalidating/load-dtd-grammar",
200                 false);
201         factory.setFeature(
202                 "http://apache.org/xml/features/nonvalidating/load-external-dtd",
203                 false);
204 
205         final Set<String> modulesNamesWhichHaveXdoc = new HashSet<>();
206 
207         for (Path path : getXdocsConfigFilePaths(getXdocsFilePaths())) {
208             final DocumentBuilder builder = factory.newDocumentBuilder();
209             final Document document = builder.parse(path.toFile());
210 
211             // optional, but recommended
212             // FYI:
213             // http://stackoverflow.com/questions/13786607/normalization-in-dom-parsing-with-
214             // java-how-does-it-work
215             document.getDocumentElement().normalize();
216 
217             final NodeList nodeList = document.getElementsByTagName("section");
218 
219             for (int i = 0; i < nodeList.getLength(); i++) {
220                 final Node currentNode = nodeList.item(i);
221                 if (currentNode.getNodeType() == Node.ELEMENT_NODE) {
222                     final Element module = (Element) currentNode;
223                     final String moduleName = module.getAttribute("name");
224                     if (!"Content".equals(moduleName)
225                             && !"Overview".equals(moduleName)) {
226                         modulesNamesWhichHaveXdoc.add(moduleName);
227                     }
228                 }
229             }
230         }
231         return modulesNamesWhichHaveXdoc;
232     }
233 
234     /**
235      * Extracts used properties from XDocs examples (from /*xml blocks).
236      *
237      * @return a map of Check name -> Set of used property names.
238      * @throws IOException if file I/O fails.
239      */
240     public static Map<String, Set<String>> extractUsedPropertiesFromXdocsExamples()
241             throws IOException {
242         final List<Path> roots = List.of(
243                 Path.of("src/xdocs-examples/resources/com/puppycrawl/tools/checkstyle/checks"),
244                 Path.of("src/xdocs-examples/resources-noncompilable/"
245                         + "com/puppycrawl/tools/checkstyle/checks")
246         );
247 
248         final Map<String, Set<String>> checkToProperties = new HashMap<>();
249 
250         for (Path root : roots) {
251             if (Files.exists(root)) {
252                 try (Stream<Path> paths = Files.walk(root)) {
253                     paths.filter(path -> path.toString().endsWith(".java"))
254                             .forEach(path -> processXdocExampleFile(path, checkToProperties));
255                 }
256             }
257         }
258 
259         return checkToProperties;
260     }
261 
262     private static void processXdocExampleFile(
263             Path file, Map<String, Set<String>> checkToProperties) {
264         try {
265             final String content = Files.readString(file);
266             final Matcher xmlBlockMatcher =
267                 Pattern.compile("/\\*xml(.*?)\\*/", Pattern.DOTALL).matcher(content);
268 
269             if (xmlBlockMatcher.find()) {
270                 final Map.Entry<String, Set<String>> entry =
271                     parseConfigBlock(xmlBlockMatcher.group(1));
272 
273                 if (entry != null) {
274                     final String checkClassKey = entry.getKey();
275                     final Set<String> props = entry.getValue();
276                     checkToProperties
277                         .computeIfAbsent(checkClassKey, key -> new HashSet<>())
278                         .addAll(props);
279                     // Also store under module name without "Check" suffix to handle checks
280                     // like SuppressWarningsHolder whose class name does not end with "Check"
281                     if (checkClassKey.endsWith("Check")) {
282                         final String moduleNameKey = checkClassKey.substring(
283                                 0, checkClassKey.length() - "Check".length());
284                         checkToProperties
285                             .computeIfAbsent(moduleNameKey, key -> new HashSet<>())
286                             .addAll(props);
287                     }
288                 }
289             }
290         }
291         catch (IOException ioe) {
292             throw new IllegalStateException("Error reading file: " + file, ioe);
293         }
294     }
295 
296     private static Map.Entry<String, Set<String>> parseConfigBlock(String configBlock) {
297         final Matcher propMatcher =
298                 Pattern.compile("<property name=\"([^\"]+)\"").matcher(configBlock);
299         final Set<String> props = new HashSet<>();
300         int firstPropStart = -1;
301         while (propMatcher.find()) {
302             if (firstPropStart == -1) {
303                 firstPropStart = propMatcher.start();
304             }
305             props.add(propMatcher.group(1));
306         }
307 
308         Map.Entry<String, Set<String>> result = null;
309         if (!props.isEmpty()) {
310             // Find the last module before the first property to correctly associate
311             // properties with their owning module (handles cases where sibling modules
312             // appear after the module that owns the properties)
313             final Matcher moduleMatcher =
314                     Pattern.compile("<module name=\"([^\"]+)\"")
315                             .matcher(configBlock.substring(0, firstPropStart));
316             String lastModule = null;
317             while (moduleMatcher.find()) {
318                 lastModule = moduleMatcher.group(1);
319             }
320 
321             if (lastModule != null) {
322                 final String checkClassName;
323                 if (lastModule.endsWith("Check")) {
324                     checkClassName = lastModule;
325                 }
326                 else {
327                     checkClassName = lastModule + "Check";
328                 }
329                 result = Map.entry(checkClassName, props);
330             }
331         }
332 
333         return result;
334     }
335 
336 }