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