001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2026 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; 033import com.puppycrawl.tools.checkstyle.utils.JavadocUtil; 034 035/** 036 * Represents the contents of a file. 037 * 038 */ 039public final class FileContents implements CommentListener { 040 041 /** 042 * The pattern to match a single-line comment containing only the comment 043 * itself -- no code. 044 */ 045 private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$"; 046 /** Compiled regexp to match a single-line comment line. */ 047 private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern 048 .compile(MATCH_SINGLELINE_COMMENT_PAT); 049 050 /** The text. */ 051 private final FileText text; 052 053 /** 054 * Map of the Javadoc comments indexed on the last line of the comment. 055 * The hack is it assumes that there is only one Javadoc comment per line. 056 */ 057 private final Map<Integer, TextBlock> javadocComments = new HashMap<>(); 058 /** Map of the C++ comments indexed on the first line of the comment. */ 059 private final Map<Integer, TextBlock> cppComments = new HashMap<>(); 060 061 /** 062 * Map of the C comments indexed on the first line of the comment to a list 063 * of comments on that line. 064 */ 065 private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>(); 066 067 /** 068 * Creates a new {@code FileContents} instance. 069 * 070 * @param text the contents of the file 071 */ 072 public FileContents(FileText text) { 073 this.text = new FileText(text); 074 } 075 076 /** 077 * Get the full text of the file. 078 * 079 * @return an object containing the full text of the file 080 */ 081 public FileText getText() { 082 return new FileText(text); 083 } 084 085 /** 086 * Gets the lines in the file. 087 * 088 * @return the lines in the file 089 */ 090 public String[] getLines() { 091 return text.toLinesArray(); 092 } 093 094 /** 095 * Get the line from text of the file. 096 * 097 * @param index index of the line 098 * @return line from text of the file 099 */ 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}