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 * Property {@code offset} - Specify how many spaces to use for new indentation level.
042 * Type is {@code int}.
043 * Default value is {@code 4}.
044 * </li>
045 * <li>
046 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
047 * if the Javadoc being examined by this check violates the tight html rules defined at
048 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>.
049 * Type is {@code boolean}.
050 * Default value is {@code false}.
051 * </li>
052 * </ul>
053 *
054 * <p>
055 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
056 * </p>
057 *
058 * <p>
059 * Violation Message Keys:
060 * </p>
061 * <ul>
062 * <li>
063 * {@code javadoc.missed.html.close}
064 * </li>
065 * <li>
066 * {@code javadoc.parse.rule.error}
067 * </li>
068 * <li>
069 * {@code javadoc.unclosedHtml}
070 * </li>
071 * <li>
072 * {@code javadoc.wrong.singleton.html.tag}
073 * </li>
074 * <li>
075 * {@code tag.continuation.indent}
076 * </li>
077 * </ul>
078 *
079 * @since 6.0
080 *
081 */
082@StatelessCheck
083public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck {
084
085    /**
086     * A key is pointing to the warning message text in "messages.properties"
087     * file.
088     */
089    public static final String MSG_KEY = "tag.continuation.indent";
090
091    /** Default tag continuation indentation. */
092    private static final int DEFAULT_INDENTATION = 4;
093
094    /**
095     * Specify how many spaces to use for new indentation level.
096     */
097    private int offset = DEFAULT_INDENTATION;
098
099    /**
100     * Setter to specify how many spaces to use for new indentation level.
101     *
102     * @param offset custom value.
103     * @since 6.0
104     */
105    public void setOffset(int offset) {
106        this.offset = offset;
107    }
108
109    @Override
110    public int[] getDefaultJavadocTokens() {
111        return new int[] {JavadocTokenTypes.HTML_TAG, JavadocTokenTypes.DESCRIPTION};
112
113    }
114
115    @Override
116    public int[] getRequiredJavadocTokens() {
117        return getAcceptableJavadocTokens();
118    }
119
120    @Override
121    public void visitJavadocToken(DetailNode ast) {
122        if (isBlockDescription(ast) && !isInlineDescription(ast)) {
123            final List<DetailNode> textNodes = getAllNewlineNodes(ast);
124            for (DetailNode newlineNode : textNodes) {
125                final DetailNode textNode = JavadocUtil.getNextSibling(newlineNode);
126                if (textNode.getType() != JavadocTokenTypes.NEWLINE && isViolation(textNode)) {
127                    log(textNode.getLineNumber(), MSG_KEY, offset);
128                }
129            }
130        }
131    }
132
133    /**
134     * Checks if a text node meets the criteria for a violation.
135     * If the text is shorter than {@code offset} characters, then a violation is
136     * detected if the text is not blank or the next node is not a newline.
137     * If the text is longer than {@code offset} characters, then a violation is
138     * detected if any of the first {@code offset} characters are not blank.
139     *
140     * @param textNode the node to check.
141     * @return true if the node has a violation.
142     */
143    private boolean isViolation(DetailNode textNode) {
144        boolean result = false;
145        final String text = textNode.getText();
146        if (text.length() <= offset) {
147            if (CommonUtil.isBlank(text)) {
148                final DetailNode nextNode = JavadocUtil.getNextSibling(textNode);
149                if (nextNode != null && nextNode.getType() != JavadocTokenTypes.NEWLINE) {
150                    // text is blank but line hasn't ended yet
151                    result = true;
152                }
153            }
154            else {
155                // text is not blank
156                result = true;
157            }
158        }
159        else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) {
160            // first offset number of characters are not blank
161            result = true;
162        }
163        return result;
164    }
165
166    /**
167     * Finds and collects all NEWLINE nodes inside DESCRIPTION node.
168     *
169     * @param descriptionNode DESCRIPTION node.
170     * @return List with NEWLINE nodes.
171     */
172    private static List<DetailNode> getAllNewlineNodes(DetailNode descriptionNode) {
173        final List<DetailNode> textNodes = new ArrayList<>();
174        DetailNode node = JavadocUtil.getFirstChild(descriptionNode);
175        while (JavadocUtil.getNextSibling(node) != null) {
176            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
177                final DetailNode descriptionNodeChild = JavadocUtil.getFirstChild(node);
178                textNodes.addAll(getAllNewlineNodes(descriptionNodeChild));
179            }
180            else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT_START
181                || node.getType() == JavadocTokenTypes.ATTRIBUTE) {
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        return textNodes;
190    }
191
192    /**
193     * Checks if the given description node is part of a block Javadoc tag.
194     *
195     * @param description the node to check
196     * @return {@code true} if the node is inside a block tag, {@code false} otherwise
197     */
198    private static boolean isBlockDescription(DetailNode description) {
199        boolean isBlock = false;
200        DetailNode currentNode = description;
201        while (currentNode != null) {
202            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_TAG) {
203                isBlock = true;
204                break;
205            }
206            currentNode = currentNode.getParent();
207        }
208        return isBlock;
209    }
210
211    /**
212     * Checks, if description node is a description of in-line tag.
213     *
214     * @param description DESCRIPTION node.
215     * @return true, if description node is a description of in-line tag.
216     */
217    private static boolean isInlineDescription(DetailNode description) {
218        boolean isInline = false;
219        DetailNode currentNode = description;
220        while (currentNode != null) {
221            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
222                isInline = true;
223                break;
224            }
225            currentNode = currentNode.getParent();
226        }
227        return isInline;
228    }
229
230}