1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.utils;
21
22 import java.util.Collection;
23 import java.util.Iterator;
24 import java.util.List;
25
26 /**
27 * Utility class for inline configuration parsing shared between
28 * ExampleMacro and InlineConfigParser.
29 *
30 * <p>Supports multiple config-delimiter conventions so that legacy examples using the
31 * historical {@code /*xml} Java-comment style keep working regardless of target file
32 * extension, while newer examples for non-Java targets (.xml, .properties) that need a
33 * genuinely valid comment in their own file format can opt into a type-appropriate
34 * delimiter instead.
35 */
36 public final class InlineConfigUtils {
37
38 /** Legacy/default config comment prefix — a loose Java block comment. */
39 public static final String JAVA_CONFIG_PREFIX = "/*";
40
41 /** Legacy/default config start delimiter for XML-style (module) config. */
42 public static final String JAVA_XML_CONFIG_START = "/*xml";
43
44 /** Legacy/default config end delimiter. */
45 public static final String JAVA_CONFIG_END = "*/";
46
47 /** Config start delimiter for XML target files that need well-formed XML content. */
48 public static final String XML_TARGET_CONFIG_START = "<!--xml";
49
50 /** Config end delimiter for XML target files. */
51 public static final String XML_TARGET_CONFIG_END = "-->";
52
53 /** Comment prefix for .properties target files. */
54 public static final String PROPERTIES_COMMENT_PREFIX = "#";
55
56 /** File extension for XML files. */
57 private static final String XML_FILE_EXTENSION = ".xml";
58
59 /** File extension for properties files. */
60 private static final String PROPERTIES_FILE_EXTENSION = ".properties";
61
62 /** Separator for delimiter descriptions. */
63 private static final String DELIMITER_SEPARATOR = ", or \"";
64
65 /** Prevent instantiation. */
66 private InlineConfigUtils() {
67 }
68
69 /**
70 * Matches the first line of a file against every delimiter convention valid for that
71 * file's type, and returns the match, or {@code null} if none apply.
72 *
73 * <p>For {@code .properties} files there is no explicit start/end marker pair: any
74 * leading line starting with {@code #} is treated as the start of the config block,
75 * and the block runs until the first blank line (see {@link #getConfigEndIndex}).
76 *
77 * @param lines the lines of the file.
78 * @param filePath the file path, used to decide which non-Java conventions are valid.
79 * @return the matched delimiter, or {@code null} if the first line matches nothing.
80 */
81 public static MatchedDelimiter matchDelimiter(List<String> lines, String filePath) {
82 MatchedDelimiter result = null;
83 if (!lines.isEmpty()) {
84 final String first = lines.getFirst();
85 if (first.startsWith(JAVA_CONFIG_PREFIX)) {
86 result = new MatchedDelimiter(JAVA_CONFIG_END, JAVA_XML_CONFIG_START.equals(first));
87 }
88 else if (filePath.endsWith(XML_FILE_EXTENSION)
89 && XML_TARGET_CONFIG_START.equals(first)) {
90 result = new MatchedDelimiter(XML_TARGET_CONFIG_END, true);
91 }
92 else if (filePath.endsWith(PROPERTIES_FILE_EXTENSION)
93 && first.startsWith(PROPERTIES_COMMENT_PREFIX)) {
94 // No explicit end marker: the leading "#" comment block, up to the
95 // first blank line, IS the config.
96 result = new MatchedDelimiter(null, true);
97 }
98 }
99 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 }