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.meta;
21  
22  import java.util.ArrayDeque;
23  import java.util.Arrays;
24  import java.util.Collections;
25  import java.util.Deque;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.LinkedHashSet;
29  import java.util.Locale;
30  import java.util.Map;
31  import java.util.Optional;
32  import java.util.Set;
33  import java.util.regex.Matcher;
34  import java.util.regex.Pattern;
35  import java.util.stream.Collectors;
36  
37  import javax.xml.parsers.ParserConfigurationException;
38  import javax.xml.transform.TransformerException;
39  
40  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
41  import com.puppycrawl.tools.checkstyle.api.DetailAST;
42  import com.puppycrawl.tools.checkstyle.api.DetailNode;
43  import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
44  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
45  import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
46  import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
47  
48  /**
49   * Class for scraping module metadata from the corresponding class' class-level javadoc.
50   */
51  @FileStatefulCheck
52  public class JavadocMetadataScraper extends AbstractJavadocCheck {
53  
54      /**
55       * A key is pointing to the warning message text in "messages.properties"
56       * file.
57       */
58      public static final String MSG_DESC_MISSING = "javadocmetadatascraper.description.missing";
59  
60      /** Module details store used for testing. */
61      private static final Map<String, ModuleDetails> MODULE_DETAILS_STORE = new HashMap<>();
62  
63      /** Regular expression for property location in class-level javadocs. */
64      private static final Pattern PROPERTY_TAG = Pattern.compile("\\s*Property\\s*");
65  
66      /** Regular expression for property type location in class-level javadocs. */
67      private static final Pattern TYPE_TAG = Pattern.compile("^ Type is\\s.*");
68  
69      /** Regular expression for property validation type location in class-level javadocs. */
70      private static final Pattern VALIDATION_TYPE_TAG =
71              Pattern.compile("\\s.*Validation type is\\s.*");
72  
73      /** Regular expression for property default value location in class-level javadocs. */
74      private static final Pattern DEFAULT_VALUE_TAG = Pattern.compile("^ Default value is:*.*");
75  
76      /** Regular expression for check example location in class-level javadocs. */
77      private static final Pattern EXAMPLES_TAG =
78              Pattern.compile("\\s*To configure the (default )?check.*");
79  
80      /** Regular expression for module parent location in class-level javadocs. */
81      private static final Pattern PARENT_TAG = Pattern.compile("\\s*Parent is\\s*");
82  
83      /** Regular expression for module violation messages location in class-level javadocs. */
84      private static final Pattern VIOLATION_MESSAGES_TAG =
85              Pattern.compile("\\s*Violation Message Keys:\\s*");
86  
87      /** Regular expression for detecting ANTLR tokens(for e.g. CLASS_DEF). */
88      private static final Pattern TOKEN_TEXT_PATTERN = Pattern.compile("([A-Z_]{2,})+");
89  
90      /** Regular expression for removal of @code{-} present at the beginning of texts. */
91      private static final Pattern DESC_CLEAN = Pattern.compile("-\\s");
92  
93      /** Regular expression for file separator corresponding to the host OS. */
94      private static final Pattern FILE_SEPARATOR_PATTERN =
95              Pattern.compile(Pattern.quote(System.getProperty("file.separator")));
96  
97      /** Regular expression for quotes. */
98      private static final Pattern QUOTE_PATTERN = Pattern.compile("\"");
99  
100     /** Java file extension. */
101     private static final String JAVA_FILE_EXTENSION = ".java";
102 
103     /**
104      * This set contains faulty property default value which should not be written to the XML
105      * metadata files.
106      */
107     private static final Set<String> PROPERTIES_TO_NOT_WRITE = Set.of(
108                     "null",
109                     "the charset property of the parent <a href=https://checkstyle.org/"
110                         + "config.html#Checker>Checker</a> module");
111 
112     /**
113      * Format for exception message for missing type for check property.
114      */
115     private static final String PROP_TYPE_MISSING = "Type for property '%s' is missing";
116 
117     /**
118      * Format for exception message for missing default value for check property.
119      */
120     private static final String PROP_DEFAULT_VALUE_MISSING =
121         "Default value for property '%s' is missing";
122 
123     /** ModuleDetails instance for each module AST traversal. */
124     private ModuleDetails moduleDetails;
125 
126     /**
127      * Boolean variable which lets us know whether violation message section is being scraped
128      * currently.
129      */
130     private boolean scrapingViolationMessageList;
131 
132     /**
133      * Boolean variable which lets us know whether we should scan and scrape the current javadoc
134      * or not. Since we need only class level javadoc, it becomes true at its root and false after
135      * encountering {@code JavadocTokenTypes.SINCE_LITERAL}.
136      */
137     private boolean toScan;
138 
139     /** DetailNode pointing to the root node of the class level javadoc of the class. */
140     private DetailNode rootNode;
141 
142     /**
143      * Child number of the property section node, where parent is the class level javadoc root
144      * node.
145      */
146     private int propertySectionStartIdx;
147 
148     /**
149      * Child number of the example section node, where parent is the class level javadoc root
150      * node.
151      */
152     private int exampleSectionStartIdx;
153 
154     /**
155      * Child number of the parent section node, where parent is the class level javadoc root
156      * node.
157      */
158     private int parentSectionStartIdx;
159 
160     /**
161      * Control whether to write XML output or not.
162      */
163     private boolean writeXmlOutput = true;
164 
165     /**
166      * Setter to control whether to write XML output or not.
167      *
168      * @param writeXmlOutput whether to write XML output or not.
169      */
170     public final void setWriteXmlOutput(boolean writeXmlOutput) {
171         this.writeXmlOutput = writeXmlOutput;
172     }
173 
174     @Override
175     public int[] getDefaultJavadocTokens() {
176         return new int[] {
177             JavadocTokenTypes.JAVADOC,
178             JavadocTokenTypes.PARAGRAPH,
179             JavadocTokenTypes.LI,
180             JavadocTokenTypes.SINCE_LITERAL,
181         };
182     }
183 
184     @Override
185     public int[] getRequiredJavadocTokens() {
186         return getAcceptableJavadocTokens();
187     }
188 
189     @Override
190     public void beginJavadocTree(DetailNode rootAst) {
191         if (isTopLevelClassJavadoc()) {
192             moduleDetails = new ModuleDetails();
193             toScan = false;
194             scrapingViolationMessageList = false;
195             propertySectionStartIdx = -1;
196             exampleSectionStartIdx = -1;
197             parentSectionStartIdx = -1;
198 
199             String moduleName = getModuleSimpleName();
200             final String checkModuleExtension = "Check";
201             if (moduleName.endsWith(checkModuleExtension)) {
202                 moduleName = moduleName
203                         .substring(0, moduleName.length() - checkModuleExtension.length());
204             }
205             moduleDetails.setName(moduleName);
206             moduleDetails.setFullQualifiedName(getPackageName(getFilePath()));
207             moduleDetails.setModuleType(getModuleType());
208         }
209     }
210 
211     @Override
212     public void visitJavadocToken(DetailNode ast) {
213         if (toScan) {
214             scrapeContent(ast);
215         }
216 
217         if (ast.getType() == JavadocTokenTypes.JAVADOC) {
218             final DetailAST parent = getParent(getBlockCommentAst());
219             if (parent.getType() == TokenTypes.CLASS_DEF) {
220                 rootNode = ast;
221                 toScan = true;
222             }
223         }
224         else if (ast.getType() == JavadocTokenTypes.SINCE_LITERAL) {
225             toScan = false;
226         }
227     }
228 
229     @Override
230     public void finishJavadocTree(DetailNode rootAst) {
231         moduleDetails.setDescription(getDescriptionText());
232         if (isTopLevelClassJavadoc()) {
233             if (moduleDetails.getDescription().isEmpty()) {
234                 final String fullQualifiedName = moduleDetails.getFullQualifiedName();
235                 log(rootAst.getLineNumber(), MSG_DESC_MISSING,
236                         fullQualifiedName.substring(fullQualifiedName.lastIndexOf('.') + 1));
237             }
238             else if (writeXmlOutput) {
239                 try {
240                     XmlMetaWriter.write(moduleDetails);
241                 }
242                 catch (TransformerException | ParserConfigurationException exc) {
243                     throw new IllegalStateException(
244                             "Failed to write metadata into XML file for module: "
245                                     + getModuleSimpleName(), exc);
246                 }
247             }
248             if (!writeXmlOutput) {
249                 MODULE_DETAILS_STORE.put(moduleDetails.getFullQualifiedName(), moduleDetails);
250             }
251 
252         }
253     }
254 
255     /**
256      * Method containing the core logic of scraping. This keeps track and decides which phase of
257      * scraping we are in, and accordingly call other subroutines.
258      *
259      * @param ast javadoc ast
260      */
261     private void scrapeContent(DetailNode ast) {
262         if (ast.getType() == JavadocTokenTypes.PARAGRAPH) {
263             if (isParentText(ast)) {
264                 parentSectionStartIdx = getParentIndexOf(ast);
265                 moduleDetails.setParent(getParentText(ast));
266             }
267             else if (isViolationMessagesText(ast)) {
268                 scrapingViolationMessageList = true;
269             }
270             else if (exampleSectionStartIdx == -1
271                     && isExamplesText(ast)) {
272                 exampleSectionStartIdx = getParentIndexOf(ast);
273             }
274         }
275         else if (ast.getType() == JavadocTokenTypes.LI) {
276             if (isPropertyList(ast)) {
277                 if (propertySectionStartIdx == -1) {
278                     propertySectionStartIdx = getParentIndexOf(ast);
279                 }
280                 moduleDetails.addToProperties(createProperties(ast));
281             }
282             else if (scrapingViolationMessageList) {
283                 moduleDetails.addToViolationMessages(getViolationMessages(ast));
284             }
285         }
286     }
287 
288     /**
289      * Create the modulePropertyDetails content.
290      *
291      * @param nodeLi list item javadoc node
292      * @return modulePropertyDetail object for the corresponding property
293      */
294     private static ModulePropertyDetails createProperties(DetailNode nodeLi) {
295         final ModulePropertyDetails modulePropertyDetails = new ModulePropertyDetails();
296 
297         final Optional<DetailNode> propertyNameNode = getFirstChildOfType(nodeLi,
298                 JavadocTokenTypes.JAVADOC_INLINE_TAG, 0);
299         if (propertyNameNode.isPresent()) {
300             final DetailNode propertyNameTag = propertyNameNode.orElseThrow();
301             final String propertyName = getTextFromTag(propertyNameTag);
302 
303             final DetailNode propertyType = getFirstChildOfMatchingText(nodeLi, TYPE_TAG)
304                 .orElseThrow(() -> {
305                     return new MetadataGenerationException(String.format(
306                         Locale.ROOT, PROP_TYPE_MISSING, propertyName)
307                     );
308                 });
309             final String propertyDesc = DESC_CLEAN.matcher(
310                     constructSubTreeText(nodeLi, propertyNameTag.getIndex() + 1,
311                             propertyType.getIndex() - 1))
312                     .replaceAll(Matcher.quoteReplacement(""));
313 
314             modulePropertyDetails.setDescription(propertyDesc.trim());
315             modulePropertyDetails.setName(propertyName);
316             modulePropertyDetails.setType(getTagTextFromProperty(nodeLi, propertyType));
317 
318             final Optional<DetailNode> validationTypeNodeOpt = getFirstChildOfMatchingText(nodeLi,
319                 VALIDATION_TYPE_TAG);
320             if (validationTypeNodeOpt.isPresent()) {
321                 final DetailNode validationTypeNode = validationTypeNodeOpt.orElseThrow();
322                 modulePropertyDetails.setValidationType(getTagTextFromProperty(nodeLi,
323                     validationTypeNode));
324             }
325 
326             final String defaultValue = getFirstChildOfMatchingText(nodeLi, DEFAULT_VALUE_TAG)
327                 .map(defaultValueNode -> getPropertyDefaultText(nodeLi, defaultValueNode))
328                 .orElseThrow(() -> {
329                     return new MetadataGenerationException(String.format(
330                         Locale.ROOT, PROP_DEFAULT_VALUE_MISSING, propertyName)
331                     );
332                 });
333             if (!PROPERTIES_TO_NOT_WRITE.contains(defaultValue)) {
334                 modulePropertyDetails.setDefaultValue(defaultValue);
335             }
336         }
337         return modulePropertyDetails;
338     }
339 
340     /**
341      * Get tag text from property data.
342      *
343      * @param nodeLi javadoc li item node
344      * @param propertyMeta property javadoc node
345      * @return property metadata text
346      */
347     private static String getTagTextFromProperty(DetailNode nodeLi, DetailNode propertyMeta) {
348         final Optional<DetailNode> tagNodeOpt = getFirstChildOfType(nodeLi,
349                 JavadocTokenTypes.JAVADOC_INLINE_TAG, propertyMeta.getIndex() + 1);
350         DetailNode tagNode = null;
351         if (tagNodeOpt.isPresent()) {
352             tagNode = tagNodeOpt.orElseThrow();
353         }
354         return getTextFromTag(tagNode);
355     }
356 
357     /**
358      * Clean up the default token text by removing hyperlinks, and only keeping token type text.
359      *
360      * @param initialText unclean text
361      * @return clean text
362      */
363     private static String cleanDefaultTokensText(String initialText) {
364         final Set<String> tokens = new LinkedHashSet<>();
365         final Matcher matcher = TOKEN_TEXT_PATTERN.matcher(initialText);
366         while (matcher.find()) {
367             tokens.add(matcher.group(0));
368         }
369         return String.join(",", tokens);
370     }
371 
372     /**
373      * Performs a DFS of the subtree with a node as the root and constructs the text of that
374      * tree, ignoring JavadocToken texts.
375      *
376      * @param node root node of subtree
377      * @param childLeftLimit the left index of root children from where to scan
378      * @param childRightLimit the right index of root children till where to scan
379      * @return constructed text of subtree
380      */
381     public static String constructSubTreeText(DetailNode node, int childLeftLimit,
382                                                int childRightLimit) {
383         DetailNode detailNode = node;
384 
385         final Deque<DetailNode> stack = new ArrayDeque<>();
386         stack.addFirst(detailNode);
387         final Set<DetailNode> visited = new HashSet<>();
388         final StringBuilder result = new StringBuilder(1024);
389         while (!stack.isEmpty()) {
390             detailNode = stack.removeFirst();
391 
392             if (visited.add(detailNode) && isContentToWrite(detailNode)) {
393                 String childText = detailNode.getText();
394 
395                 if (detailNode.getParent().getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
396                     childText = adjustCodeInlineTagChildToHtml(detailNode);
397                 }
398 
399                 result.insert(0, childText);
400             }
401 
402             for (DetailNode child : detailNode.getChildren()) {
403                 if (child.getParent().equals(node)
404                         && (child.getIndex() < childLeftLimit
405                         || child.getIndex() > childRightLimit)) {
406                     continue;
407                 }
408                 if (!visited.contains(child)) {
409                     stack.addFirst(child);
410                 }
411             }
412         }
413         return result.toString().trim();
414     }
415 
416     /**
417      * Checks whether selected Javadoc node is considered as something to write.
418      *
419      * @param detailNode javadoc node to check.
420      * @return whether javadoc node is something to write.
421      */
422     private static boolean isContentToWrite(DetailNode detailNode) {
423 
424         return detailNode.getType() != JavadocTokenTypes.LEADING_ASTERISK
425             && (detailNode.getType() == JavadocTokenTypes.TEXT
426             || !TOKEN_TEXT_PATTERN.matcher(detailNode.getText()).matches());
427     }
428 
429     /**
430      * Adjusts child of {@code @code} Javadoc inline tag to html format.
431      *
432      * @param codeChild {@code @code} child to convert.
433      * @return converted {@code @code} child element, otherwise just the original text.
434      */
435     private static String adjustCodeInlineTagChildToHtml(DetailNode codeChild) {
436 
437         return switch (codeChild.getType()) {
438             case JavadocTokenTypes.JAVADOC_INLINE_TAG_END -> "</code>";
439             case JavadocTokenTypes.WS -> "";
440             case JavadocTokenTypes.CODE_LITERAL -> codeChild.getText().replace("@", "") + ">";
441             case JavadocTokenTypes.JAVADOC_INLINE_TAG_START -> "<";
442             default -> codeChild.getText();
443         };
444     }
445 
446     /**
447      * Create the description text with starting index as 0 and ending index would be the first
448      * valid non-zero index amongst in the order of {@code propertySectionStartIdx},
449      * {@code exampleSectionStartIdx} and {@code parentSectionStartIdx}.
450      *
451      * @return description text
452      */
453     private String getDescriptionText() {
454         final int descriptionEndIdx;
455         if (propertySectionStartIdx > -1) {
456             descriptionEndIdx = propertySectionStartIdx;
457         }
458         else if (exampleSectionStartIdx > -1) {
459             descriptionEndIdx = exampleSectionStartIdx;
460         }
461         else {
462             descriptionEndIdx = parentSectionStartIdx;
463         }
464         return constructSubTreeText(rootNode, 0, descriptionEndIdx - 1);
465     }
466 
467     /**
468      * Create property default text, which is either normal property value or list of tokens.
469      *
470      * @param nodeLi list item javadoc node
471      * @param defaultValueNode default value node
472      * @return default property text
473      */
474     private static String getPropertyDefaultText(DetailNode nodeLi, DetailNode defaultValueNode) {
475         final Optional<DetailNode> propertyDefaultValueTag = getFirstChildOfType(nodeLi,
476                 JavadocTokenTypes.JAVADOC_INLINE_TAG, defaultValueNode.getIndex() + 1);
477         final String result;
478         if (propertyDefaultValueTag.isPresent()) {
479             result = getTextFromTag(propertyDefaultValueTag.orElseThrow());
480         }
481         else {
482             final String tokenText = constructSubTreeText(nodeLi,
483                     defaultValueNode.getIndex(), nodeLi.getChildren().length);
484             result = cleanDefaultTokensText(tokenText);
485         }
486         return result;
487     }
488 
489     /**
490      * Get the violation message text for a specific key from the list item.
491      *
492      * @param nodeLi list item javadoc node
493      * @return violation message key text
494      */
495     private static String getViolationMessages(DetailNode nodeLi) {
496         final Optional<DetailNode> resultNode = getFirstChildOfType(nodeLi,
497                 JavadocTokenTypes.JAVADOC_INLINE_TAG, 0);
498         return resultNode.map(JavadocMetadataScraper::getTextFromTag).orElse("");
499     }
500 
501     /**
502      * Get text from {@code JavadocTokenTypes.JAVADOC_INLINE_TAG}.
503      *
504      * @param nodeTag target javadoc tag
505      * @return text contained by the tag
506      */
507     private static String getTextFromTag(DetailNode nodeTag) {
508         return Optional.ofNullable(nodeTag).map(JavadocMetadataScraper::getText).orElse("");
509     }
510 
511     /**
512      * Returns the first child node which matches the provided {@code TokenType} and has the
513      * children index after the offset value.
514      *
515      * @param node parent node
516      * @param tokenType token type to match
517      * @param offset children array index offset
518      * @return the first child satisfying the conditions
519      */
520     private static Optional<DetailNode> getFirstChildOfType(DetailNode node, int tokenType,
521                                                             int offset) {
522         return Arrays.stream(node.getChildren())
523                 .filter(child -> child.getIndex() >= offset && child.getType() == tokenType)
524                 .findFirst();
525     }
526 
527     /**
528      * Get joined text from all text children nodes.
529      *
530      * @param parentNode parent node
531      * @return the joined text of node
532      */
533     private static String getText(DetailNode parentNode) {
534         return Arrays.stream(parentNode.getChildren())
535                 .filter(child -> child.getType() == JavadocTokenTypes.TEXT)
536                 .map(node -> QUOTE_PATTERN.matcher(node.getText().trim()).replaceAll(""))
537                 .collect(Collectors.joining(" "));
538     }
539 
540     /**
541      * Get first child of parent node matching the provided pattern.
542      *
543      * @param node parent node
544      * @param pattern pattern to match against
545      * @return the first child node matching the condition
546      */
547     private static Optional<DetailNode> getFirstChildOfMatchingText(DetailNode node,
548                                                                     Pattern pattern) {
549         return Arrays.stream(node.getChildren())
550                 .filter(child -> pattern.matcher(child.getText()).matches())
551                 .findFirst();
552     }
553 
554     /**
555      * Returns parent node, removing modifier/annotation nodes.
556      *
557      * @param commentBlock child node.
558      * @return parent node.
559      */
560     private static DetailAST getParent(DetailAST commentBlock) {
561         final DetailAST parentNode = commentBlock.getParent();
562         DetailAST result = parentNode;
563         if (result.getType() == TokenTypes.ANNOTATION) {
564             result = parentNode.getParent().getParent();
565         }
566         else if (result.getType() == TokenTypes.MODIFIERS) {
567             result = parentNode.getParent();
568         }
569         return result;
570     }
571 
572     /**
573      * Traverse parents until we reach the root node (@code{JavadocTokenTypes.JAVADOC})
574      * child and return its index.
575      *
576      * @param node subtree child node
577      * @return root node child index
578      */
579     public static int getParentIndexOf(DetailNode node) {
580         DetailNode currNode = node;
581         while (currNode.getParent().getIndex() != -1) {
582             currNode = currNode.getParent();
583         }
584         return currNode.getIndex();
585     }
586 
587     /**
588      * Get module parent text from paragraph javadoc node.
589      *
590      * @param nodeParagraph paragraph javadoc node
591      * @return parent text
592      */
593     private static String getParentText(DetailNode nodeParagraph) {
594         return getFirstChildOfType(nodeParagraph, JavadocTokenTypes.JAVADOC_INLINE_TAG, 0)
595                 .map(JavadocMetadataScraper::getTextFromTag)
596                 .orElse(null);
597     }
598 
599     /**
600      * Get module type(check/filter/filefilter) based on file name.
601      *
602      * @return module type
603      */
604     private ModuleType getModuleType() {
605         final String simpleModuleName = getModuleSimpleName();
606         final ModuleType result;
607         if (simpleModuleName.endsWith("FileFilter")) {
608             result = ModuleType.FILEFILTER;
609         }
610         else if (simpleModuleName.endsWith("Filter")) {
611             result = ModuleType.FILTER;
612         }
613         else {
614             result = ModuleType.CHECK;
615         }
616         return result;
617     }
618 
619     /**
620      * Extract simple file name from the whole file path name.
621      *
622      * @return simple module name
623      */
624     private String getModuleSimpleName() {
625         final String fullFileName = getFilePath();
626         final String[] pathTokens = FILE_SEPARATOR_PATTERN.split(fullFileName);
627         final String fileName = pathTokens[pathTokens.length - 1];
628         return fileName.substring(0, fileName.length() - JAVA_FILE_EXTENSION.length());
629     }
630 
631     /**
632      * Retrieve package name of module from the absolute file path.
633      *
634      * @param filePath absolute file path
635      * @return package name
636      */
637     private static String getPackageName(String filePath) {
638         final Deque<String> result = new ArrayDeque<>();
639         final String[] filePathTokens = FILE_SEPARATOR_PATTERN.split(filePath);
640         for (int i = filePathTokens.length - 1; i >= 0; i--) {
641             if ("java".equals(filePathTokens[i]) || "resources".equals(filePathTokens[i])) {
642                 break;
643             }
644             result.addFirst(filePathTokens[i]);
645         }
646         final String fileName = result.removeLast();
647         result.addLast(fileName.substring(0, fileName.length() - JAVA_FILE_EXTENSION.length()));
648         return String.join(".", result);
649     }
650 
651     /**
652      * Getter method for {@code moduleDetailsStore}.
653      *
654      * @return map containing module details of supplied checks.
655      */
656     public static Map<String, ModuleDetails> getModuleDetailsStore() {
657         return Collections.unmodifiableMap(MODULE_DETAILS_STORE);
658     }
659 
660     /** Reset the module detail store of any previous information. */
661     public static void resetModuleDetailsStore() {
662         MODULE_DETAILS_STORE.clear();
663     }
664 
665     /**
666      * Check if the current javadoc block comment AST corresponds to the top-level class as we
667      * only want to scrape top-level class javadoc.
668      *
669      * @return true if the current AST corresponds to top level class
670      */
671     private boolean isTopLevelClassJavadoc() {
672         final DetailAST parent = getParent(getBlockCommentAst());
673         final Optional<DetailAST> className = TokenUtil
674                 .findFirstTokenByPredicate(parent, child -> {
675                     return parent.getType() == TokenTypes.CLASS_DEF
676                             && child.getType() == TokenTypes.IDENT;
677                 });
678         return className.isPresent()
679                 && getModuleSimpleName().equals(className.orElseThrow().getText());
680     }
681 
682     /**
683      * Checks whether the paragraph node corresponds to the example section.
684      *
685      * @param ast javadoc paragraph node
686      * @return true if the section matches the example section marker
687      */
688     private static boolean isExamplesText(DetailNode ast) {
689         return isChildNodeTextMatches(ast, EXAMPLES_TAG);
690     }
691 
692     /**
693      * Checks whether the list item node is part of a property list.
694      *
695      * @param nodeLi {@code JavadocTokenType.LI} node
696      * @return true if the node is part of a property list
697      */
698     private static boolean isPropertyList(DetailNode nodeLi) {
699         return isChildNodeTextMatches(nodeLi, PROPERTY_TAG);
700     }
701 
702     /**
703      * Checks whether the {@code JavadocTokenType.PARAGRAPH} node is referring to the violation
704      * message keys javadoc segment.
705      *
706      * @param nodeParagraph paragraph javadoc node
707      * @return true if paragraph node contains the violation message keys text
708      */
709     private static boolean isViolationMessagesText(DetailNode nodeParagraph) {
710         return isChildNodeTextMatches(nodeParagraph, VIOLATION_MESSAGES_TAG);
711     }
712 
713     /**
714      * Checks whether the {@code JavadocTokenType.PARAGRAPH} node is referring to the parent
715      * javadoc segment.
716      *
717      * @param nodeParagraph paragraph javadoc node
718      * @return true if paragraph node contains the parent text
719      */
720     public static boolean isParentText(DetailNode nodeParagraph) {
721         return isChildNodeTextMatches(nodeParagraph, PARENT_TAG);
722     }
723 
724     /**
725      * Checks whether the first child {@code JavadocTokenType.TEXT} node matches given pattern.
726      *
727      * @param ast parent javadoc node
728      * @param pattern pattern to match
729      * @return true if one of child text nodes matches pattern
730      */
731     public static boolean isChildNodeTextMatches(DetailNode ast, Pattern pattern) {
732         return getFirstChildOfType(ast, JavadocTokenTypes.TEXT, 0)
733                 .map(DetailNode::getText)
734                 .map(pattern::matcher)
735                 .map(Matcher::matches)
736                 .orElse(Boolean.FALSE);
737     }
738 }