View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2026 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  import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
34  
35  /**
36   * Represents the contents of a file.
37   *
38   */
39  public final class FileContents implements CommentListener {
40  
41      /**
42       * The pattern to match a single-line comment containing only the comment
43       * itself -- no code.
44       */
45      private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
46      /** Compiled regexp to match a single-line comment line. */
47      private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
48              .compile(MATCH_SINGLELINE_COMMENT_PAT);
49  
50      /** The text. */
51      private final FileText text;
52  
53      /**
54       * Map of the Javadoc comments indexed on the last line of the comment.
55       * The hack is it assumes that there is only one Javadoc comment per line.
56       */
57      private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
58      /** Map of the C++ comments indexed on the first line of the comment. */
59      private final Map<Integer, TextBlock> cppComments = new HashMap<>();
60  
61      /**
62       * Map of the C comments indexed on the first line of the comment to a list
63       * of comments on that line.
64       */
65      private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
66  
67      /**
68       * Creates a new {@code FileContents} instance.
69       *
70       * @param text the contents of the file
71       */
72      public FileContents(FileText text) {
73          this.text = new FileText(text);
74      }
75  
76      /**
77       * Get the full text of the file.
78       *
79       * @return an object containing the full text of the file
80       */
81      public FileText getText() {
82          return new FileText(text);
83      }
84  
85      /**
86       * Gets the lines in the file.
87       *
88       * @return the lines in the file
89       */
90      public String[] getLines() {
91          return text.toLinesArray();
92      }
93  
94      /**
95       * Get the line from text of the file.
96       *
97       * @param index index of the line
98       * @return line from text of the file
99       */
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 }