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.charset.StandardCharsets;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
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                  Path.of(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         final CommentType type = getCommentType(comment);
157         return switch (type) {
158             case MULTILEVEL ->
159                 isMultiLevelCommentConsistent(comment, indentInComment, isWarnComment);
160             case SINGLE_LEVEL ->
161                 isSingleLevelCommentConsistent(comment, indentInComment, isWarnComment);
162             case NON_STRICT_LEVEL ->
163                 isNonStrictCommentConsistent(comment, indentInComment, isWarnComment);
164             case UNKNOWN ->
165                     throw new IllegalArgumentException("Cannot determine comment consistent");
166             default ->
167                     throw new IllegalStateException("Cannot determine comment is consistent");
168         };
169     }
170 
171     /**
172      * Checks if a Non Strict Comment is consistent or not.
173      *
174      * @param comment the comment to be checked.
175      * @param indentInComment the actual indentation in that comment.
176      * @param isWarnComment if comment is Warn comment or not.
177      * @return true if Non Strict comment is consistent else returns false.
178      */
179     private static boolean isNonStrictCommentConsistent(String comment,
180             int indentInComment, boolean isWarnComment) {
181         final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
182         nonStrictLevelMatch.matches();
183         final int expectedMinimalIndent = Integer.parseInt(nonStrictLevelMatch.group(1));
184 
185         return indentInComment >= expectedMinimalIndent && !isWarnComment
186                 || indentInComment < expectedMinimalIndent && isWarnComment;
187     }
188 
189     /**
190      * Checks if a Single Level comment is consistent or not.
191      *
192      * @param comment the comment to be checked.
193      * @param indentInComment the actual indentation in that comment.
194      * @param isWarnComment if comment is Warn comment or not.
195      * @return true if Single Level comment is consistent or not else returns false.
196      */
197     private static boolean isSingleLevelCommentConsistent(String comment,
198             int indentInComment, boolean isWarnComment) {
199         final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
200         singleLevelMatch.matches();
201         final int expectedLevel = Integer.parseInt(singleLevelMatch.group(1));
202 
203         return expectedLevel == indentInComment && !isWarnComment
204                 || expectedLevel != indentInComment && isWarnComment;
205     }
206 
207     /**
208      * Checks if a Multi-Level comment is consistent or not.
209      *
210      * @param comment the comment to be checked.
211      * @param indentInComment the actual indentation in that comment.
212      * @param isWarnComment if comment is Warn comment or not.
213      * @return true if Multi-Level comment is consistent or not else returns false.
214      */
215     private static boolean isMultiLevelCommentConsistent(String comment,
216             int indentInComment, boolean isWarnComment) {
217         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
218         multilevelMatch.matches();
219         final String[] levels = multilevelMatch.group(1).split(",");
220         final String indentInCommentStr = String.valueOf(indentInComment);
221         final boolean containsActualLevel =
222                 Arrays.asList(levels).contains(indentInCommentStr);
223 
224         return containsActualLevel && !isWarnComment
225                 || !containsActualLevel && isWarnComment;
226     }
227 
228     /**
229      * Returns the type of Comment by matching with specific regex for each type.
230      * Possible types include {@link CommentType#MULTILEVEL}, {@link CommentType#SINGLE_LEVEL},
231      * {@link CommentType#NON_STRICT_LEVEL}, and {@link CommentType#UNKNOWN}.
232      *
233      * @param comment the comment whose type is to be returned.
234      * @return {@link CommentType} instance for the given comment.
235      */
236     private static CommentType getCommentType(String comment) {
237         CommentType result = CommentType.UNKNOWN;
238         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
239         if (multilevelMatch.matches()) {
240             result = CommentType.MULTILEVEL;
241         }
242         else {
243             final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
244             if (singleLevelMatch.matches()) {
245                 result = CommentType.SINGLE_LEVEL;
246             }
247             else {
248                 final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
249                 if (nonStrictLevelMatch.matches()) {
250                     result = CommentType.NON_STRICT_LEVEL;
251                 }
252             }
253         }
254         return result;
255     }
256 
257     /**
258      * Returns starting position of a Line.
259      *
260      * @param line the line whose starting position is required.
261      * @param tabWidth tab width (passed value is 4 to this method).
262      * @return starting position of given line.
263      */
264     private static int getLineStart(String line, final int tabWidth) {
265         int lineStart = 0;
266         for (int index = 0; index < line.length(); ++index) {
267             if (!Character.isWhitespace(line.charAt(index))) {
268                 lineStart = CommonUtil.lengthExpandedTabs(line, index, tabWidth);
269                 break;
270             }
271         }
272         return lineStart;
273     }
274 
275     private enum CommentType {
276 
277         MULTILEVEL,
278         SINGLE_LEVEL,
279         NON_STRICT_LEVEL,
280         UNKNOWN,
281 
282     }
283 
284 }