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 /** 078 * Creates a new {@code TextBlockGoogleStyleFormattingCheck} instance. 079 */ 080 public TextBlockGoogleStyleFormattingCheck() { 081 // no code by default 082 } 083 084 @Override 085 public int[] getDefaultTokens() { 086 return getRequiredTokens(); 087 } 088 089 @Override 090 public int[] getAcceptableTokens() { 091 return getRequiredTokens(); 092 } 093 094 @Override 095 public int[] getRequiredTokens() { 096 return new int[] { 097 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 098 }; 099 } 100 101 @Override 102 public void visitToken(DetailAST ast) { 103 if (!openingQuotesAreAloneOnTheLine(ast)) { 104 log(ast, MSG_OPEN_QUOTES_ERROR); 105 } 106 107 final DetailAST closingQuotes = getClosingQuotes(ast); 108 if (!closingQuotesAreAloneOnTheLine(closingQuotes)) { 109 log(closingQuotes, MSG_CLOSE_QUOTES_ERROR); 110 } 111 112 if (!quotesAreVerticallyAligned(ast, closingQuotes)) { 113 log(closingQuotes, MSG_VERTICALLY_UNALIGNED); 114 } 115 116 if (!isContentIndentedProperly(ast)) { 117 log(ast.getFirstChild(), MSG_TEXT_BLOCK_CONTENT); 118 } 119 120 } 121 122 /** 123 * Checks if opening and closing quotes are vertically aligned. 124 * 125 * @param openQuotes the ast to check. 126 * @param closeQuotes the ast to check. 127 * @return true if both quotes have same indentation else false. 128 */ 129 private static boolean quotesAreVerticallyAligned(DetailAST openQuotes, DetailAST closeQuotes) { 130 return openQuotes.getColumnNo() == closeQuotes.getColumnNo(); 131 } 132 133 /** 134 * Gets the {@code TEXT_BLOCK_LITERAL_END} of a {@code TEXT_BLOCK_LITERAL_BEGIN}. 135 * 136 * @param ast the ast to check 137 * @return DetailAST {@code TEXT_BLOCK_LITERAL_END} 138 */ 139 private static DetailAST getClosingQuotes(DetailAST ast) { 140 return ast.getFirstChild().getNextSibling(); 141 } 142 143 /** 144 * Determines if the Opening quotes of text block are not preceded by any code. 145 * 146 * @param openingQuotes opening quotes 147 * @return true if the opening quotes are on the new line. 148 */ 149 private static boolean openingQuotesAreAloneOnTheLine(DetailAST openingQuotes) { 150 final DetailAST previousSibling = openingQuotes.getPreviousSibling(); 151 boolean quotesAreNotPreceded = previousSibling == null 152 || !TokenUtil.areOnSameLine(openingQuotes, previousSibling); 153 for (DetailAST parent = openingQuotes.getParent(); parent != null; 154 parent = parent.getParent()) { 155 if (!quotesAreNotPreceded || parent.getType() == TokenTypes.ELIST 156 || parent.getType() == TokenTypes.EXPR) { 157 continue; 158 } 159 if (parent.getType() == TokenTypes.METHOD_DEF) { 160 quotesAreNotPreceded = !quotesArePrecededWithComma(openingQuotes); 161 } 162 else { 163 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, parent); 164 } 165 } 166 return quotesAreNotPreceded; 167 } 168 169 /** 170 * Determines if opening quotes are preceded by {@code ,}. 171 * 172 * @param openingQuotes the quotes 173 * @return true if {@code ,} is present before opening quotes. 174 */ 175 private static boolean quotesArePrecededWithComma(DetailAST openingQuotes) { 176 final DetailAST expression = openingQuotes.getParent(); 177 return expression.getPreviousSibling() != null 178 && TokenUtil.areOnSameLine(openingQuotes, expression.getPreviousSibling()); 179 } 180 181 /** 182 * Determines if the Closing quotes of text block are not preceded by any code. 183 * 184 * @param closingQuotes closing quotes 185 * @return true if the closing quotes are on the new line. 186 */ 187 private static boolean closingQuotesAreAloneOnTheLine(DetailAST closingQuotes) { 188 final DetailAST content = closingQuotes.getPreviousSibling(); 189 final String text = content.getText(); 190 int index = text.length() - 1; 191 while (text.charAt(index) == ' ') { 192 index--; 193 } 194 return Character.isWhitespace(text.charAt(index)); 195 } 196 197 /** 198 * Determine if the Text Block content indentation is equal or less than 199 * opening quotes indentation. 200 * 201 * @param openingQuotes openingQuotes 202 * @return true if text-block content is properly indented. 203 */ 204 private static boolean isContentIndentedProperly(DetailAST openingQuotes) { 205 final int quoteIndent = openingQuotes.getColumnNo(); 206 final DetailAST textAst = openingQuotes.getFirstChild(); 207 boolean result = true; 208 209 final String[] lines = textAst.getText().split("\n", -1); 210 211 for (String line : lines) { 212 213 int indentation = 0; 214 while (indentation < line.length() 215 && Character.isWhitespace(line.charAt(indentation))) { 216 indentation++; 217 } 218 219 if (indentation < quoteIndent && indentation < line.length()) { 220 result = false; 221 } 222 } 223 224 return result; 225 } 226 227}