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}