001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2025 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 @Override 069 public int[] getDefaultTokens() { 070 return getRequiredTokens(); 071 } 072 073 @Override 074 public int[] getAcceptableTokens() { 075 return getRequiredTokens(); 076 } 077 078 @Override 079 public int[] getRequiredTokens() { 080 return new int[] { 081 TokenTypes.TEXT_BLOCK_LITERAL_BEGIN, 082 }; 083 } 084 085 @Override 086 public void visitToken(DetailAST ast) { 087 if (!openingQuotesAreAloneOnTheLine(ast)) { 088 log(ast, MSG_OPEN_QUOTES_ERROR); 089 } 090 091 final DetailAST closingQuotes = getClosingQuotes(ast); 092 if (!closingQuotesAreAloneOnTheLine(closingQuotes)) { 093 log(closingQuotes, MSG_CLOSE_QUOTES_ERROR); 094 } 095 096 if (!quotesAreVerticallyAligned(ast, closingQuotes)) { 097 log(closingQuotes, MSG_VERTICALLY_UNALIGNED); 098 } 099 } 100 101 /** 102 * Checks if opening and closing quotes are vertically aligned. 103 * 104 * @param openQuotes the ast to check. 105 * @param closeQuotes the ast to check. 106 * @return true if both quotes have same indentation else false. 107 */ 108 private static boolean quotesAreVerticallyAligned(DetailAST openQuotes, DetailAST closeQuotes) { 109 return openQuotes.getColumnNo() == closeQuotes.getColumnNo(); 110 } 111 112 /** 113 * Gets the {@code TEXT_BLOCK_LITERAL_END} of a {@code TEXT_BLOCK_LITERAL_BEGIN}. 114 * 115 * @param ast the ast to check 116 * @return DetailAST {@code TEXT_BLOCK_LITERAL_END} 117 */ 118 private static DetailAST getClosingQuotes(DetailAST ast) { 119 return ast.getFirstChild().getNextSibling(); 120 } 121 122 /** 123 * Determines if the Opening quotes of text block are not preceded by any code. 124 * 125 * @param openingQuotes opening quotes 126 * @return true if the opening quotes are on the new line. 127 */ 128 private static boolean openingQuotesAreAloneOnTheLine(DetailAST openingQuotes) { 129 DetailAST parent = openingQuotes; 130 boolean quotesAreNotPreceded = true; 131 while (quotesAreNotPreceded || parent.getType() == TokenTypes.ELIST 132 || parent.getType() == TokenTypes.EXPR) { 133 134 parent = parent.getParent(); 135 136 if (parent.getType() == TokenTypes.METHOD_DEF) { 137 quotesAreNotPreceded = !quotesArePrecededWithComma(openingQuotes); 138 } 139 else if (parent.getType() == TokenTypes.QUESTION 140 && openingQuotes.getPreviousSibling() != null) { 141 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, 142 openingQuotes.getPreviousSibling()); 143 } 144 else { 145 quotesAreNotPreceded = !TokenUtil.areOnSameLine(openingQuotes, parent); 146 } 147 148 if (TokenUtil.isOfType(parent.getType(), 149 TokenTypes.LITERAL_RETURN, 150 TokenTypes.VARIABLE_DEF, 151 TokenTypes.METHOD_DEF, 152 TokenTypes.CTOR_DEF, 153 TokenTypes.ENUM_DEF)) { 154 break; 155 } 156 } 157 return quotesAreNotPreceded; 158 } 159 160 /** 161 * Determines if opening quotes are preceded by {@code ,}. 162 * 163 * @param openingQuotes the quotes 164 * @return true if {@code ,} is present before opening quotes. 165 */ 166 private static boolean quotesArePrecededWithComma(DetailAST openingQuotes) { 167 final DetailAST expression = openingQuotes.getParent(); 168 return expression.getPreviousSibling() != null 169 && TokenUtil.areOnSameLine(openingQuotes, expression.getPreviousSibling()); 170 } 171 172 /** 173 * Determines if the Closing quotes of text block are not preceded by any code. 174 * 175 * @param closingQuotes closing quotes 176 * @return true if the closing quotes are on the new line. 177 */ 178 private static boolean closingQuotesAreAloneOnTheLine(DetailAST closingQuotes) { 179 final DetailAST content = closingQuotes.getPreviousSibling(); 180 final String text = content.getText(); 181 int index = text.length() - 1; 182 while (text.charAt(index) == ' ') { 183 index--; 184 } 185 return Character.isWhitespace(text.charAt(index)); 186 } 187}