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.Deque;
025import java.util.HashSet;
026import java.util.Optional;
027import java.util.Set;
028import java.util.regex.Matcher;
029import java.util.regex.Pattern;
030
031import com.puppycrawl.tools.checkstyle.api.DetailNode;
032import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
033
034/**
035 * Class for scraping module metadata from the corresponding class' class-level javadoc.
036 */
037public final class JavadocMetadataScraperUtil {
038
039    /** Regular expression for detecting ANTLR tokens(for e.g. CLASS_DEF). */
040    private static final Pattern TOKEN_TEXT_PATTERN = Pattern.compile("([A-Z_]{2,})+");
041
042    /**
043     * Private utility constructor.
044     */
045    private JavadocMetadataScraperUtil() {
046    }
047
048    /**
049     * Performs a DFS of the subtree with a node as the root and constructs the text of that
050     * tree, ignoring JavadocToken texts.
051     *
052     * @param node root node of subtree
053     * @param childLeftLimit the left index of root children from where to scan
054     * @param childRightLimit the right index of root children till where to scan
055     * @return constructed text of subtree
056     */
057    public static String constructSubTreeText(DetailNode node, int childLeftLimit,
058                                               int childRightLimit) {
059        DetailNode detailNode = node;
060
061        final Deque<DetailNode> stack = new ArrayDeque<>();
062        stack.addFirst(detailNode);
063        final Set<DetailNode> visited = new HashSet<>();
064        final StringBuilder result = new StringBuilder(1024);
065        while (!stack.isEmpty()) {
066            detailNode = stack.removeFirst();
067
068            if (visited.add(detailNode) && isContentToWrite(detailNode)) {
069                String childText = detailNode.getText();
070
071                if (detailNode.getParent().getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
072                    childText = adjustCodeInlineTagChildToHtml(detailNode);
073                }
074
075                result.insert(0, childText);
076            }
077
078            for (DetailNode child : detailNode.getChildren()) {
079                if (child.getParent().equals(node)
080                        && (child.getIndex() < childLeftLimit
081                        || child.getIndex() > childRightLimit)) {
082                    continue;
083                }
084                if (!visited.contains(child)) {
085                    stack.addFirst(child);
086                }
087            }
088        }
089        return result.toString().trim();
090    }
091
092    /**
093     * Checks whether selected Javadoc node is considered as something to write.
094     *
095     * @param detailNode javadoc node to check.
096     * @return whether javadoc node is something to write.
097     */
098    private static boolean isContentToWrite(DetailNode detailNode) {
099
100        return detailNode.getType() != JavadocTokenTypes.LEADING_ASTERISK
101            && (detailNode.getType() == JavadocTokenTypes.TEXT
102            || !TOKEN_TEXT_PATTERN.matcher(detailNode.getText()).matches());
103    }
104
105    /**
106     * Adjusts certain child of {@code @code} Javadoc inline tag to its analogous html format.
107     *
108     * @param codeChild {@code @code} child to convert.
109     * @return converted {@code @code} child element, otherwise just the original text.
110     */
111    public static String adjustCodeInlineTagChildToHtml(DetailNode codeChild) {
112
113        return switch (codeChild.getType()) {
114            case JavadocTokenTypes.JAVADOC_INLINE_TAG_END -> "</code>";
115            case JavadocTokenTypes.WS -> "";
116            case JavadocTokenTypes.CODE_LITERAL -> codeChild.getText().replace("@", "") + ">";
117            case JavadocTokenTypes.JAVADOC_INLINE_TAG_START -> "<";
118            default -> codeChild.getText();
119        };
120    }
121
122    /**
123     * Returns the first child node which matches the provided {@code TokenType} and has the
124     * children index after the offset value.
125     *
126     * @param node parent node
127     * @param tokenType token type to match
128     * @param offset children array index offset
129     * @return the first child satisfying the conditions
130     */
131    private static Optional<DetailNode> getFirstChildOfType(DetailNode node, int tokenType,
132                                                            int offset) {
133        return Arrays.stream(node.getChildren())
134                .filter(child -> child.getIndex() >= offset && child.getType() == tokenType)
135                .findFirst();
136    }
137
138    /**
139     * Checks whether the first child {@code JavadocTokenType.TEXT} node matches given pattern.
140     *
141     * @param ast parent javadoc node
142     * @param pattern pattern to match
143     * @return true if one of child text nodes matches pattern
144     */
145    public static boolean isChildNodeTextMatches(DetailNode ast, Pattern pattern) {
146        return getFirstChildOfType(ast, JavadocTokenTypes.TEXT, 0)
147                .map(DetailNode::getText)
148                .map(pattern::matcher)
149                .map(Matcher::matches)
150                .orElse(Boolean.FALSE);
151    }
152}