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.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 AbstractModuleTestSupport {
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      private static Integer[] getLinesWithWarnAndCheckComments(String aFileName,
64              final int tabWidth)
65                      throws IOException {
66          final List<Integer> result = new ArrayList<>();
67          try (BufferedReader br = Files.newBufferedReader(
68                  Paths.get(aFileName), StandardCharsets.UTF_8)) {
69              int lineNumber = 1;
70              for (String line = br.readLine(); line != null; line = br.readLine()) {
71                  final Matcher match = LINE_WITH_COMMENT_REGEX.matcher(line);
72                  if (match.matches()) {
73                      final String comment = match.group(1);
74                      final int indentInComment = getIndentFromComment(comment);
75                      final int actualIndent = getLineStart(line, tabWidth);
76  
77                      if (actualIndent != indentInComment) {
78                          throw new IllegalStateException(String.format(Locale.ROOT,
79                                          "File \"%1$s\" has incorrect indentation in comment."
80                                                          + "Line %2$d: comment:%3$d, actual:%4$d.",
81                                          aFileName,
82                                          lineNumber,
83                                          indentInComment,
84                                          actualIndent));
85                      }
86  
87                      if (isWarnComment(comment)) {
88                          result.add(lineNumber);
89                      }
90  
91                      if (!isCommentConsistent(comment)) {
92                          throw new IllegalStateException(String.format(Locale.ROOT,
93                                          "File \"%1$s\" has inconsistent comment on line %2$d",
94                                          aFileName,
95                                          lineNumber));
96                      }
97                  }
98                  else if (NONEMPTY_LINE_REGEX.matcher(line).matches()) {
99                      throw new IllegalStateException(String.format(Locale.ROOT,
100                                     "File \"%1$s\" has no indentation comment or its format "
101                                                     + "malformed. Error on line: %2$d(%3$s)",
102                                     aFileName,
103                                     lineNumber,
104                                     line));
105                 }
106                 lineNumber++;
107             }
108         }
109         return result.toArray(new Integer[0]);
110     }
111 
112     private static int getIndentFromComment(String comment) {
113         final Matcher match = GET_INDENT_FROM_COMMENT_REGEX.matcher(comment);
114         match.matches();
115         return Integer.parseInt(match.group(1));
116     }
117 
118     private static boolean isWarnComment(String comment) {
119         return comment.endsWith(" warn");
120     }
121 
122     private static boolean isCommentConsistent(String comment) {
123         final int indentInComment = getIndentFromComment(comment);
124         final boolean isWarnComment = isWarnComment(comment);
125 
126         final boolean result;
127         final CommentType type = getCommentType(comment);
128         switch (type) {
129             case MULTILEVEL:
130                 result = isMultiLevelCommentConsistent(comment, indentInComment, isWarnComment);
131                 break;
132 
133             case SINGLE_LEVEL:
134                 result = isSingleLevelCommentConsistent(comment, indentInComment, isWarnComment);
135                 break;
136 
137             case NON_STRICT_LEVEL:
138                 result = isNonStrictCommentConsistent(comment, indentInComment, isWarnComment);
139                 break;
140 
141             case UNKNOWN:
142                 throw new IllegalArgumentException("Cannot determine comment consistent");
143 
144             default:
145                 throw new IllegalStateException("Cannot determine comment is consistent");
146         }
147         return result;
148     }
149 
150     private static boolean isNonStrictCommentConsistent(String comment,
151             int indentInComment, boolean isWarnComment) {
152         final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
153         nonStrictLevelMatch.matches();
154         final int expectedMinimalIndent = Integer.parseInt(nonStrictLevelMatch.group(1));
155 
156         return indentInComment >= expectedMinimalIndent && !isWarnComment
157                 || indentInComment < expectedMinimalIndent && isWarnComment;
158     }
159 
160     private static boolean isSingleLevelCommentConsistent(String comment,
161             int indentInComment, boolean isWarnComment) {
162         final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
163         singleLevelMatch.matches();
164         final int expectedLevel = Integer.parseInt(singleLevelMatch.group(1));
165 
166         return expectedLevel == indentInComment && !isWarnComment
167                 || expectedLevel != indentInComment && isWarnComment;
168     }
169 
170     private static boolean isMultiLevelCommentConsistent(String comment,
171             int indentInComment, boolean isWarnComment) {
172         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
173         multilevelMatch.matches();
174         final String[] levels = multilevelMatch.group(1).split(",");
175         final String indentInCommentStr = String.valueOf(indentInComment);
176         final boolean containsActualLevel =
177                 Arrays.asList(levels).contains(indentInCommentStr);
178 
179         return containsActualLevel && !isWarnComment
180                 || !containsActualLevel && isWarnComment;
181     }
182 
183     private static CommentType getCommentType(String comment) {
184         CommentType result = CommentType.UNKNOWN;
185         final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment);
186         if (multilevelMatch.matches()) {
187             result = CommentType.MULTILEVEL;
188         }
189         else {
190             final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment);
191             if (singleLevelMatch.matches()) {
192                 result = CommentType.SINGLE_LEVEL;
193             }
194             else {
195                 final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment);
196                 if (nonStrictLevelMatch.matches()) {
197                     result = CommentType.NON_STRICT_LEVEL;
198                 }
199             }
200         }
201         return result;
202     }
203 
204     private static int getLineStart(String line, final int tabWidth) {
205         int lineStart = 0;
206         for (int index = 0; index < line.length(); ++index) {
207             if (!Character.isWhitespace(line.charAt(index))) {
208                 lineStart = CommonUtil.lengthExpandedTabs(line, index, tabWidth);
209                 break;
210             }
211         }
212         return lineStart;
213     }
214 
215     private enum CommentType {
216 
217         MULTILEVEL,
218         SINGLE_LEVEL,
219         NON_STRICT_LEVEL,
220         UNKNOWN,
221 
222     }
223 
224 }