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