001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2018 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.whitespace;
021
022import com.puppycrawl.tools.checkstyle.StatelessCheck;
023import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
024import com.puppycrawl.tools.checkstyle.api.DetailAST;
025import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
026
027/**
028 * <p>
029 * Checks that non-whitespace characters are separated by no more than one
030 * whitespace. Separating characters by tabs or multiple spaces will be
031 * reported. Currently the check doesn't permit horizontal alignment. To inspect
032 * whitespaces before and after comments, set the property
033 * <b>validateComments</b> to true.
034 * </p>
035 *
036 * <p>
037 * Setting <b>validateComments</b> to false will ignore cases like:
038 * </p>
039 *
040 * <pre>
041 * int i;  &#47;&#47; Multiple whitespaces before comment tokens will be ignored.
042 * private void foo(int  &#47;* whitespaces before and after block-comments will be
043 * ignored *&#47;  i) {
044 * </pre>
045 *
046 * <p>
047 * Sometimes, users like to space similar items on different lines to the same
048 * column position for easier reading. This feature isn't supported by this
049 * check, so both braces in the following case will be reported as violations.
050 * </p>
051 *
052 * <pre>
053 * public long toNanos(long d)  { return d;             }  &#47;&#47; 2 violations
054 * public long toMicros(long d) { return d / (C1 / C0); }
055 * </pre>
056 *
057 * <p>
058 * Check have following options:
059 * </p>
060 *
061 * <ul>
062 * <li>validateComments - Boolean when set to {@code true}, whitespaces
063 * surrounding comments will be ignored. Default value is {@code false}.</li>
064 * </ul>
065 *
066 * <p>
067 * To configure the check:
068 * </p>
069 *
070 * <pre>
071 * &lt;module name=&quot;SingleSpaceSeparator&quot;/&gt;
072 * </pre>
073 *
074 * <p>
075 * To configure the check so that it validates comments:
076 * </p>
077 *
078 * <pre>
079 * &lt;module name=&quot;SingleSpaceSeparator&quot;&gt;
080 * &lt;property name=&quot;validateComments&quot; value=&quot;true&quot;/&gt;
081 * &lt;/module&gt;
082 * </pre>
083 *
084 * @author Robert Whitebit
085 * @author Richard Veach
086 */
087@StatelessCheck
088public class SingleSpaceSeparatorCheck extends AbstractCheck {
089
090    /**
091     * A key is pointing to the warning message text in "messages.properties"
092     * file.
093     */
094    public static final String MSG_KEY = "single.space.separator";
095
096    /** Indicates if whitespaces surrounding comments will be ignored. */
097    private boolean validateComments;
098
099    /**
100     * Sets whether or not to validate surrounding whitespaces at comments.
101     *
102     * @param validateComments {@code true} to validate surrounding whitespaces at comments.
103     */
104    public void setValidateComments(boolean validateComments) {
105        this.validateComments = validateComments;
106    }
107
108    @Override
109    public int[] getDefaultTokens() {
110        return getRequiredTokens();
111    }
112
113    @Override
114    public int[] getAcceptableTokens() {
115        return getRequiredTokens();
116    }
117
118    @Override
119    public int[] getRequiredTokens() {
120        return CommonUtils.EMPTY_INT_ARRAY;
121    }
122
123    // -@cs[SimpleAccessorNameNotation] Overrides method from base class.
124    // Issue: https://github.com/sevntu-checkstyle/sevntu.checkstyle/issues/166
125    @Override
126    public boolean isCommentNodesRequired() {
127        return validateComments;
128    }
129
130    @Override
131    public void beginTree(DetailAST rootAST) {
132        visitEachToken(rootAST);
133    }
134
135    /**
136     * Examines every sibling and child of {@code node} for violations.
137     *
138     * @param node The node to start examining.
139     */
140    private void visitEachToken(DetailAST node) {
141        DetailAST sibling = node;
142
143        while (sibling != null) {
144            final int columnNo = sibling.getColumnNo() - 1;
145
146            // in such expression: "j  =123", placed at the start of the string index of the second
147            // space character will be: 2 = 0(j) + 1(whitespace) + 1(whitespace). It is a minimal
148            // possible index for the second whitespace between non-whitespace characters.
149            final int minSecondWhitespaceColumnNo = 2;
150
151            if (columnNo >= minSecondWhitespaceColumnNo
152                    && !isTextSeparatedCorrectlyFromPrevious(getLine(sibling.getLineNo() - 1),
153                            columnNo)) {
154                log(sibling.getLineNo(), columnNo, MSG_KEY);
155            }
156            if (sibling.getChildCount() >= 1) {
157                visitEachToken(sibling.getFirstChild());
158            }
159
160            sibling = sibling.getNextSibling();
161        }
162    }
163
164    /**
165     * Checks if characters in {@code line} at and around {@code columnNo} has
166     * the correct number of spaces. to return {@code true} the following
167     * conditions must be met:<br />
168     * - the character at {@code columnNo} is the first in the line.<br />
169     * - the character at {@code columnNo} is not separated by whitespaces from
170     * the previous non-whitespace character. <br />
171     * - the character at {@code columnNo} is separated by only one whitespace
172     * from the previous non-whitespace character.<br />
173     * - {@link #validateComments} is disabled and the previous text is the
174     * end of a block comment.
175     *
176     * @param line The line in the file to examine.
177     * @param columnNo The column position in the {@code line} to examine.
178     * @return {@code true} if the text at {@code columnNo} is separated
179     *         correctly from the previous token.
180     */
181    private boolean isTextSeparatedCorrectlyFromPrevious(String line, int columnNo) {
182        return isSingleSpace(line, columnNo)
183                || !isWhitespace(line, columnNo)
184                || isFirstInLine(line, columnNo)
185                || !validateComments && isBlockCommentEnd(line, columnNo);
186    }
187
188    /**
189     * Checks if the {@code line} at {@code columnNo} is a single space, and not
190     * preceded by another space.
191     *
192     * @param line The line in the file to examine.
193     * @param columnNo The column position in the {@code line} to examine.
194     * @return {@code true} if the character at {@code columnNo} is a space, and
195     *         not preceded by another space.
196     */
197    private static boolean isSingleSpace(String line, int columnNo) {
198        return !isPrecededByMultipleWhitespaces(line, columnNo)
199                && isSpace(line, columnNo);
200    }
201
202    /**
203     * Checks if the {@code line} at {@code columnNo} is a space.
204     *
205     * @param line The line in the file to examine.
206     * @param columnNo The column position in the {@code line} to examine.
207     * @return {@code true} if the character at {@code columnNo} is a space.
208     */
209    private static boolean isSpace(String line, int columnNo) {
210        return line.charAt(columnNo) == ' ';
211    }
212
213    /**
214     * Checks if the {@code line} at {@code columnNo} is preceded by at least 2
215     * whitespaces.
216     *
217     * @param line The line in the file to examine.
218     * @param columnNo The column position in the {@code line} to examine.
219     * @return {@code true} if there are at least 2 whitespace characters before
220     *         {@code columnNo}.
221     */
222    private static boolean isPrecededByMultipleWhitespaces(String line, int columnNo) {
223        return Character.isWhitespace(line.charAt(columnNo))
224                && Character.isWhitespace(line.charAt(columnNo - 1));
225    }
226
227    /**
228     * Checks if the {@code line} at {@code columnNo} is a whitespace character.
229     *
230     * @param line The line in the file to examine.
231     * @param columnNo The column position in the {@code line} to examine.
232     * @return {@code true} if the character at {@code columnNo} is a
233     *         whitespace.
234     */
235    private static boolean isWhitespace(String line, int columnNo) {
236        return Character.isWhitespace(line.charAt(columnNo));
237    }
238
239    /**
240     * Checks if the {@code line} up to and including {@code columnNo} is all
241     * non-whitespace text encountered.
242     *
243     * @param line The line in the file to examine.
244     * @param columnNo The column position in the {@code line} to examine.
245     * @return {@code true} if the column position is the first non-whitespace
246     *         text on the {@code line}.
247     */
248    private static boolean isFirstInLine(String line, int columnNo) {
249        return CommonUtils.isBlank(line.substring(0, columnNo));
250    }
251
252    /**
253     * Checks if the {@code line} at {@code columnNo} is the end of a comment,
254     * '*&#47;'.
255     *
256     * @param line The line in the file to examine.
257     * @param columnNo The column position in the {@code line} to examine.
258     * @return {@code true} if the previous text is a end comment block.
259     */
260    private static boolean isBlockCommentEnd(String line, int columnNo) {
261        return line.substring(0, columnNo).trim().endsWith("*/");
262    }
263
264}