View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2024 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.google.checkstyle.test.base;
21  
22  import java.io.BufferedReader;
23  import java.io.IOException;
24  import java.nio.charset.StandardCharsets;
25  import java.nio.file.Files;
26  import java.nio.file.Paths;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.regex.Matcher;
32  import java.util.regex.Pattern;
33  
34  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
35  
36  public abstract class AbstractIndentationTestSupport extends AbstractGoogleModuleTestSupport {
37  
38      private static final int TAB_WIDTH = 4;
39  
40      private static final Pattern NONEMPTY_LINE_REGEX =
41              Pattern.compile(".*?\\S+.*?");
42  
43      private static final Pattern LINE_WITH_COMMENT_REGEX =
44              Pattern.compile(".*?\\S+.*?(//indent:(\\d+) exp:((>=\\d+)|(\\d+(,\\d+)*?))( warn)?)");
45  
46      private static final Pattern GET_INDENT_FROM_COMMENT_REGEX =
47              Pattern.compile("//indent:(\\d+).*?");
48  
49      private static final Pattern MULTILEVEL_COMMENT_REGEX =
50              Pattern.compile("//indent:\\d+ exp:(\\d+(,\\d+)+?)( warn)?");
51  
52      private static final Pattern SINGLE_LEVEL_COMMENT_REGEX =
53              Pattern.compile("//indent:\\d+ exp:(\\d+)( warn)?");
54  
55      private static final Pattern NON_STRICT_LEVEL_COMMENT_REGEX =
56              Pattern.compile("//indent:\\d+ exp:>=(\\d+)( warn)?");
57  
58      @Override
59      protected Integer[] getLinesWithWarn(String fileName) throws IOException {
60          return getLinesWithWarnAndCheckComments(fileName, TAB_WIDTH);
61      }
62  
63      /**
64       * Returns line numbers for lines with 'warn' comments.
65       *
66       * @param aFileName file name.
67       * @param tabWidth tab width.
68       * @return array of line numbers containing 'warn' comments ('warn').
69       * @throws IOException while reading the file for checking lines.
70       * @throws IllegalStateException if file has incorrect indentation in comment or
71       *     comment is inconsistent or if file has no indentation comment.
72       */
73      private static Integer[] getLinesWithWarnAndCheckComments(String aFileName,
74              final int tabWidth)
75                      throws IOException {
76          final List<Integer> result = new ArrayList<>();
77          try (BufferedReader br = Files.newBufferedReader(
78                  Paths.get(aFileName), StandardCharsets.UTF_8)) {
79              int lineNumber = 1;
80              for (String line = br.readLine(); line != null; line = br.readLine()) {
81                  final Matcher match = LINE_WITH_COMMENT_REGEX.matcher(line);
82                  if (match.matches()) {
83                      final String comment = match.group(1);
84                      final int indentInComment = getIndentFromComment(comment);
85                      final int actualIndent = getLineStart(line, tabWidth);
86  
87                      if (actualIndent != indentInComment) {
88                          throw new IllegalStateException(String.format(Locale.ROOT,
89                                          "File \"%1$s\" has incorrect indentation in comment."
90                                                          + "Line %2$d: comment:%3$d, actual:%4$d.",
91                                          aFileName,
92                                          lineNumber,
93                                          indentInComment,
94                                          actualIndent));
95                      }
96  
97                      if (isWarnComment(comment)) {
98                          result.add(lineNumber);
99                      }
100 
101                     if (!isCommentConsistent(comment)) {
102                         throw new IllegalStateException(String.format(Locale.ROOT,
103                                         "File \"%1$s\" has inconsistent comment on line %2$d",
104                                         aFileName,
105                                         lineNumber));
106                     }
107                 }
108                 else if (NONEMPTY_LINE_REGEX.matcher(line).matches()) {
109                     throw new IllegalStateException(String.format(Locale.ROOT,
110                                     "File \"%1$s\" has no indentation comment or its format "
111                                                     + "malformed. Error on line: %2$d(%3$s)",
112                                     aFileName,
113                                     lineNumber,
114                                     line));
115                 }
116                 lineNumber++;
117             }
118         }
119         return result.toArray(new Integer[0]);
120     }
121 
122     /**
123      * Returns amount of indentation from the comment.
124      *
125      * @param comment the indentation comment to be checked.
126      * @return amount of indentation in comment.
127      */
128     private static int getIndentFromComment(String comment) {
129         final Matcher match = GET_INDENT_FROM_COMMENT_REGEX.matcher(comment);
130         match.matches();
131         return Integer.parseInt(match.group(1));
132     }
133 
134     /**
135      * Checks if comment is a warn comment (ends with "warn") or not.
136      *
137      * @param comment the comment to be checked.
138      * @return true if comment ends with " warn" else returns false.
139      */
140     private static boolean isWarnComment(String comment) {
141         return comment.endsWith(" warn");
142     }
143 
144     /**
145      * Checks if a comment of comment type is consistent or not.
146      *
147      * @param comment the comment to be checked.
148      * @return true if comment is consistent based on expected indent level, actual indent level
149      *     and if comment is a warn comment else it returns false.
150      * @throws IllegalArgumentException if comment type is unknown and cannot determine consistency.
151      * @throws IllegalStateException if cannot determine that comment is consistent(default case).
152      */
153     private static boolean isCommentConsistent(String comment) {
154         final int indentInComment = getIndentFromComment(comment);
155         final boolean isWarnComment = isWarnComment(comment);
156 
157         final boolean result;
158         final CommentType type = getCommentType(comment);
159         switch (type) {
160             case MULTILEVEL:
161                 result = isMultiLevelCommentConsistent(comment, indentInComment, isWarnComment);
162                 break;
163 
164             case SINGLE_LEVEL:
165                 result = isSingleLevelCommentConsistent(comment, indentInComment, isWarnComment);
166                 break;
167 
168             case NON_STRICT_LEVEL:
169                 result = isNonStrictCommentConsistent(comment, indentInComment, isWarnComment);
170                 break;
171 
172             case UNKNOWN:
173                 throw new IllegalArgumentException("Cannot determine comment consistent");
174 
175             default:
176                 throw new IllegalStateException("Cannot determine comment is consistent");
177         }
178         return result;
179     }
180 
181     /**
182      * Checks if a Non Strict Comment is consistent or not.
183      *
184      * @param comment the comment to be checked.
185      * @param indentInComment the actual indentation in that comment.
186      * @param isWarnComment if comment is Warn comment or not.
187      * @return true if Non Strict comment is consistent else returns false.
188      */
189     private static boolean isNonStrictCommentConsistent(String comment,
190             int indentInComment, boolean isWarnComment) {
191         final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
192         nonStrictLevelMatch.matches();
193         final int expectedMinimalIndent = Integer.parseInt(nonStrictLevelMatch.group(1));
194 
195         return indentInComment >= expectedMinimalIndent && !isWarnComment
196                 || indentInComment < expectedMinimalIndent && isWarnComment;
197     }
198 
199     /**
200      * Checks if a Single Level comment is consistent or not.
201      *
202      * @param comment the comment to be checked.
203      * @param indentInComment the actual indentation in that comment.
204      * @param isWarnComment if comment is Warn comment or not.
205      * @return true if Single Level comment is consistent or not else returns false.
206      */
207     private static boolean isSingleLevelCommentConsistent(String comment,
208             int indentInComment, boolean isWarnComment) {
209         final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
210         singleLevelMatch.matches();
211         final int expectedLevel = Integer.parseInt(singleLevelMatch.group(1));
212 
213         return expectedLevel == indentInComment && !isWarnComment
214                 || expectedLevel != indentInComment && isWarnComment;
215     }
216 
217     /**
218      * Checks if a Multi-Level comment is consistent or not.
219      *
220      * @param comment the comment to be checked.
221      * @param indentInComment the actual indentation in that comment.
222      * @param isWarnComment if comment is Warn comment or not.
223      * @return true if Multi-Level comment is consistent or not else returns false.
224      */
225     private static boolean isMultiLevelCommentConsistent(String comment,
226             int indentInComment, boolean isWarnComment) {
227         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
228         multilevelMatch.matches();
229         final String[] levels = multilevelMatch.group(1).split(",");
230         final String indentInCommentStr = String.valueOf(indentInComment);
231         final boolean containsActualLevel =
232                 Arrays.asList(levels).contains(indentInCommentStr);
233 
234         return containsActualLevel && !isWarnComment
235                 || !containsActualLevel && isWarnComment;
236     }
237 
238     /**
239      * Returns the type of Comment by matching with specific regex for each type.
240      * Possible types include {@link CommentType#MULTILEVEL}, {@link CommentType#SINGLE_LEVEL},
241      * {@link CommentType#NON_STRICT_LEVEL}, and {@link CommentType#UNKNOWN}.
242      *
243      * @param comment the comment whose type is to be returned.
244      * @return {@link CommentType} instance for the given comment.
245      */
246     private static CommentType getCommentType(String comment) {
247         CommentType result = CommentType.UNKNOWN;
248         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
249         if (multilevelMatch.matches()) {
250             result = CommentType.MULTILEVEL;
251         }
252         else {
253             final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
254             if (singleLevelMatch.matches()) {
255                 result = CommentType.SINGLE_LEVEL;
256             }
257             else {
258                 final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
259                 if (nonStrictLevelMatch.matches()) {
260                     result = CommentType.NON_STRICT_LEVEL;
261                 }
262             }
263         }
264         return result;
265     }
266 
267     /**
268      * Returns starting position of a Line.
269      *
270      * @param line the line whose starting position is required.
271      * @param tabWidth tab width (passed value is 4 to this method).
272      * @return starting position of given line.
273      */
274     private static int getLineStart(String line, final int tabWidth) {
275         int lineStart = 0;
276         for (int index = 0; index < line.length(); ++index) {
277             if (!Character.isWhitespace(line.charAt(index))) {
278                 lineStart = CommonUtil.lengthExpandedTabs(line, index, tabWidth);
279                 break;
280             }
281         }
282         return lineStart;
283     }
284 
285     private enum CommentType {
286 
287         MULTILEVEL,
288         SINGLE_LEVEL,
289         NON_STRICT_LEVEL,
290         UNKNOWN,
291 
292     }
293 
294 }