001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 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.meta;
021
022import java.util.ArrayDeque;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.Deque;
026import java.util.HashMap;
027import java.util.HashSet;
028import java.util.LinkedHashSet;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Optional;
032import java.util.Set;
033import java.util.regex.Matcher;
034import java.util.regex.Pattern;
035import java.util.stream.Collectors;
036
037import javax.xml.parsers.ParserConfigurationException;
038import javax.xml.transform.TransformerException;
039
040import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
041import com.puppycrawl.tools.checkstyle.api.DetailAST;
042import com.puppycrawl.tools.checkstyle.api.DetailNode;
043import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
044import com.puppycrawl.tools.checkstyle.api.TokenTypes;
045import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
046import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
047
048/**
049 * Class for scraping module metadata from the corresponding class' class-level javadoc.
050 */
051@FileStatefulCheck
052public class JavadocMetadataScraper extends AbstractJavadocCheck {
053
054    /**
055     * A key is pointing to the warning message text in "messages.properties"
056     * file.
057     */
058    public static final String MSG_DESC_MISSING = "javadocmetadatascraper.description.missing";
059
060    /** Module details store used for testing. */
061    private static final Map<String, ModuleDetails> MODULE_DETAILS_STORE = new HashMap<>();
062
063    /** Regular expression for property location in class-level javadocs. */
064    private static final Pattern PROPERTY_TAG = Pattern.compile("\\s*Property\\s*");
065
066    /** Regular expression for property type location in class-level javadocs. */
067    private static final Pattern TYPE_TAG = Pattern.compile("^ Type is\\s.*");
068
069    /** Regular expression for property validation type location in class-level javadocs. */
070    private static final Pattern VALIDATION_TYPE_TAG =
071            Pattern.compile("\\s.*Validation type is\\s.*");
072
073    /** Regular expression for property default value location in class-level javadocs. */
074    private static final Pattern DEFAULT_VALUE_TAG = Pattern.compile("^ Default value is:*.*");
075
076    /** Regular expression for check example location in class-level javadocs. */
077    private static final Pattern EXAMPLES_TAG =
078            Pattern.compile("\\s*To configure the (default )?check.*");
079
080    /** Regular expression for module parent location in class-level javadocs. */
081    private static final Pattern PARENT_TAG = Pattern.compile("\\s*Parent is\\s*");
082
083    /** Regular expression for module violation messages location in class-level javadocs. */
084    private static final Pattern VIOLATION_MESSAGES_TAG =
085            Pattern.compile("\\s*Violation Message Keys:\\s*");
086
087    /** Regular expression for detecting ANTLR tokens(for e.g. CLASS_DEF). */
088    private static final Pattern TOKEN_TEXT_PATTERN = Pattern.compile("([A-Z_]{2,})+");
089
090    /** Regular expression for removal of @code{-} present at the beginning of texts. */
091    private static final Pattern DESC_CLEAN = Pattern.compile("-\\s");
092
093    /** Regular expression for file separator corresponding to the host OS. */
094    private static final Pattern FILE_SEPARATOR_PATTERN =
095            Pattern.compile(Pattern.quote(System.getProperty("file.separator")));
096
097    /** Regular expression for quotes. */
098    private static final Pattern QUOTE_PATTERN = Pattern.compile("\"");
099
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}