View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 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.api;
21  
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashMap;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.regex.Pattern;
29  
30  import com.puppycrawl.tools.checkstyle.grammar.CommentListener;
31  import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
32  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
33  
34  /**
35   * Represents the contents of a file.
36   *
37   */
38  public final class FileContents implements CommentListener {
39  
40      /**
41       * The pattern to match a single-line comment containing only the comment
42       * itself -- no code.
43       */
44      private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
45      /** Compiled regexp to match a single-line comment line. */
46      private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
47              .compile(MATCH_SINGLELINE_COMMENT_PAT);
48  
49      /** The text. */
50      private final FileText text;
51  
52      /**
53       * Map of the Javadoc comments indexed on the last line of the comment.
54       * The hack is it assumes that there is only one Javadoc comment per line.
55       */
56      private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
57      /** Map of the C++ comments indexed on the first line of the comment. */
58      private final Map<Integer, TextBlock> cppComments = new HashMap<>();
59  
60      /**
61       * Map of the C comments indexed on the first line of the comment to a list
62       * of comments on that line.
63       */
64      private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
65  
66      /**
67       * Creates a new {@code FileContents} instance.
68       *
69       * @param text the contents of the file
70       */
71      public FileContents(FileText text) {
72          this.text = new FileText(text);
73      }
74  
75      /**
76       * Get the full text of the file.
77       *
78       * @return an object containing the full text of the file
79       */
80      public FileText getText() {
81          return new FileText(text);
82      }
83  
84      /**
85       * Gets the lines in the file.
86       *
87       * @return the lines in the file
88       */
89      public String[] getLines() {
90          return text.toLinesArray();
91      }
92  
93      /**
94       * Get the line from text of the file.
95       *
96       * @param index index of the line
97       * @return line from text of the file
98       */
99      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 -> isLineBlockComment(lineNo, comment));
243     }
244 
245     /**
246      * Checks if the given line is inside a block comment
247      * and both the start and end lines contain only the comment.
248      *
249      * @param lineNo the line number to check
250      * @param comment the block comment to inspect
251      * @return {@code true} line is in block comment, {@code false} otherwise
252      */
253     private boolean isLineBlockComment(int lineNo, TextBlock comment) {
254         final boolean lineInSideBlockComment = lineNo >= comment.getStartLineNo()
255                 && lineNo <= comment.getEndLineNo();
256         boolean lineHasOnlyBlockComment = true;
257         final String startLine = line(comment.getStartLineNo() - 1).trim();
258         if (!startLine.startsWith("/*")) {
259             lineHasOnlyBlockComment = false;
260         }
261 
262         final String endLine = line(comment.getEndLineNo() - 1).trim();
263         if (!endLine.endsWith("*/")) {
264             lineHasOnlyBlockComment = false;
265         }
266         return lineInSideBlockComment && lineHasOnlyBlockComment;
267     }
268 
269     /**
270      * Checks if the specified line is blank.
271      *
272      * @param lineNo the line number to check
273      * @return if the specified line consists only of tabs and spaces.
274      **/
275     public boolean lineIsBlank(int lineNo) {
276         return CommonUtil.isBlank(line(lineNo));
277     }
278 
279     /**
280      * Checks if the specified line is a single-line comment without code.
281      *
282      * @param lineNo  the line number to check
283      * @return if the specified line consists of only a single-line comment
284      *         without code.
285      **/
286     public boolean lineIsComment(int lineNo) {
287         return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
288     }
289 
290     /**
291      * Checks if the specified position intersects with a comment.
292      *
293      * @param startLineNo the starting line number
294      * @param startColNo the starting column number
295      * @param endLineNo the ending line number
296      * @param endColNo the ending column number
297      * @return true if the positions intersects with a comment.
298      **/
299     public boolean hasIntersectionWithComment(int startLineNo,
300             int startColNo, int endLineNo, int endColNo) {
301         return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
302                 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
303                         endColNo);
304     }
305 
306     /**
307      * Checks if the specified position intersects with a block comment.
308      *
309      * @param startLineNo the starting line number
310      * @param startColNo the starting column number
311      * @param endLineNo the ending line number
312      * @param endColNo the ending column number
313      * @return true if the positions intersects with a block comment.
314      */
315     private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
316             int endLineNo, int endColNo) {
317         // Check C comments (all comments should be checked)
318         final Collection<List<TextBlock>> values = clangComments.values();
319         return values.stream()
320             .flatMap(List::stream)
321             .anyMatch(comment -> comment.intersects(startLineNo, startColNo, endLineNo, endColNo));
322     }
323 
324     /**
325      * Checks if the specified position intersects with a single-line comment.
326      *
327      * @param startLineNo the starting line number
328      * @param startColNo the starting column number
329      * @param endLineNo the ending line number
330      * @param endColNo the ending column number
331      * @return true if the positions intersects with a single-line comment.
332      */
333     private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
334             int endLineNo, int endColNo) {
335         boolean hasIntersection = false;
336         // Check CPP comments (line searching is possible)
337         for (int lineNumber = startLineNo; lineNumber <= endLineNo;
338              lineNumber++) {
339             final TextBlock comment = cppComments.get(lineNumber);
340             if (comment != null && comment.intersects(startLineNo, startColNo,
341                     endLineNo, endColNo)) {
342                 hasIntersection = true;
343                 break;
344             }
345         }
346         return hasIntersection;
347     }
348 
349     /**
350      * Returns a map of all the single-line comments. The key is a line number,
351      * the value is the comment {@link TextBlock} at the line.
352      *
353      * @return the Map of comments
354      */
355     public Map<Integer, TextBlock> getSingleLineComments() {
356         return Collections.unmodifiableMap(cppComments);
357     }
358 
359     /**
360      * Returns a map of all block comments. The key is the line number, the
361      * value is a {@link List} of block comment {@link TextBlock}s
362      * that start at that line.
363      *
364      * @return the map of comments
365      */
366     public Map<Integer, List<TextBlock>> getBlockComments() {
367         return Collections.unmodifiableMap(clangComments);
368     }
369 
370     /**
371      * Checks if the current file is a package-info.java file.
372      *
373      * @return true if the package file.
374      * @deprecated use {@link CheckUtil#isPackageInfo(String)} for the same functionality,
375      *              or use {@link AbstractCheck#getFilePath()} to process your own standards.
376      */
377     @Deprecated(since = "10.2")
378     public boolean inPackageInfo() {
379         return "package-info.java".equals(text.getFile().getName());
380     }
381 }