001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.api;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.List;
027import java.util.Map;
028import java.util.regex.Pattern;
029
030import com.puppycrawl.tools.checkstyle.grammar.CommentListener;
031import com.puppycrawl.tools.checkstyle.utils.CheckUtil;
032import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
033
034/**
035 * Represents the contents of a file.
036 *
037 */
038public final class FileContents implements CommentListener {
039
040 /**
041 * The pattern to match a single-line comment containing only the comment
042 * itself -- no code.
043 */
044 private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
045 /** Compiled regexp to match a single-line comment line. */
046 private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
047 .compile(MATCH_SINGLELINE_COMMENT_PAT);
048
049 /** The text. */
050 private final FileText text;
051
052 /**
053 * Map of the Javadoc comments indexed on the last line of the comment.
054 * The hack is it assumes that there is only one Javadoc comment per line.
055 */
056 private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
057 /** Map of the C++ comments indexed on the first line of the comment. */
058 private final Map<Integer, TextBlock> cppComments = new HashMap<>();
059
060 /**
061 * Map of the C comments indexed on the first line of the comment to a list
062 * of comments on that line.
063 */
064 private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
065
066 /**
067 * Creates a new {@code FileContents} instance.
068 *
069 * @param text the contents of the file
070 */
071 public FileContents(FileText text) {
072 this.text = new FileText(text);
073 }
074
075 /**
076 * Get the full text of the file.
077 *
078 * @return an object containing the full text of the file
079 */
080 public FileText getText() {
081 return new FileText(text);
082 }
083
084 /**
085 * Gets the lines in the file.
086 *
087 * @return the lines in the file
088 */
089 public String[] getLines() {
090 return text.toLinesArray();
091 }
092
093 /**
094 * Get the line from text of the file.
095 *
096 * @param index index of the line
097 * @return line from text of the file
098 */
099 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}