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.utils; 021 022import java.util.Collection; 023import java.util.Iterator; 024import java.util.List; 025 026/** 027 * Utility class for inline configuration parsing shared between 028 * ExampleMacro and InlineConfigParser. 029 * 030 * <p>Supports multiple config-delimiter conventions so that legacy examples using the 031 * historical {@code /*xml} Java-comment style keep working regardless of target file 032 * extension, while newer examples for non-Java targets (.xml, .properties) that need a 033 * genuinely valid comment in their own file format can opt into a type-appropriate 034 * delimiter instead. 035 */ 036public final class InlineConfigUtils { 037 038 /** Legacy/default config comment prefix — a loose Java block comment. */ 039 public static final String JAVA_CONFIG_PREFIX = "/*"; 040 041 /** Legacy/default config start delimiter for XML-style (module) config. */ 042 public static final String JAVA_XML_CONFIG_START = "/*xml"; 043 044 /** Legacy/default config end delimiter. */ 045 public static final String JAVA_CONFIG_END = "*/"; 046 047 /** Config start delimiter for XML target files that need well-formed XML content. */ 048 public static final String XML_TARGET_CONFIG_START = "<!--xml"; 049 050 /** Config end delimiter for XML target files. */ 051 public static final String XML_TARGET_CONFIG_END = "-->"; 052 053 /** Comment prefix for .properties target files. */ 054 public static final String PROPERTIES_COMMENT_PREFIX = "#"; 055 056 /** File extension for XML files. */ 057 private static final String XML_FILE_EXTENSION = ".xml"; 058 059 /** File extension for properties files. */ 060 private static final String PROPERTIES_FILE_EXTENSION = ".properties"; 061 062 /** Separator for delimiter descriptions. */ 063 private static final String DELIMITER_SEPARATOR = ", or \""; 064 065 /** Prevent instantiation. */ 066 private InlineConfigUtils() { 067 } 068 069 /** 070 * Matches the first line of a file against every delimiter convention valid for that 071 * file's type, and returns the match, or {@code null} if none apply. 072 * 073 * <p>For {@code .properties} files there is no explicit start/end marker pair: any 074 * leading line starting with {@code #} is treated as the start of the config block, 075 * and the block runs until the first blank line (see {@link #getConfigEndIndex}). 076 * 077 * @param lines the lines of the file. 078 * @param filePath the file path, used to decide which non-Java conventions are valid. 079 * @return the matched delimiter, or {@code null} if the first line matches nothing. 080 */ 081 public static MatchedDelimiter matchDelimiter(List<String> lines, String filePath) { 082 MatchedDelimiter result = null; 083 if (!lines.isEmpty()) { 084 final String first = lines.getFirst(); 085 if (first.startsWith(JAVA_CONFIG_PREFIX)) { 086 result = new MatchedDelimiter(JAVA_CONFIG_END, JAVA_XML_CONFIG_START.equals(first)); 087 } 088 else if (filePath.endsWith(XML_FILE_EXTENSION) 089 && XML_TARGET_CONFIG_START.equals(first)) { 090 result = new MatchedDelimiter(XML_TARGET_CONFIG_END, true); 091 } 092 else if (filePath.endsWith(PROPERTIES_FILE_EXTENSION) 093 && first.startsWith(PROPERTIES_COMMENT_PREFIX)) { 094 // No explicit end marker: the leading "#" comment block, up to the 095 // first blank line, IS the config. 096 result = new MatchedDelimiter(null, true); 097 } 098 } 099 return result; 100 } 101 102 /** 103 * Finds the index of the line where a matched config block ends. For delimiter-based 104 * matches, this is the first line starting with the end delimiter. For properties-style 105 * matches (no explicit end delimiter), this is the first blank line after the start, 106 * or the end of the file if there is no blank line. 107 * 108 * @param lines the lines of the file, including the start line at index 0. 109 * @param matched the matched delimiter describing how to find the end. 110 * @return the index of the line where the config block ends (exclusive), or -1 if a 111 * delimiter-based end could not be found. 112 */ 113 public static int getConfigEndIndex(Iterable<String> lines, MatchedDelimiter matched) { 114 final int result; 115 if (matched.end() == null) { 116 int index = 0; 117 final Iterator<String> iterator = lines.iterator(); 118 while (iterator.hasNext() && !iterator.next().isEmpty()) { 119 index++; 120 } 121 result = index; 122 } 123 else { 124 result = indexOfStartingWith(lines, matched.end()); 125 } 126 return result; 127 } 128 129 /** 130 * Finds the index of the first line that starts with the given prefix. 131 * 132 * @param lines the lines to search. 133 * @param prefix the prefix to search for. 134 * @return the index of the first matching line, or -1 if not found. 135 */ 136 private static int indexOfStartingWith(Iterable<String> lines, String prefix) { 137 int result = -1; 138 int index = 0; 139 for (String line : lines) { 140 if (line.startsWith(prefix)) { 141 result = index; 142 break; 143 } 144 index++; 145 } 146 return result; 147 } 148 149 /** 150 * Builds a human-readable description of every delimiter convention valid for the 151 * given file's type, for use in error messages. 152 * 153 * @param filePath the file path. 154 * @return a description of valid start delimiters for this file type. 155 */ 156 public static String describeExpectedDelimiters(String filePath) { 157 final StringBuilder builder = new StringBuilder(160); 158 builder.append("\"/*xml\" or \"/*\" (Java-comment style)"); 159 if (filePath.endsWith(XML_FILE_EXTENSION)) { 160 builder.append(DELIMITER_SEPARATOR).append(XML_TARGET_CONFIG_START) 161 .append("\" (XML-comment style)"); 162 } 163 if (filePath.endsWith(PROPERTIES_FILE_EXTENSION)) { 164 builder.append(DELIMITER_SEPARATOR) 165 .append(PROPERTIES_COMMENT_PREFIX.charAt(0)) 166 .append("\" leading comment block, ending at the first blank line"); 167 } 168 return builder.toString(); 169 } 170 171 /** 172 * Strips the leading {@code #} comment marker from each line in a properties-style 173 * config block. Used because config lines in {@code .properties} files must themselves 174 * be valid properties-file comments. 175 * 176 * @param lines the lines to process. 177 * @return the lines with a leading {@code #} removed where present. 178 */ 179 public static List<String> stripPropertiesCommentPrefix(Collection<String> lines) { 180 return lines.stream() 181 .map(InlineConfigUtils::stripLeadingHash) 182 .toList(); 183 } 184 185 /** 186 * Strips a leading hash character from the given line if present. 187 * 188 * @param line the line to process. 189 * @return the line with leading hash removed, or the original line if no hash. 190 */ 191 private static String stripLeadingHash(String line) { 192 String result = line; 193 if (line.startsWith(PROPERTIES_COMMENT_PREFIX)) { 194 result = line.substring(1); 195 } 196 return result; 197 } 198 199 /** 200 * Describes which delimiter convention matched the first line of a file, so callers 201 * can locate the end of the config block and know whether the config content should 202 * be parsed as XML module config or the legacy bare key=value format. 203 * 204 * @param end the end delimiter to search for, or {@code null} if the config block 205 * ends implicitly at the first blank line (properties-style). 206 * @param xmlStyleConfig true if the config content is XML ({@code <module>} form), 207 * false if it is the legacy bare key=value form. 208 */ 209 public record MatchedDelimiter( 210 String end, 211 boolean xmlStyleConfig 212 ) { 213 } 214 215}