1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package com.puppycrawl.tools.checkstyle.internal;
21
22 import static com.google.common.truth.Truth.assertWithMessage;
23
24 import java.io.IOException;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.Paths;
28 import java.util.ArrayList;
29 import java.util.HashMap;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.Optional;
34 import java.util.Set;
35 import java.util.stream.Collectors;
36 import java.util.stream.Stream;
37
38 import javax.xml.parsers.ParserConfigurationException;
39
40 import org.junit.jupiter.api.Test;
41 import org.w3c.dom.Document;
42 import org.w3c.dom.Element;
43 import org.w3c.dom.Node;
44 import org.w3c.dom.NodeList;
45
46 import com.puppycrawl.tools.checkstyle.AbstractModuleTestSupport;
47 import com.puppycrawl.tools.checkstyle.internal.utils.XmlUtil;
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67 public class XdocsCategoryIndexTest extends AbstractModuleTestSupport {
68
69 private static final Path XDOC_CHECKS_DIR = Paths.get("src", "site", "xdoc", "checks");
70
71 @Override
72 protected String getPackageLocation() {
73 return "com.puppycrawl.tools.checkstyle.internal";
74 }
75
76 @Test
77 public void testAllChecksListedInCategoryIndexAndDescriptionMatches() throws Exception {
78 final List<Path> checkXdocFiles = getCheckXdocFiles();
79
80 for (final Path checkXdocFile : checkXdocFiles) {
81 final String mainSectionName = getMainSectionName(checkXdocFile);
82 final Path categoryDir = checkXdocFile.getParent();
83 final Path categoryIndexFile = categoryDir.resolve("index.xml");
84
85 assertWithMessage("Category index file should exist for check: %s", checkXdocFile)
86 .that(Files.exists(categoryIndexFile)).isTrue();
87
88 final Map<String, CheckIndexInfo> indexedChecks = parseCategoryIndex(categoryIndexFile);
89 final Set<String> foundKeys = indexedChecks.keySet();
90
91 final String checkNotFoundFmt = "Check '%s' from %s not in %s. Found Checks: %s";
92 final String checkNotFoundMsg = String.format(Locale.ROOT,
93 checkNotFoundFmt,
94 mainSectionName, checkXdocFile.getFileName(), categoryIndexFile, foundKeys);
95 assertWithMessage(checkNotFoundMsg)
96 .that(indexedChecks.containsKey(mainSectionName)).isTrue();
97
98 final CheckIndexInfo checkInfoFromIndex = indexedChecks.get(mainSectionName);
99 final String internalErrorMsg = String.format(Locale.ROOT,
100 "CheckInfo for '%s' null (key present). Test error.", mainSectionName);
101 assertWithMessage(internalErrorMsg)
102 .that(checkInfoFromIndex)
103 .isNotNull();
104
105
106 final String expectedHrefFileName = checkXdocFile.getFileName().toString()
107 .replace(".xml", ".html");
108 final String expectedHref = expectedHrefFileName.toLowerCase(Locale.ROOT)
109 + "#" + mainSectionName;
110 final String actualHref = checkInfoFromIndex.href();
111
112 final String hrefMismatchFmt = "Href mismatch for '%s' in %s."
113 + "Expected: '%s', Found: '%s'";
114 final String hrefMismatchMsg = String.format(Locale.ROOT,
115 hrefMismatchFmt,
116 mainSectionName, categoryIndexFile, expectedHref, actualHref);
117 assertWithMessage(hrefMismatchMsg)
118 .that(actualHref).isEqualTo(expectedHref);
119
120
121 final String descriptionFromXdoc = getCheckDescriptionFromXdoc(checkXdocFile);
122 final String descriptionFromIndex = checkInfoFromIndex.description();
123 final String normalizedIndexDesc = normalizeText(descriptionFromIndex);
124 final String normalizedXdocDesc = normalizeText(descriptionFromXdoc);
125
126 final String descMismatchFmt = "Check '%s' in index '%s': "
127 + "index description is not a prefix of XDoc description.";
128 final String descMismatchMsg = String.format(Locale.ROOT,
129 descMismatchFmt,
130 mainSectionName, categoryIndexFile);
131 assertWithMessage(descMismatchMsg)
132 .that(normalizedXdocDesc)
133 .startsWith(normalizedIndexDesc);
134 }
135 }
136
137
138
139
140
141
142
143
144 private static List<Path> getCheckXdocFiles() throws IOException {
145 try (Stream<Path> paths = Files.walk(XDOC_CHECKS_DIR)) {
146 return paths
147 .filter(Files::isRegularFile)
148 .filter(path -> path.toString().endsWith(".xml"))
149 .filter(path -> !"index.xml".equals(path.getFileName().toString()))
150 .filter(path -> !"property_types.xml".equals(path.getFileName().toString()))
151 .collect(Collectors.toUnmodifiableList());
152 }
153 }
154
155
156
157
158
159
160
161
162
163
164
165 private static String getMainSectionName(Path checkXdocFile)
166 throws ParserConfigurationException, IOException {
167 final String content = Files.readString(checkXdocFile);
168 final Document document = XmlUtil.getRawXml(checkXdocFile.toString(), content, content);
169 final NodeList sections = document.getElementsByTagName("section");
170
171 for (int sectionIndex = 0; sectionIndex < sections.getLength(); sectionIndex++) {
172 final Node sectionNode = sections.item(sectionIndex);
173 if (sectionNode instanceof Element) {
174 final Element sectionElement = (Element) sectionNode;
175 if (sectionElement.hasAttribute("name")) {
176 return sectionElement.getAttribute("name");
177 }
178 }
179 }
180 final String errorFormat = "No <section name=...> found in %s";
181 final String errorMsg = String.format(Locale.ROOT, errorFormat, checkXdocFile);
182 throw new AssertionError(errorMsg);
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198 private static String getCheckDescriptionFromXdoc(Path checkXdocFile)
199 throws ParserConfigurationException, IOException {
200 final String content = Files.readString(checkXdocFile);
201 final Document document = XmlUtil.getRawXml(checkXdocFile.toString(), content, content);
202 final NodeList subsections = document.getElementsByTagName("subsection");
203
204 for (int subsectionIdx = 0; subsectionIdx < subsections.getLength(); subsectionIdx++) {
205 final Node subsectionNode = subsections.item(subsectionIdx);
206 if (subsectionNode instanceof Element) {
207 final Element subsectionElement = (Element) subsectionNode;
208 if ("Description".equals(subsectionElement.getAttribute("name"))) {
209 final Optional<String> description =
210 getDescriptionFromSubsection(subsectionElement);
211 if (description.isPresent()) {
212 return description.get();
213 }
214 }
215 }
216 }
217 final String errorFormat =
218 "No <subsection name=\"Description\"> with suitable content in %s";
219 final String errorMsg = String.format(Locale.ROOT, errorFormat, checkXdocFile);
220 throw new AssertionError(errorMsg);
221 }
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237 private static Optional<String> getDescriptionFromSubsection(Element subsectionElement) {
238 Optional<String> description = Optional.empty();
239 final Optional<String> textFromDiv = findTextInChildElements(subsectionElement, "div");
240 if (textFromDiv.isPresent()) {
241 description = textFromDiv;
242 }
243
244 if (description.isEmpty()) {
245 final Optional<String> textFromP = findTextInChildElements(subsectionElement, "p");
246 if (textFromP.isPresent()) {
247 description = textFromP;
248 }
249 }
250
251 if (description.isEmpty()) {
252 final Optional<String> aggregatedText = getAggregatedDirectText(subsectionElement);
253 if (aggregatedText.isPresent()) {
254 description = aggregatedText;
255 }
256 }
257
258 if (description.isEmpty()) {
259 final String fullSubsectionText = subsectionElement.getTextContent();
260 if (fullSubsectionText != null && !fullSubsectionText.isBlank()) {
261 description = Optional.of(fullSubsectionText);
262 }
263 }
264 return description;
265 }
266
267
268
269
270
271
272
273
274 private static Optional<String> findTextInChildElements(Element parent, String tagName) {
275 Optional<String> foundText = Optional.empty();
276 for (final Element childElement : getChildrenElementsByTagName(parent, tagName)) {
277 final String text = childElement.getTextContent();
278 if (text != null && !text.isBlank()) {
279 foundText = Optional.of(text);
280 break;
281 }
282 }
283 return foundText;
284 }
285
286
287
288
289
290
291
292 private static Optional<String> getAggregatedDirectText(Element parent) {
293 final StringBuilder directTextContent = new StringBuilder(32);
294 final NodeList directChildren = parent.getChildNodes();
295 for (int childIdx = 0; childIdx < directChildren.getLength(); childIdx++) {
296 final Node directChild = directChildren.item(childIdx);
297 if (directChild.getNodeType() == Node.TEXT_NODE) {
298 directTextContent.append(directChild.getNodeValue());
299 }
300 }
301 final String aggregatedText = directTextContent.toString();
302 Optional<String> result = Optional.empty();
303 if (!aggregatedText.isBlank()) {
304 result = Optional.of(aggregatedText);
305 }
306 return result;
307 }
308
309
310
311
312
313
314
315
316
317
318
319
320 private static Map<String, CheckIndexInfo> parseCategoryIndex(Path categoryIndexFile)
321 throws ParserConfigurationException, IOException {
322 final Map<String, CheckIndexInfo> indexedChecks = new HashMap<>();
323 final String content = Files.readString(categoryIndexFile);
324 final Document document = XmlUtil.getRawXml(categoryIndexFile.toString(), content, content);
325 final NodeList tableNodes = document.getElementsByTagName("table");
326
327 if (tableNodes.getLength() == 0) {
328 final String errorMsg = String.format(Locale.ROOT,
329 "No <table> found in %s", categoryIndexFile);
330 throw new AssertionError(errorMsg);
331 }
332
333 for (int tableIdx = 0; tableIdx < tableNodes.getLength(); tableIdx++) {
334 final Node tableNode = tableNodes.item(tableIdx);
335 if (tableNode instanceof Element) {
336 processTableElement((Element) tableNode, indexedChecks);
337 }
338 }
339 return indexedChecks;
340 }
341
342
343
344
345
346
347
348
349 private static void processTableElement(Element tableElement,
350 Map<String, CheckIndexInfo> indexedChecks) {
351 final List<Element> rowElements = getChildrenElementsByTagName(tableElement, "tr");
352 boolean isFirstRowInTable = true;
353
354 for (final Element rowElement : rowElements) {
355 if (isFirstRowInTable) {
356 isFirstRowInTable = false;
357 if (isHeaderRow(rowElement)) {
358 continue;
359 }
360 }
361 processDataRow(rowElement, indexedChecks);
362 }
363 }
364
365
366
367
368
369
370
371 private static boolean isHeaderRow(Element rowElement) {
372 return !getChildrenElementsByTagName(rowElement, "th").isEmpty();
373 }
374
375
376
377
378
379
380
381
382 private static void processDataRow(Element rowElement,
383 Map<String, CheckIndexInfo> indexedChecks) {
384 final List<Element> cellElements = getChildrenElementsByTagName(rowElement, "td");
385 if (cellElements.size() >= 2) {
386 final Element nameCell = cellElements.get(0);
387 final Element descCell = cellElements.get(1);
388
389 getFirstChildElementByTagName(nameCell, "a").ifPresent(anchorElement -> {
390 if (anchorElement.hasAttribute("href")) {
391 final String checkNameInIndex = anchorElement.getTextContent().trim();
392 final String href = anchorElement.getAttribute("href");
393 final String description = descCell.getTextContent();
394 indexedChecks.put(checkNameInIndex,
395 new CheckIndexInfo(href, description));
396 }
397 });
398 }
399 }
400
401
402
403
404
405
406
407
408 private static List<Element> getChildrenElementsByTagName(Node parent, String tagName) {
409 final List<Element> elements = new ArrayList<>();
410 if (parent != null) {
411 final NodeList children = parent.getChildNodes();
412 for (int childIdx = 0; childIdx < children.getLength(); childIdx++) {
413 final Node child = children.item(childIdx);
414 if (child instanceof Element && tagName.equals(child.getNodeName())) {
415 elements.add((Element) child);
416 }
417 }
418 }
419 return elements;
420 }
421
422
423
424
425
426
427
428
429
430 private static Optional<Element> getFirstChildElementByTagName(Node parent, String tagName) {
431 Optional<Element> result = Optional.empty();
432 if (parent != null) {
433 final NodeList children = parent.getChildNodes();
434 for (int childIdx = 0; childIdx < children.getLength(); childIdx++) {
435 final Node child = children.item(childIdx);
436 if (child instanceof Element && tagName.equals(child.getNodeName())) {
437 result = Optional.of((Element) child);
438 break;
439 }
440 }
441 }
442 return result;
443 }
444
445
446
447
448
449
450
451
452 private static String normalizeText(String text) {
453 String normalized = "";
454 if (text != null) {
455 normalized = text.replace("\u00a0", " ").trim().replaceAll("\\s+", " ");
456 }
457 return normalized;
458 }
459
460
461
462
463
464 private static final class CheckIndexInfo {
465 private final String hrefValue;
466 private final String descriptionText;
467
468
469
470
471
472
473
474 CheckIndexInfo(String href, String description) {
475 hrefValue = href;
476 descriptionText = description;
477 }
478
479
480
481
482
483
484 public String href() {
485 return hrefValue;
486 }
487
488
489
490
491
492
493 public String description() {
494 return descriptionText;
495 }
496 }
497 }