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 }