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