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 * <li> 046 * Each line of text in the text block must be indented at 047 * least as much as the opening and closing quotes. 048 * </li> 049 * </ol> 050 * Note: Closing quotes can be followed by additional code on the same line. 051 * 052 * @since 12.3.0 053 */ 054@StatelessCheck 055public class TextBlockGoogleStyleFormattingCheck extends AbstractCheck { 056 057 /** 058 * A key is pointing to the warning message text in "messages.properties" file. 059 */ 060 public static final String MSG_OPEN_QUOTES_ERROR = "textblock.format.open"; 061 062 /** 063 * A key is pointing to the warning message text in "messages.properties" file. 064 */ 065 public static final String MSG_CLOSE_QUOTES_ERROR = "textblock.format.close"; 066 067 /** 068 * A key is pointing to the warning message text in "messages.properties" file. 069 */ 070 public static final String MSG_VERTICALLY_UNALIGNED = "textblock.vertically.unaligned"; 071 072 /** 073 * A key is pointing to the warning message text in "messages.properties" file. 074 */ 075 public static final String MSG_TEXT_BLOCK_CONTENT = "textblock.indentation"; 076 077 @Override 078 public int[] getDefaultTokens() { 079 return getRequiredTokens(); 080 } 081 082 @Override 083 public int[] getAcceptableTokens() { 084 return getRequiredTokens(); 085 } 086 087 @Override 088 public int[] getRequiredTokens() { 089 return new int[] { 090 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 091 }; 092 } 093 094 @Override 095 public void visitToken(DetailAST ast) { 096 if (!openingQuotesAreAloneOnTheLine(ast)) { 097 log(ast, MSG_OPEN_QUOTES_ERROR); 098 } 099 100 final DetailAST closingQuotes = getClosingQuotes(ast); 101 if (!closingQuotesAreAloneOnTheLine(closingQuotes)) { 102 log(closingQuotes, MSG_CLOSE_QUOTES_ERROR); 103 } 104 105 if (!quotesAreVerticallyAligned(ast, closingQuotes)) { 106 log(closingQuotes, MSG_VERTICALLY_UNALIGNED); 107 } 108 109 if (!isContentIndentedProperly(ast)) { 110 log(ast.getFirstChild(), MSG_TEXT_BLOCK_CONTENT); 111 } 112 113 } 114 115 /** 116 * Checks if opening and closing quotes are vertically aligned. 117 * 118 * @param openQuotes the ast to check. 119 * @param closeQuotes the ast to check. 120 * @return true if both quotes have same indentation else false. 121 */ 122 private static boolean quotesAreVerticallyAligned(DetailAST openQuotes, DetailAST closeQuotes) { 123 return openQuotes.getColumnNo() == closeQuotes.getColumnNo(); 124 } 125 126 /** 127 * Gets the {@code TEXT_BLOCK_LITERAL_END} of a {@code TEXT_BLOCK_LITERAL_BEGIN}. 128 * 129 * @param ast the ast to check 130 * @return DetailAST {@code TEXT_BLOCK_LITERAL_END} 131 */ 132 private static DetailAST getClosingQuotes(DetailAST ast) { 133 return ast.getFirstChild().getNextSibling(); 134 } 135 136 /** 137 * Determines if the Opening quotes of text block are not preceded by any code. 138 * 139 * @param openingQuotes opening quotes 140 * @return true if the opening quotes are on the new line. 141 */ 142 private static boolean openingQuotesAreAloneOnTheLine(DetailAST openingQuotes) { 143 DetailAST parent = openingQuotes; 144 boolean quotesAreNotPreceded = true; 145 while (quotesAreNotPreceded || parent.getType() == TokenTypes.ELIST 146 || parent.getType() == TokenTypes.EXPR) { 147 148 parent = parent.getParent(); 149 150 if (parent.getType() == TokenTypes.METHOD_DEF) { 151 quotesAreNotPreceded = !quotesArePrecededWithComma(openingQuotes); 152 } 153 else if (parent.getType() == TokenTypes.QUESTION 154 && openingQuotes.getPreviousSibling() != null) { 155 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, 156 openingQuotes.getPreviousSibling()); 157 } 158 else { 159 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, parent); 160 } 161 162 if (TokenUtil.isOfType(parent.getType(), 163 TokenTypes.LITERAL_RETURN, 164 TokenTypes.VARIABLE_DEF, 165 TokenTypes.METHOD_DEF, 166 TokenTypes.CTOR_DEF, 167 TokenTypes.ENUM_DEF, 168 TokenTypes.CLASS_DEF)) { 169 break; 170 } 171 } 172 return quotesAreNotPreceded; 173 } 174 175 /** 176 * Determines if opening quotes are preceded by {@code ,}. 177 * 178 * @param openingQuotes the quotes 179 * @return true if {@code ,} is present before opening quotes. 180 */ 181 private static boolean quotesArePrecededWithComma(DetailAST openingQuotes) { 182 final DetailAST expression = openingQuotes.getParent(); 183 return expression.getPreviousSibling() != null 184 && TokenUtil.areOnSameLine(openingQuotes, expression.getPreviousSibling()); 185 } 186 187 /** 188 * Determines if the Closing quotes of text block are not preceded by any code. 189 * 190 * @param closingQuotes closing quotes 191 * @return true if the closing quotes are on the new line. 192 */ 193 private static boolean closingQuotesAreAloneOnTheLine(DetailAST closingQuotes) { 194 final DetailAST content = closingQuotes.getPreviousSibling(); 195 final String text = content.getText(); 196 int index = text.length() - 1; 197 while (text.charAt(index) == ' ') { 198 index--; 199 } 200 return Character.isWhitespace(text.charAt(index)); 201 } 202 203 /** 204 * Determine if the Text Block content indentation is equal or less than 205 * opening quotes indentation. 206 * 207 * @param openingQuotes openingQuotes 208 * @return true if text-block content is properly indented. 209 */ 210 private static boolean isContentIndentedProperly(DetailAST openingQuotes) { 211 final int quoteIndent = openingQuotes.getColumnNo(); 212 final DetailAST textAst = openingQuotes.getFirstChild(); 213 boolean result = true; 214 215 final String[] lines = textAst.getText().split("\n", -1); 216 217 for (String line : lines) { 218 219 int indentation = 0; 220 while (indentation < line.length() 221 && Character.isWhitespace(line.charAt(indentation))) { 222 indentation++; 223 } 224 225 if (indentation < quoteIndent && indentation < line.length()) { 226 result = false; 227 } 228 } 229 230 return result; 231 } 232}