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 }