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