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