View Javadoc
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 }