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