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 }