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.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.List;
024
025import com.puppycrawl.tools.checkstyle.StatelessCheck;
026import com.puppycrawl.tools.checkstyle.api.DetailNode;
027import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
028import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
029import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
030
031/**
032 * <div>
033 * Checks the indentation of the continuation lines in block tags. That is whether the continued
034 * description of at clauses should be indented or not. If the text is not properly indented it
035 * throws a violation. A continuation line is when the description starts/spans past the line with
036 * the tag. Default indentation required is at least 4, but this can be changed with the help of
037 * properties below.
038 * </div>
039 * <ul>
040 * <li>
041 * Notes:
042 * This check does not validate the indentation of lines inside {@code pre} tags.
043 * </li>
044 * </ul>
045 *
046 * @since 6.0
047 */
048@StatelessCheck
049public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck {
050
051    /**
052     * A key is pointing to the warning message text in "messages.properties"
053     * file.
054     */
055    public static final String MSG_KEY = "tag.continuation.indent";
056
057    /** Default tag continuation indentation. */
058    private static final int DEFAULT_INDENTATION = 4;
059
060    /**
061     * Specify how many spaces to use for new indentation level.
062     */
063    private int offset = DEFAULT_INDENTATION;
064
065    /**
066     * Setter to specify how many spaces to use for new indentation level.
067     *
068     * @param offset custom value.
069     * @since 6.0
070     */
071    public void setOffset(int offset) {
072        this.offset = offset;
073    }
074
075    @Override
076    public int[] getDefaultJavadocTokens() {
077        return new int[] {JavadocTokenTypes.HTML_TAG, JavadocTokenTypes.DESCRIPTION};
078
079    }
080
081    @Override
082    public int[] getRequiredJavadocTokens() {
083        return getAcceptableJavadocTokens();
084    }
085
086    @Override
087    public void visitJavadocToken(DetailNode ast) {
088        if (isBlockDescription(ast) && !isInlineDescription(ast)) {
089            final List<DetailNode> textNodes = getAllNewlineNodes(ast);
090            boolean isTextPreTagChildren = false;
091            for (DetailNode newlineNode : textNodes) {
092                final DetailNode textNode = JavadocUtil.getNextSibling(newlineNode);
093                if (newlineNode.getType() == JavadocTokenTypes.HTML_ELEMENT_START
094                        || isHtmlTagChildOfPreTag(ast)) {
095                    isTextPreTagChildren = true;
096                }
097                else if (newlineNode.getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
098                    isTextPreTagChildren = false;
099                }
100                else if (!isTextPreTagChildren && textNode.getType() != JavadocTokenTypes.NEWLINE
101                    && isViolation(textNode)) {
102                    log(textNode.getLineNumber(), MSG_KEY, offset);
103                }
104            }
105        }
106    }
107
108    /**
109     * Checks if the given HTML_TAG is contained inside {@code <pre>} tag.
110     * For cases when another HTML tag is placed next to or before {@code <pre>} tag.
111     * For example:
112     * <pre>
113     * {@code
114     * <pre><someOtherTag>
115     *     some thing
116     * </someOtherTag></pre>
117     * }
118     * </pre>
119     *
120     * @param htmlTag HTML_TAG
121     * @return {@code true} if {@code pre} tag is parent of the given tag, else {@code false}.
122     */
123    private static boolean isHtmlTagChildOfPreTag(DetailNode htmlTag) {
124        DetailNode node = htmlTag.getParent().getParent();
125        node = JavadocUtil.getFirstChild(node);
126        return containsPreTag(node);
127    }
128
129    /**
130     * Checks if a text node meets the criteria for a violation.
131     * If the text is shorter than {@code offset} characters, then a violation is
132     * detected if the text is not blank or the next node is not a newline.
133     * If the text is longer than {@code offset} characters, then a violation is
134     * detected if any of the first {@code offset} characters are not blank.
135     *
136     * @param textNode the node to check.
137     * @return true if the node has a violation.
138     */
139    private boolean isViolation(DetailNode textNode) {
140        boolean result = false;
141        final String text = textNode.getText();
142        if (text.length() <= offset) {
143            if (CommonUtil.isBlank(text)) {
144                final DetailNode nextNode = JavadocUtil.getNextSibling(textNode);
145                // text is blank but line hasn't ended yet
146                if (nextNode != null && nextNode.getType() != JavadocTokenTypes.NEWLINE
147                        && !containsPreTag(nextNode)) {
148                    result = true;
149                }
150            }
151            else {
152                // text is not blank
153                result = true;
154            }
155        }
156        else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) {
157            // first offset number of characters are not blank
158            result = true;
159        }
160        return result;
161    }
162
163    /**
164     * Finds and collects all NEWLINE nodes inside DESCRIPTION node.
165     *
166     * @param descriptionNode DESCRIPTION node.
167     * @return List with NEWLINE nodes.
168     */
169    private static List<DetailNode> getAllNewlineNodes(DetailNode descriptionNode) {
170        final List<DetailNode> textNodes = new ArrayList<>();
171        DetailNode node = JavadocUtil.getFirstChild(descriptionNode);
172        while (JavadocUtil.getNextSibling(node) != null) {
173            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
174                final DetailNode descriptionNodeChild = JavadocUtil.getFirstChild(node);
175                textNodes.addAll(getAllNewlineNodes(descriptionNodeChild));
176            }
177            else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START
178                || node.getType() == JavadocTokenTypes.ATTRIBUTE) {
179                if (containsPreTag(node)) {
180                    textNodes.add(node);
181                }
182                textNodes.addAll(getAllNewlineNodes(node));
183            }
184            if (node.getType() == JavadocTokenTypes.LEADING_ASTERISK) {
185                textNodes.add(node);
186            }
187            node = JavadocUtil.getNextSibling(node);
188        }
189
190        // Last node does not get checked in the loop
191        if (containsPreTag(node)) {
192            textNodes.add(node);
193        }
194        return textNodes;
195    }
196
197    /**
198     * Checks if the given HTML related node contains {@code <pre>} tag.
199     *
200     * @param ast the node to check
201     * @return {@code true} if the {@code <pre>} tag is inside the node, {@code false} otherwise
202     */
203    private static boolean containsPreTag(DetailNode ast) {
204        DetailNode node = ast;
205        if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START
206                || node.getType() == JavadocTokenTypes.HTML_ELEMENT_END) {
207            node = JavadocUtil.findFirstToken(node, JavadocTokenTypes.HTML_TAG_NAME);
208        }
209        if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
210            final DetailNode htmlTag = JavadocUtil.getFirstChild(node);
211            if (htmlTag.getType() == JavadocTokenTypes.HTML_TAG) {
212                final DetailNode htmlElementStart = JavadocUtil.getFirstChild(htmlTag);
213                node = JavadocUtil.findFirstToken(htmlElementStart,
214                    JavadocTokenTypes.HTML_TAG_NAME);
215            }
216        }
217        return "pre".equalsIgnoreCase(node.getText());
218    }
219
220    /**
221     * Checks if the given description node is part of a block Javadoc tag.
222     *
223     * @param description the node to check
224     * @return {@code true} if the node is inside a block tag, {@code false} otherwise
225     */
226    private static boolean isBlockDescription(DetailNode description) {
227        boolean isBlock = false;
228        DetailNode currentNode = description;
229        while (currentNode != null) {
230            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
231                isBlock = true;
232                break;
233            }
234            currentNode = currentNode.getParent();
235        }
236        return isBlock;
237    }
238
239    /**
240     * Checks, if description node is a description of in-line tag.
241     *
242     * @param description DESCRIPTION node.
243     * @return true, if description node is a description of in-line tag.
244     */
245    private static boolean isInlineDescription(DetailNode description) {
246        boolean isInline = false;
247        DetailNode currentNode = description;
248        while (currentNode != null) {
249            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
250                isInline = true;
251                break;
252            }
253            currentNode = currentNode.getParent();
254        }
255        return isInline;
256    }
257
258}