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.api;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.grammar.CommentListener;
031import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
032import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
033
034/**
035 * Represents the contents of a file.
036 *
037 */
038public final class FileContents implements CommentListener {
039
040    /**
041     * The pattern to match a single-line comment containing only the comment itself -- no code.
042     */
043    private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
044    /** Compiled regexp to match a single-line comment line. */
045    private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
046            .compile(MATCH_SINGLELINE_COMMENT_PAT);
047
048    /** The text. */
049    private final FileText text;
050
051    /**
052     * Map of the Javadoc comments indexed on the last line of the comment.
053     * The hack is it assumes that there is only one Javadoc comment per line.
054     */
055    private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
056    /** Map of the C++ comments indexed on the first line of the comment. */
057    private final Map<Integer, TextBlock> cppComments = new HashMap<>();
058
059    /**
060     * Map of the C comments indexed on the first line of the comment to a list
061     * of comments on that line.
062     */
063    private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
064
065    /**
066     * Creates a new {@code FileContents} instance.
067     *
068     * @param text the contents of the file
069     */
070    public FileContents(FileText text) {
071        this.text = new FileText(text);
072    }
073
074    /**
075     * Get the full text of the file.
076     *
077     * @return an object containing the full text of the file
078     */
079    public FileText getText() {
080        return new FileText(text);
081    }
082
083    /**
084     * Gets the lines in the file.
085     *
086     * @return the lines in the file
087     */
088    public String[] getLines() {
089        return text.toLinesArray();
090    }
091
092    /**
093     * Get the line from text of the file.
094     *
095     * @param index index of the line
096     * @return line from text of the file
097     */
098    public String getLine(int index) {
099        return text.get(index);
100    }
101
102    /**
103     * Gets the name of the file.
104     *
105     * @return the name of the file
106     */
107    public String getFileName() {
108        return text.getFile().toString();
109    }
110
111    /**
112     * Report the location of a single-line comment.
113     *
114     * @param startLineNo the starting line number
115     * @param startColNo the starting column number
116     */
117    public void reportSingleLineComment(int startLineNo, int startColNo) {
118        final String line = line(startLineNo - 1);
119        final String[] txt = {line.substring(startColNo)};
120        final Comment comment = new Comment(txt, startColNo, startLineNo,
121                line.length() - 1);
122        cppComments.put(startLineNo, comment);
123    }
124
125    @Override
126    public void reportSingleLineComment(String type, int startLineNo,
127            int startColNo) {
128        reportSingleLineComment(startLineNo, startColNo);
129    }
130
131    /**
132     * Report the location of a block comment.
133     *
134     * @param startLineNo the starting line number
135     * @param startColNo the starting column number
136     * @param endLineNo the ending line number
137     * @param endColNo the ending column number
138     */
139    public void reportBlockComment(int startLineNo, int startColNo,
140            int endLineNo, int endColNo) {
141        final String[] cComment = extractBlockComment(startLineNo, startColNo,
142                endLineNo, endColNo);
143        final Comment comment = new Comment(cComment, startColNo, endLineNo,
144                endColNo);
145
146        // save the comment
147        final List<TextBlock> entries = clangComments.computeIfAbsent(startLineNo,
148                empty -> new ArrayList<>());
149
150        entries.add(comment);
151
152        // Remember if possible Javadoc comment
153        final String firstLine = line(startLineNo - 1);
154        if (firstLine.contains("/**") && !firstLine.contains("/**/")) {
155            javadocComments.put(endLineNo - 1, comment);
156        }
157    }
158
159    @Override
160    public void reportBlockComment(String type, int startLineNo,
161            int startColNo, int endLineNo, int endColNo) {
162        reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
163    }
164
165    /**
166     * Returns the specified block comment as a String array.
167     *
168     * @param startLineNo the starting line number
169     * @param startColNo the starting column number
170     * @param endLineNo the ending line number
171     * @param endColNo the ending column number
172     * @return block comment as an array
173     */
174    private String[] extractBlockComment(int startLineNo, int startColNo,
175            int endLineNo, int endColNo) {
176        final String[] returnValue;
177        if (startLineNo == endLineNo) {
178            returnValue = new String[1];
179            returnValue[0] = line(startLineNo - 1).substring(startColNo,
180                    endColNo + 1);
181        }
182        else {
183            returnValue = new String[endLineNo - startLineNo + 1];
184            returnValue[0] = line(startLineNo - 1).substring(startColNo);
185            for (int index = startLineNo; index < endLineNo; index++) {
186                returnValue[index - startLineNo + 1] = line(index);
187            }
188            returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0,
189                    endColNo + 1);
190        }
191        return returnValue;
192    }
193
194    /**
195     * Get a single-line.
196     * For internal use only, as getText().get(lineNo) is just as
197     * suitable for external use and avoids method duplication.
198     *
199     * @param lineNo the number of the line to get
200     * @return the corresponding line, without terminator
201     * @throws IndexOutOfBoundsException if lineNo is invalid
202     */
203    private String line(int lineNo) {
204        return text.get(lineNo);
205    }
206
207    /**
208     * Returns the Javadoc comment before the specified line.
209     * A return value of {@code null} means there is no such comment.
210     *
211     * @param lineNoBefore the line number to check before
212     * @return the Javadoc comment, or {@code null} if none
213     * @deprecated this method supports legacy checks that inspect Javadoc comments from
214     *             {@code FileContents}; use
215     *             {@link JavadocUtil#getAttachedJavadocComment(DetailAST)} with AST-based
216     *             Javadoc processing instead.
217     * @noinspection DeprecatedIsStillUsed
218     * @noinspectionreason DeprecatedIsStillUsed - Method used in unit testing to verify
219     *             legacy API behavior.
220     */
221    @Deprecated(since = "13.9.0")
222    public TextBlock getJavadocBefore(int lineNoBefore) {
223        // Lines start at 1 to the callers perspective, so need to take off 2
224        int lineNo = lineNoBefore - 2;
225
226        // skip blank lines and comments
227        while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo)
228                            || lineInsideBlockComment(lineNo + 1))) {
229            lineNo--;
230        }
231
232        return javadocComments.get(lineNo);
233    }
234
235    /**
236     * Checks if the specified line number is inside a block comment.
237     * This method scans through all block comments (excluding Javadoc comments)
238     * and determines whether the given line number falls within any of them
239     *
240     * @param lineNo the line number to check
241     * @return {@code true} if the line is inside a block comment (excluding Javadoc comments)
242     *          , {@code false} otherwise
243     */
244    private boolean lineInsideBlockComment(int lineNo) {
245        final Collection<List<TextBlock>> values = clangComments.values();
246        return values.stream()
247            .flatMap(List::stream)
248            .filter(comment -> !javadocComments.containsValue(comment))
249            .anyMatch(comment -> isLineBlockComment(lineNo, comment));
250    }
251
252    /**
253     * Checks if the given line is inside a block comment
254     * and both the start and end lines contain only the comment.
255     *
256     * @param lineNo the line number to check
257     * @param comment the block comment to inspect
258     * @return {@code true} line is in block comment, {@code false} otherwise
259     */
260    private boolean isLineBlockComment(int lineNo, TextBlock comment) {
261        final boolean lineInSideBlockComment = lineNo >= comment.getStartLineNo()
262                && lineNo <= comment.getEndLineNo();
263        boolean lineHasOnlyBlockComment = true;
264        final String startLine = line(comment.getStartLineNo() - 1).trim();
265        if (!startLine.startsWith("/*")) {
266            lineHasOnlyBlockComment = false;
267        }
268
269        final String endLine = line(comment.getEndLineNo() - 1).trim();
270        if (!endLine.endsWith("*/")) {
271            lineHasOnlyBlockComment = false;
272        }
273        return lineInSideBlockComment && lineHasOnlyBlockComment;
274    }
275
276    /**
277     * Checks if the specified line is blank.
278     *
279     * @param lineNo the line number to check
280     * @return if the specified line consists only of tabs and spaces.
281     */
282    public boolean lineIsBlank(int lineNo) {
283        return line(lineNo).isBlank();
284    }
285
286    /**
287     * Checks if the specified line is a single-line comment without code.
288     *
289     * @param lineNo  the line number to check
290     * @return if the specified line consists of only a single-line comment without code.
291     */
292    public boolean lineIsComment(int lineNo) {
293        return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
294    }
295
296    /**
297     * Checks if the specified position intersects with a comment.
298     *
299     * @param startLineNo the starting line number
300     * @param startColNo the starting column number
301     * @param endLineNo the ending line number
302     * @param endColNo the ending column number
303     * @return true if the positions intersects with a comment.
304     */
305    public boolean hasIntersectionWithComment(int startLineNo,
306            int startColNo, int endLineNo, int endColNo) {
307        return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
308                || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
309                        endColNo);
310    }
311
312    /**
313     * Checks if the specified position intersects with a block comment.
314     *
315     * @param startLineNo the starting line number
316     * @param startColNo the starting column number
317     * @param endLineNo the ending line number
318     * @param endColNo the ending column number
319     * @return true if the positions intersects with a block comment.
320     */
321    private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
322            int endLineNo, int endColNo) {
323        // Check C comments (all comments should be checked)
324        final Collection<List<TextBlock>> values = clangComments.values();
325        return values.stream()
326            .flatMap(List::stream)
327            .anyMatch(comment -> comment.intersects(startLineNo, startColNo, endLineNo, endColNo));
328    }
329
330    /**
331     * Checks if the specified position intersects with a single-line comment.
332     *
333     * @param startLineNo the starting line number
334     * @param startColNo the starting column number
335     * @param endLineNo the ending line number
336     * @param endColNo the ending column number
337     * @return true if the positions intersects with a single-line comment.
338     */
339    private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
340            int endLineNo, int endColNo) {
341        boolean hasIntersection = false;
342        // Check CPP comments (line searching is possible)
343        for (int lineNumber = startLineNo; lineNumber <= endLineNo;
344             lineNumber++) {
345            final TextBlock comment = cppComments.get(lineNumber);
346            if (comment != null && comment.intersects(startLineNo, startColNo,
347                    endLineNo, endColNo)) {
348                hasIntersection = true;
349                break;
350            }
351        }
352        return hasIntersection;
353    }
354
355    /**
356     * Returns a map of all the single-line comments. The key is a line number,
357     * the value is the comment {@link TextBlock} at the line.
358     *
359     * @return the Map of comments
360     */
361    public Map<Integer, TextBlock> getSingleLineComments() {
362        return Collections.unmodifiableMap(cppComments);
363    }
364
365    /**
366     * Returns a map of all block comments. The key is the line number, the
367     * value is a {@link List} of block comment {@link TextBlock}s that start at that line.
368     *
369     * @return the map of comments
370     */
371    public Map<Integer, List<TextBlock>> getBlockComments() {
372        return Collections.unmodifiableMap(clangComments);
373    }
374
375    /**
376     * Checks if the current file is a package-info.java file.
377     *
378     * @return true if the package file.
379     * @deprecated use {@link CheckUtil#isPackageInfo(String)} for the same functionality,
380     *              or use {@link AbstractCheck#getFilePath()} to process your own standards.
381     */
382    @Deprecated(since = "10.2")
383    public boolean inPackageInfo() {
384        return "package-info.java".equals(text.getFile().getName());
385    }
386
387}