001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 the original author or authors. 004// 005// This library is free software; you can redistribute it and/or 006// modify it under the terms of the GNU Lesser General Public 007// License as published by the Free Software Foundation; either 008// version 2.1 of the License, or (at your option) any later version. 009// 010// This library is distributed in the hope that it will be useful, 011// but WITHOUT ANY WARRANTY; without even the implied warranty of 012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 013// Lesser General Public License for more details. 014// 015// You should have received a copy of the GNU Lesser General Public 016// License along with this library; if not, write to the Free Software 017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 018/////////////////////////////////////////////////////////////////////////////////////////////// 019 020package com.puppycrawl.tools.checkstyle.checks.coding; 021 022import com.puppycrawl.tools.checkstyle.StatelessCheck; 023import com.puppycrawl.tools.checkstyle.api.AbstractCheck; 024import com.puppycrawl.tools.checkstyle.api.DetailAST; 025import com.puppycrawl.tools.checkstyle.api.TokenTypes; 026import com.puppycrawl.tools.checkstyle.utils.TokenUtil; 027 028/** 029 * <div> 030 * Checks correct format of 031 * <a href="https://docs.oracle.com/en/java/javase/17/text-blocks/index.html">Java Text Blocks</a> 032 * as specified in 033 * <a href="https://google.github.io/styleguide/javaguide.html#s4.8.9-text-blocks"> 034 * Google Java Style Guide</a>. 035 * </div> 036 * This Check performs two validations: 037 * <ol> 038 * <li> 039 * It ensures that the opening and closing text-block quotes ({@code """}) each appear on their 040 * own line, with no other item preceding them. 041 * </li> 042 * <li> 043 * Opening and closing quotes are vertically aligned. 044 * </li> 045 * </ol> 046 * Note: Closing quotes can be followed by additional code on the same line. 047 * 048 * @since 12.3.0 049 */ 050@StatelessCheck 051public class TextBlockGoogleStyleFormattingCheck extends AbstractCheck { 052 053 /** 054 * A key is pointing to the warning message text in "messages.properties" file. 055 */ 056 public static final String MSG_OPEN_QUOTES_ERROR = "textblock.format.open"; 057 058 /** 059 * A key is pointing to the warning message text in "messages.properties" file. 060 */ 061 public static final String MSG_CLOSE_QUOTES_ERROR = "textblock.format.close"; 062 063 /** 064 * A key is pointing to the warning message text in "messages.properties" file. 065 */ 066 public static final String MSG_VERTICALLY_UNALIGNED = "textblock.vertically.unaligned"; 067 068 /** 069 * A key is pointing to the warning message text in "messages.properties" file. 070 */ 071 public static final String MSG_TEXT_BLOCK_UNALIGNED = "textblock.Indentation.unaligned"; 072 073 @Override 074 public int[] getDefaultTokens() { 075 return getRequiredTokens(); 076 } 077 078 @Override 079 public int[] getAcceptableTokens() { 080 return getRequiredTokens(); 081 } 082 083 @Override 084 public int[] getRequiredTokens() { 085 return new int[] { 086 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 087 }; 088 } 089 090 @Override 091 public void visitToken(DetailAST ast) { 092 if (!openingQuotesAreAloneOnTheLine(ast)) { 093 log(ast, MSG_OPEN_QUOTES_ERROR); 094 } 095 096 final DetailAST closingQuotes = getClosingQuotes(ast); 097 if (!closingQuotesAreAloneOnTheLine(closingQuotes)) { 098 log(closingQuotes, MSG_CLOSE_QUOTES_ERROR); 099 } 100 101 if (!quotesAreVerticallyAligned(ast, closingQuotes)) { 102 log(closingQuotes, MSG_VERTICALLY_UNALIGNED); 103 } 104 105 final int[] violation = getImproperIndentationLine(ast); 106 if (violation != null) { 107 log(violation[0], violation[1], MSG_TEXT_BLOCK_UNALIGNED); 108 } 109 110 } 111 112 /** 113 * Checks if opening and closing quotes are vertically aligned. 114 * 115 * @param openQuotes the ast to check. 116 * @param closeQuotes the ast to check. 117 * @return true if both quotes have same indentation else false. 118 */ 119 private static boolean quotesAreVerticallyAligned(DetailAST openQuotes, DetailAST closeQuotes) { 120 return openQuotes.getColumnNo() == closeQuotes.getColumnNo(); 121 } 122 123 /** 124 * Gets the {@code TEXT_BLOCK_LITERAL_END} of a {@code TEXT_BLOCK_LITERAL_BEGIN}. 125 * 126 * @param ast the ast to check 127 * @return DetailAST {@code TEXT_BLOCK_LITERAL_END} 128 */ 129 private static DetailAST getClosingQuotes(DetailAST ast) { 130 return ast.getFirstChild().getNextSibling(); 131 } 132 133 /** 134 * Determines if the Opening quotes of text block are not preceded by any code. 135 * 136 * @param openingQuotes opening quotes 137 * @return true if the opening quotes are on the new line. 138 */ 139 private static boolean openingQuotesAreAloneOnTheLine(DetailAST openingQuotes) { 140 DetailAST parent = openingQuotes; 141 boolean quotesAreNotPreceded = true; 142 while (quotesAreNotPreceded || parent.getType() == TokenTypes.ELIST 143 || parent.getType() == TokenTypes.EXPR) { 144 145 parent = parent.getParent(); 146 147 if (parent.getType() == TokenTypes.METHOD_DEF) { 148 quotesAreNotPreceded = !quotesArePrecededWithComma(openingQuotes); 149 } 150 else if (parent.getType() == TokenTypes.QUESTION 151 && openingQuotes.getPreviousSibling() != null) { 152 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, 153 openingQuotes.getPreviousSibling()); 154 } 155 else { 156 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, parent); 157 } 158 159 if (TokenUtil.isOfType(parent.getType(), 160 TokenTypes.LITERAL_RETURN, 161 TokenTypes.VARIABLE_DEF, 162 TokenTypes.METHOD_DEF, 163 TokenTypes.CTOR_DEF, 164 TokenTypes.ENUM_DEF, 165 TokenTypes.CLASS_DEF)) { 166 break; 167 } 168 } 169 return quotesAreNotPreceded; 170 } 171 172 /** 173 * Determines if opening quotes are preceded by {@code ,}. 174 * 175 * @param openingQuotes the quotes 176 * @return true if {@code ,} is present before opening quotes. 177 */ 178 private static boolean quotesArePrecededWithComma(DetailAST openingQuotes) { 179 final DetailAST expression = openingQuotes.getParent(); 180 return expression.getPreviousSibling() != null 181 && TokenUtil.areOnSameLine(openingQuotes, expression.getPreviousSibling()); 182 } 183 184 /** 185 * Determines if the Closing quotes of text block are not preceded by any code. 186 * 187 * @param closingQuotes closing quotes 188 * @return true if the closing quotes are on the new line. 189 */ 190 private static boolean closingQuotesAreAloneOnTheLine(DetailAST closingQuotes) { 191 final DetailAST content = closingQuotes.getPreviousSibling(); 192 final String text = content.getText(); 193 int index = text.length() - 1; 194 while (text.charAt(index) == ' ') { 195 index--; 196 } 197 return Character.isWhitespace(text.charAt(index)); 198 } 199 200 /** 201 * Determines the String content between opening and closing quotes must be indented same. 202 * 203 * @param openingQuotes opening quotes 204 * @return the first line with incorrect indentation, or -1 if all lines are OK. 205 */ 206 private int[] getImproperIndentationLine(DetailAST openingQuotes) { 207 int[] violationLine = null; 208 final int quoteIndent = openingQuotes.getColumnNo(); 209 210 final int startLine = openingQuotes.getLineNo() + 1; 211 final int endLine = getClosingQuotes(openingQuotes).getLineNo() - 1; 212 final String[] lines = getLines(); 213 for (int index = startLine - 1; index < endLine; index++) { 214 final String line = lines[index]; 215 int indent = 0; 216 while (indent < line.length() 217 && Character.isWhitespace(line.charAt(indent))) { 218 indent++; 219 } 220 221 if (indent < quoteIndent) { 222 violationLine = new int[] {index + 1, indent}; 223 break; 224 } 225 } 226 return violationLine; 227 } 228 229}