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.Optional;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.api.DetailNode;
027import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
029
030/**
031 * Class for scraping module metadata from the corresponding class' class-level javadoc.
032 */
033public final class JavadocMetadataScraperUtil {
034
035    /** Regular expression for detecting ANTLR tokens(for e.g. CLASS_DEF). */
036    private static final Pattern TOKEN_TEXT_PATTERN = Pattern.compile("([A-Z_]{2,})+");
037
038    /**
039     * Private utility constructor.
040     */
041    private JavadocMetadataScraperUtil() {
042    }
043
044    /**
045     * Performs a depth-first traversal of the subtree starting at {@code startNode}
046     * and ending at {@code endNode}, and constructs the concatenated text of all nodes
047     * in that range, ignoring {@code JavadocToken} texts.
048     *
049     * @param startNode the node where traversal begins (inclusive)
050     * @param endNode the node where traversal ends (inclusive)
051     * @return the constructed text from the specified subtree range
052     */
053    public static String constructSubTreeText(DetailNode startNode,
054                                               DetailNode endNode) {
055        DetailNode curNode = startNode;
056        final StringBuilder result = new StringBuilder(1024);
057
058        while (curNode != null) {
059            if (isContentToWrite(curNode)) {
060                String childText = curNode.getText();
061
062                if (isInsideCodeInlineTag(curNode)) {
063                    childText = adjustCodeInlineTagChildToHtml(curNode);
064                }
065
066                result.append(childText);
067            }
068
069            DetailNode toVisit = curNode.getFirstChild();
070            while (curNode != endNode && toVisit == null) {
071                toVisit = curNode.getNextSibling();
072                curNode = curNode.getParent();
073            }
074
075            curNode = toVisit;
076        }
077        return result.toString().trim();
078    }
079
080    /**
081     * Checks whether the given node is inside a {@code @code} Javadoc inline tag.
082     *
083     * @param node the node to check
084     * @return true if the node is inside a {@code @code} inline tag, false otherwise
085     */
086    private static boolean isInsideCodeInlineTag(DetailNode node) {
087        return node.getParent() != null
088                && node.getParent().getType() == JavadocCommentsTokenTypes.CODE_INLINE_TAG;
089    }
090
091    /**
092     * Checks whether selected Javadoc node is considered as something to write.
093     *
094     * @param detailNode javadoc node to check.
095     * @return whether javadoc node is something to write.
096     */
097    private static boolean isContentToWrite(DetailNode detailNode) {
098
099        return detailNode.getType() != JavadocCommentsTokenTypes.LEADING_ASTERISK
100            && (detailNode.getType() == JavadocCommentsTokenTypes.TEXT
101            || !TOKEN_TEXT_PATTERN.matcher(detailNode.getText()).matches());
102    }
103
104    /**
105     * Adjusts certain child of {@code @code} Javadoc inline tag to its analogous html format.
106     *
107     * @param codeChild {@code @code} child to convert.
108     * @return converted {@code @code} child element, otherwise just the original text.
109     */
110    public static String adjustCodeInlineTagChildToHtml(DetailNode codeChild) {
111
112        return switch (codeChild.getType()) {
113            case JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_END -> "</code>";
114            case JavadocCommentsTokenTypes.TAG_NAME -> "";
115            case JavadocCommentsTokenTypes.JAVADOC_INLINE_TAG_START -> "<code>";
116            default -> codeChild.getText().trim();
117        };
118    }
119
120    /**
121     * Returns the first child node of the given parent that matches the provided {@code tokenType}.
122     *
123     * @param node the parent node
124     * @param tokenType the token type to match
125     * @return an {@link Optional} containing the first matching child node,
126     *         or an empty {@link Optional} if none is found
127     */
128    private static Optional<DetailNode> getFirstChildOfType(DetailNode node, int tokenType) {
129        return JavadocUtil.getAllNodesOfType(node, tokenType).stream().findFirst();
130    }
131
132    /**
133     * Checks whether the first child {@code JavadocTokenType.TEXT} node matches given pattern.
134     *
135     * @param ast parent javadoc node
136     * @param pattern pattern to match
137     * @return true if one of child text nodes matches pattern
138     */
139    public static boolean isChildNodeTextMatches(DetailNode ast, Pattern pattern) {
140        return getFirstChildOfType(ast, JavadocCommentsTokenTypes.TEXT)
141                .map(DetailNode::getText)
142                .map(pattern::matcher)
143                .map(Matcher::matches)
144                .orElse(Boolean.FALSE);
145    }
146}