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}