//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.google.checkstyle.test.base; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.puppycrawl.tools.checkstyle.utils.CommonUtils; public class BaseIndentationCheckSupport extends BaseCheckTestSupport { private static final int TAB_WIDTH = 4; private static final Pattern NONEMPTY_LINE_REGEX = Pattern.compile(".*?\\S+.*?"); private static final Pattern LINE_WITH_COMMENT_REGEX = Pattern.compile(".*?\\S+.*?(//indent:(\\d+) exp:((>=\\d+)|(\\d+(,\\d+)*?))( warn)?)"); private static final Pattern GET_INDENT_FROM_COMMENT_REGEX = Pattern.compile("//indent:(\\d+).*?"); private static final Pattern MULTILEVEL_COMMENT_REGEX = Pattern.compile("//indent:\\d+ exp:(\\d+(,\\d+)+?)( warn)?"); private static final Pattern SINGLE_LEVEL_COMMENT_REGEX = Pattern.compile("//indent:\\d+ exp:(\\d+)( warn)?"); private static final Pattern NON_STRICT_LEVEL_COMMENT_REGEX = Pattern.compile("//indent:\\d+ exp:>=(\\d+)( warn)?"); @Override protected Integer[] getLinesWithWarn(String fileName) throws IOException { return getLinesWithWarnAndCheckComments(fileName, TAB_WIDTH); } private static Integer[] getLinesWithWarnAndCheckComments(String aFileName, final int tabWidth) throws IOException { final List<Integer> result = new ArrayList<>(); try (BufferedReader br = new BufferedReader(new InputStreamReader( new FileInputStream(aFileName), StandardCharsets.UTF_8))) { int lineNumber = 1; for (String line = br.readLine(); line != null; line = br.readLine()) { final Matcher match = LINE_WITH_COMMENT_REGEX.matcher(line); if (match.matches()) { final String comment = match.group(1); final int indentInComment = getIndentFromComment(comment); final int actualIndent = getLineStart(line, tabWidth); if (actualIndent != indentInComment) { throw new IllegalStateException(String.format(Locale.ROOT, "File \"%1$s\" has incorrect indentation in comment." + "Line %2$d: comment:%3$d, actual:%4$d.", aFileName, lineNumber, indentInComment, actualIndent)); } if (isWarnComment(comment)) { result.add(lineNumber); } if (!isCommentConsistent(comment)) { throw new IllegalStateException(String.format(Locale.ROOT, "File \"%1$s\" has inconsistent comment on line %2$d", aFileName, lineNumber)); } } else if (NONEMPTY_LINE_REGEX.matcher(line).matches()) { throw new IllegalStateException(String.format(Locale.ROOT, "File \"%1$s\" has no indentation comment or its format " + "malformed. Error on line: %2$d(%3$s)", aFileName, lineNumber, line)); } lineNumber++; } } return result.toArray(new Integer[result.size()]); } private static int getIndentFromComment(String comment) { final Matcher match = GET_INDENT_FROM_COMMENT_REGEX.matcher(comment); match.matches(); return Integer.parseInt(match.group(1)); } private static boolean isWarnComment(String comment) { return comment.endsWith(" warn"); } private static boolean isCommentConsistent(String comment) { final int indentInComment = getIndentFromComment(comment); final boolean isWarnComment = isWarnComment(comment); final boolean result; final CommentType type = getCommentType(comment); switch (type) { case MULTILEVEL: result = isMultiLevelCommentConsistent(comment, indentInComment, isWarnComment); break; case SINGLE_LEVEL: result = isSingleLevelCommentConsistent(comment, indentInComment, isWarnComment); break; case NON_STRICT_LEVEL: result = isNonStrictCommentConsistent(comment, indentInComment, isWarnComment); break; case UNKNOWN: throw new IllegalArgumentException("Cannot determine comment consistent"); default: throw new IllegalStateException("Cannot determine comment is consistent"); } return result; } private static boolean isNonStrictCommentConsistent(String comment, int indentInComment, boolean isWarnComment) { final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment); nonStrictLevelMatch.matches(); final int expectedMinimalIndent = Integer.parseInt(nonStrictLevelMatch.group(1)); return indentInComment >= expectedMinimalIndent && !isWarnComment || indentInComment < expectedMinimalIndent && isWarnComment; } private static boolean isSingleLevelCommentConsistent(String comment, int indentInComment, boolean isWarnComment) { final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment); singleLevelMatch.matches(); final int expectedLevel = Integer.parseInt(singleLevelMatch.group(1)); return expectedLevel == indentInComment && !isWarnComment || expectedLevel != indentInComment && isWarnComment; } private static boolean isMultiLevelCommentConsistent(String comment, int indentInComment, boolean isWarnComment) { final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment); multilevelMatch.matches(); final String[] levels = multilevelMatch.group(1).split(","); final String indentInCommentStr = String.valueOf(indentInComment); final boolean containsActualLevel = Arrays.asList(levels).contains(indentInCommentStr); return containsActualLevel && !isWarnComment || !containsActualLevel && isWarnComment; } private static CommentType getCommentType(String comment) { CommentType result = CommentType.UNKNOWN; final Matcher multilevelMatch = MULTILEVEL_COMMENT_REGEX.matcher(comment); if (multilevelMatch.matches()) { result = CommentType.MULTILEVEL; } else { final Matcher singleLevelMatch = SINGLE_LEVEL_COMMENT_REGEX.matcher(comment); if (singleLevelMatch.matches()) { result = CommentType.SINGLE_LEVEL; } else { final Matcher nonStrictLevelMatch = NON_STRICT_LEVEL_COMMENT_REGEX.matcher(comment); if (nonStrictLevelMatch.matches()) { result = CommentType.NON_STRICT_LEVEL; } } } return result; } private static int getLineStart(String line, final int tabWidth) { int lineStart = 0; for (int index = 0; index < line.length(); ++index) { if (!Character.isWhitespace(line.charAt(index))) { lineStart = CommonUtils.lengthExpandedTabs(line, index, tabWidth); break; } } return lineStart; } private enum CommentType { MULTILEVEL, SINGLE_LEVEL, NON_STRICT_LEVEL, UNKNOWN } }