1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.whitespace;
21  
22  import com.puppycrawl.tools.checkstyle.StatelessCheck;
23  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
24  import com.puppycrawl.tools.checkstyle.api.DetailAST;
25  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
26  
27  /**
28   * <p>
29   * Checks that non-whitespace characters are separated by no more than one
30   * whitespace. Separating characters by tabs or multiple spaces will be
31   * reported. Currently the check doesn't permit horizontal alignment. To inspect
32   * whitespaces before and after comments, set the property
33   * <b>validateComments</b> to true.
34   * </p>
35   *
36   * <p>
37   * Setting <b>validateComments</b> to false will ignore cases like:
38   * </p>
39   *
40   * <pre>
41   * int i;  &#47;&#47; Multiple whitespaces before comment tokens will be ignored.
42   * private void foo(int  &#47;* whitespaces before and after block-comments will be
43   * ignored *&#47;  i) {
44   * </pre>
45   *
46   * <p>
47   * Sometimes, users like to space similar items on different lines to the same
48   * column position for easier reading. This feature isn't supported by this
49   * check, so both braces in the following case will be reported as violations.
50   * </p>
51   *
52   * <pre>
53   * public long toNanos(long d)  { return d;             }  &#47;&#47; 2 violations
54   * public long toMicros(long d) { return d / (C1 / C0); }
55   * </pre>
56   *
57   * <p>
58   * Check have following options:
59   * </p>
60   *
61   * <ul>
62   * <li>validateComments - Boolean when set to {@code true}, whitespaces
63   * surrounding comments will be ignored. Default value is {@code false}.</li>
64   * </ul>
65   *
66   * <p>
67   * To configure the check:
68   * </p>
69   *
70   * <pre>
71   * &lt;module name=&quot;SingleSpaceSeparator&quot;/&gt;
72   * </pre>
73   *
74   * <p>
75   * To configure the check so that it validates comments:
76   * </p>
77   *
78   * <pre>
79   * &lt;module name=&quot;SingleSpaceSeparator&quot;&gt;
80   * &lt;property name=&quot;validateComments&quot; value=&quot;true&quot;/&gt;
81   * &lt;/module&gt;
82   * </pre>
83   *
84   */
85  @StatelessCheck
86  public class SingleSpaceSeparatorCheck extends AbstractCheck {
87  
88      /**
89       * A key is pointing to the warning message text in "messages.properties"
90       * file.
91       */
92      public static final String MSG_KEY = "single.space.separator";
93  
94      /** Indicates if whitespaces surrounding comments will be ignored. */
95      private boolean validateComments;
96  
97      /**
98       * Sets whether or not to validate surrounding whitespaces at comments.
99       *
100      * @param validateComments {@code true} to validate surrounding whitespaces at comments.
101      */
102     public void setValidateComments(boolean validateComments) {
103         this.validateComments = validateComments;
104     }
105 
106     @Override
107     public int[] getDefaultTokens() {
108         return getRequiredTokens();
109     }
110 
111     @Override
112     public int[] getAcceptableTokens() {
113         return getRequiredTokens();
114     }
115 
116     @Override
117     public int[] getRequiredTokens() {
118         return CommonUtil.EMPTY_INT_ARRAY;
119     }
120 
121     // -@cs[SimpleAccessorNameNotation] Overrides method from base class.
122     // Issue: https://github.com/sevntu-checkstyle/sevntu.checkstyle/issues/166
123     @Override
124     public boolean isCommentNodesRequired() {
125         return validateComments;
126     }
127 
128     @Override
129     public void beginTree(DetailAST rootAST) {
130         if (rootAST != null) {
131             visitEachToken(rootAST);
132         }
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         do {
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, MSG_KEY);
155             }
156             if (sibling.getChildCount() >= 1) {
157                 visitEachToken(sibling.getFirstChild());
158             }
159 
160             sibling = sibling.getNextSibling();
161         } while (sibling != null);
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 isSpace(line, columnNo) && !Character.isWhitespace(line.charAt(columnNo - 1));
199     }
200 
201     /**
202      * Checks if the {@code line} at {@code columnNo} is a space.
203      *
204      * @param line The line in the file to examine.
205      * @param columnNo The column position in the {@code line} to examine.
206      * @return {@code true} if the character at {@code columnNo} is a space.
207      */
208     private static boolean isSpace(String line, int columnNo) {
209         return line.charAt(columnNo) == ' ';
210     }
211 
212     /**
213      * Checks if the {@code line} at {@code columnNo} is a whitespace character.
214      *
215      * @param line The line in the file to examine.
216      * @param columnNo The column position in the {@code line} to examine.
217      * @return {@code true} if the character at {@code columnNo} is a
218      *         whitespace.
219      */
220     private static boolean isWhitespace(String line, int columnNo) {
221         return Character.isWhitespace(line.charAt(columnNo));
222     }
223 
224     /**
225      * Checks if the {@code line} up to and including {@code columnNo} is all
226      * non-whitespace text encountered.
227      *
228      * @param line The line in the file to examine.
229      * @param columnNo The column position in the {@code line} to examine.
230      * @return {@code true} if the column position is the first non-whitespace
231      *         text on the {@code line}.
232      */
233     private static boolean isFirstInLine(String line, int columnNo) {
234         return CommonUtil.isBlank(line.substring(0, columnNo));
235     }
236 
237     /**
238      * Checks if the {@code line} at {@code columnNo} is the end of a comment,
239      * '*&#47;'.
240      *
241      * @param line The line in the file to examine.
242      * @param columnNo The column position in the {@code line} to examine.
243      * @return {@code true} if the previous text is a end comment block.
244      */
245     private static boolean isBlockCommentEnd(String line, int columnNo) {
246         return line.substring(0, columnNo).trim().endsWith("*/");
247     }
248 
249 }