001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2023 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 * <p>
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 * </p>
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 * <p>
054 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
055 * </p>
056 * <p>
057 * Violation Message Keys:
058 * </p>
059 * <ul>
060 * <li>
061 * {@code javadoc.missed.html.close}
062 * </li>
063 * <li>
064 * {@code javadoc.parse.rule.error}
065 * </li>
066 * <li>
067 * {@code javadoc.wrong.singleton.html.tag}
068 * </li>
069 * <li>
070 * {@code tag.continuation.indent}
071 * </li>
072 * </ul>
073 *
074 * @since 6.0
075 *
076 */
077@StatelessCheck
078public class JavadocTagContinuationIndentationCheck extends AbstractJavadocCheck {
079
080    /**
081     * A key is pointing to the warning message text in "messages.properties"
082     * file.
083     */
084    public static final String MSG_KEY = "tag.continuation.indent";
085
086    /** Default tag continuation indentation. */
087    private static final int DEFAULT_INDENTATION = 4;
088
089    /**
090     * Specify how many spaces to use for new indentation level.
091     */
092    private int offset = DEFAULT_INDENTATION;
093
094    /**
095     * Setter to specify how many spaces to use for new indentation level.
096     *
097     * @param offset custom value.
098     * @since 6.0
099     */
100    public void setOffset(int offset) {
101        this.offset = offset;
102    }
103
104    @Override
105    public int[] getDefaultJavadocTokens() {
106        return new int[] {JavadocTokenTypes.DESCRIPTION };
107    }
108
109    @Override
110    public int[] getRequiredJavadocTokens() {
111        return getAcceptableJavadocTokens();
112    }
113
114    @Override
115    public void visitJavadocToken(DetailNode ast) {
116        if (!isInlineDescription(ast)) {
117            final List<DetailNode> textNodes = getAllNewlineNodes(ast);
118            for (DetailNode newlineNode : textNodes) {
119                final DetailNode textNode = JavadocUtil.getNextSibling(newlineNode);
120                if (textNode.getType() == JavadocTokenTypes.TEXT && isViolation(textNode)) {
121                    log(textNode.getLineNumber(), MSG_KEY, offset);
122                }
123            }
124        }
125    }
126
127    /**
128     * Checks if a text node meets the criteria for a violation.
129     * If the text is shorter than {@code offset} characters, then a violation is
130     * detected if the text is not blank or the next node is not a newline.
131     * If the text is longer than {@code offset} characters, then a violation is
132     * detected if any of the first {@code offset} characters are not blank.
133     *
134     * @param textNode the node to check.
135     * @return true if the node has a violation.
136     */
137    private boolean isViolation(DetailNode textNode) {
138        boolean result = false;
139        final String text = textNode.getText();
140        if (text.length() <= offset) {
141            if (CommonUtil.isBlank(text)) {
142                final DetailNode nextNode = JavadocUtil.getNextSibling(textNode);
143                if (nextNode != null && nextNode.getType() != JavadocTokenTypes.NEWLINE) {
144                    // text is blank but line hasn't ended yet
145                    result = true;
146                }
147            }
148            else {
149                // text is not blank
150                result = true;
151            }
152        }
153        else if (!CommonUtil.isBlank(text.substring(1, offset + 1))) {
154            // first offset number of characters are not blank
155            result = true;
156        }
157        return result;
158    }
159
160    /**
161     * Finds and collects all NEWLINE nodes inside DESCRIPTION node.
162     *
163     * @param descriptionNode DESCRIPTION node.
164     * @return List with NEWLINE nodes.
165     */
166    private static List<DetailNode> getAllNewlineNodes(DetailNode descriptionNode) {
167        final List<DetailNode> textNodes = new ArrayList<>();
168        DetailNode node = JavadocUtil.getFirstChild(descriptionNode);
169        while (JavadocUtil.getNextSibling(node) != null) {
170            if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
171                final DetailNode descriptionNodeChild = JavadocUtil.getFirstChild(node);
172                textNodes.addAll(getAllNewlineNodes(descriptionNodeChild));
173            }
174            if (node.getType() == JavadocTokenTypes.LEADING_ASTERISK) {
175                textNodes.add(node);
176            }
177            node = JavadocUtil.getNextSibling(node);
178        }
179        return textNodes;
180    }
181
182    /**
183     * Checks, if description node is a description of in-line tag.
184     *
185     * @param description DESCRIPTION node.
186     * @return true, if description node is a description of in-line tag.
187     */
188    private static boolean isInlineDescription(DetailNode description) {
189        boolean isInline = false;
190        DetailNode currentNode = description;
191        while (currentNode != null) {
192            if (currentNode.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
193                isInline = true;
194                break;
195            }
196            currentNode = currentNode.getParent();
197        }
198        return isInline;
199    }
200
201}