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