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