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.JavadocUtil; 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 itself -- no code. 042 */ 043 private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$"; 044 /** Compiled regexp to match a single-line comment line. */ 045 private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern 046 .compile(MATCH_SINGLELINE_COMMENT_PAT); 047 048 /** The text. */ 049 private final FileText text; 050 051 /** 052 * Map of the Javadoc comments indexed on the last line of the comment. 053 * The hack is it assumes that there is only one Javadoc comment per line. 054 */ 055 private final Map<Integer, TextBlock> javadocComments = new HashMap<>(); 056 /** Map of the C++ comments indexed on the first line of the comment. */ 057 private final Map<Integer, TextBlock> cppComments = new HashMap<>(); 058 059 /** 060 * Map of the C comments indexed on the first line of the comment to a list 061 * of comments on that line. 062 */ 063 private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>(); 064 065 /** 066 * Creates a new {@code FileContents} instance. 067 * 068 * @param text the contents of the file 069 */ 070 public FileContents(FileText text) { 071 this.text = new FileText(text); 072 } 073 074 /** 075 * Get the full text of the file. 076 * 077 * @return an object containing the full text of the file 078 */ 079 public FileText getText() { 080 return new FileText(text); 081 } 082 083 /** 084 * Gets the lines in the file. 085 * 086 * @return the lines in the file 087 */ 088 public String[] getLines() { 089 return text.toLinesArray(); 090 } 091 092 /** 093 * Get the line from text of the file. 094 * 095 * @param index index of the line 096 * @return line from text of the file 097 */ 098 public String getLine(int index) { 099 return text.get(index); 100 } 101 102 /** 103 * Gets the name of the file. 104 * 105 * @return the name of the file 106 */ 107 public String getFileName() { 108 return text.getFile().toString(); 109 } 110 111 /** 112 * Report the location of a single-line comment. 113 * 114 * @param startLineNo the starting line number 115 * @param startColNo the starting column number 116 */ 117 public void reportSingleLineComment(int startLineNo, int startColNo) { 118 final String line = line(startLineNo - 1); 119 final String[] txt = {line.substring(startColNo)}; 120 final Comment comment = new Comment(txt, startColNo, startLineNo, 121 line.length() - 1); 122 cppComments.put(startLineNo, comment); 123 } 124 125 @Override 126 public void reportSingleLineComment(String type, int startLineNo, 127 int startColNo) { 128 reportSingleLineComment(startLineNo, startColNo); 129 } 130 131 /** 132 * Report the location of a block comment. 133 * 134 * @param startLineNo the starting line number 135 * @param startColNo the starting column number 136 * @param endLineNo the ending line number 137 * @param endColNo the ending column number 138 */ 139 public void reportBlockComment(int startLineNo, int startColNo, 140 int endLineNo, int endColNo) { 141 final String[] cComment = extractBlockComment(startLineNo, startColNo, 142 endLineNo, endColNo); 143 final Comment comment = new Comment(cComment, startColNo, endLineNo, 144 endColNo); 145 146 // save the comment 147 final List<TextBlock> entries = clangComments.computeIfAbsent(startLineNo, 148 empty -> new ArrayList<>()); 149 150 entries.add(comment); 151 152 // Remember if possible Javadoc comment 153 final String firstLine = line(startLineNo - 1); 154 if (firstLine.contains("/**") && !firstLine.contains("/**/")) { 155 javadocComments.put(endLineNo - 1, comment); 156 } 157 } 158 159 @Override 160 public void reportBlockComment(String type, int startLineNo, 161 int startColNo, int endLineNo, int endColNo) { 162 reportBlockComment(startLineNo, startColNo, endLineNo, endColNo); 163 } 164 165 /** 166 * Returns the specified block comment as a String array. 167 * 168 * @param startLineNo the starting line number 169 * @param startColNo the starting column number 170 * @param endLineNo the ending line number 171 * @param endColNo the ending column number 172 * @return block comment as an array 173 */ 174 private String[] extractBlockComment(int startLineNo, int startColNo, 175 int endLineNo, int endColNo) { 176 final String[] returnValue; 177 if (startLineNo == endLineNo) { 178 returnValue = new String[1]; 179 returnValue[0] = line(startLineNo - 1).substring(startColNo, 180 endColNo + 1); 181 } 182 else { 183 returnValue = new String[endLineNo - startLineNo + 1]; 184 returnValue[0] = line(startLineNo - 1).substring(startColNo); 185 for (int index = startLineNo; index < endLineNo; index++) { 186 returnValue[index - startLineNo + 1] = line(index); 187 } 188 returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0, 189 endColNo + 1); 190 } 191 return returnValue; 192 } 193 194 /** 195 * Get a single-line. 196 * For internal use only, as getText().get(lineNo) is just as 197 * suitable for external use and avoids method duplication. 198 * 199 * @param lineNo the number of the line to get 200 * @return the corresponding line, without terminator 201 * @throws IndexOutOfBoundsException if lineNo is invalid 202 */ 203 private String line(int lineNo) { 204 return text.get(lineNo); 205 } 206 207 /** 208 * Returns the Javadoc comment before the specified line. 209 * A return value of {@code null} means there is no such comment. 210 * 211 * @param lineNoBefore the line number to check before 212 * @return the Javadoc comment, or {@code null} if none 213 * @deprecated this method supports legacy checks that inspect Javadoc comments from 214 * {@code FileContents}; use 215 * {@link JavadocUtil#getAttachedJavadocComment(DetailAST)} with AST-based 216 * Javadoc processing instead. 217 * @noinspection DeprecatedIsStillUsed 218 * @noinspectionreason DeprecatedIsStillUsed - Method used in unit testing to verify 219 * legacy API behavior. 220 */ 221 @Deprecated(since = "13.9.0") 222 public TextBlock getJavadocBefore(int lineNoBefore) { 223 // Lines start at 1 to the callers perspective, so need to take off 2 224 int lineNo = lineNoBefore - 2; 225 226 // skip blank lines and comments 227 while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo) 228 || lineInsideBlockComment(lineNo + 1))) { 229 lineNo--; 230 } 231 232 return javadocComments.get(lineNo); 233 } 234 235 /** 236 * Checks if the specified line number is inside a block comment. 237 * This method scans through all block comments (excluding Javadoc comments) 238 * and determines whether the given line number falls within any of them 239 * 240 * @param lineNo the line number to check 241 * @return {@code true} if the line is inside a block comment (excluding Javadoc comments) 242 * , {@code false} otherwise 243 */ 244 private boolean lineInsideBlockComment(int lineNo) { 245 final Collection<List<TextBlock>> values = clangComments.values(); 246 return values.stream() 247 .flatMap(List::stream) 248 .filter(comment -> !javadocComments.containsValue(comment)) 249 .anyMatch(comment -> isLineBlockComment(lineNo, comment)); 250 } 251 252 /** 253 * Checks if the given line is inside a block comment 254 * and both the start and end lines contain only the comment. 255 * 256 * @param lineNo the line number to check 257 * @param comment the block comment to inspect 258 * @return {@code true} line is in block comment, {@code false} otherwise 259 */ 260 private boolean isLineBlockComment(int lineNo, TextBlock comment) { 261 final boolean lineInSideBlockComment = lineNo >= comment.getStartLineNo() 262 && lineNo <= comment.getEndLineNo(); 263 boolean lineHasOnlyBlockComment = true; 264 final String startLine = line(comment.getStartLineNo() - 1).trim(); 265 if (!startLine.startsWith("/*")) { 266 lineHasOnlyBlockComment = false; 267 } 268 269 final String endLine = line(comment.getEndLineNo() - 1).trim(); 270 if (!endLine.endsWith("*/")) { 271 lineHasOnlyBlockComment = false; 272 } 273 return lineInSideBlockComment && lineHasOnlyBlockComment; 274 } 275 276 /** 277 * Checks if the specified line is blank. 278 * 279 * @param lineNo the line number to check 280 * @return if the specified line consists only of tabs and spaces. 281 */ 282 public boolean lineIsBlank(int lineNo) { 283 return line(lineNo).isBlank(); 284 } 285 286 /** 287 * Checks if the specified line is a single-line comment without code. 288 * 289 * @param lineNo the line number to check 290 * @return if the specified line consists of only a single-line comment without code. 291 */ 292 public boolean lineIsComment(int lineNo) { 293 return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches(); 294 } 295 296 /** 297 * Checks if the specified position intersects with a comment. 298 * 299 * @param startLineNo the starting line number 300 * @param startColNo the starting column number 301 * @param endLineNo the ending line number 302 * @param endColNo the ending column number 303 * @return true if the positions intersects with a comment. 304 */ 305 public boolean hasIntersectionWithComment(int startLineNo, 306 int startColNo, int endLineNo, int endColNo) { 307 return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo) 308 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo, 309 endColNo); 310 } 311 312 /** 313 * Checks if the specified position intersects with a block comment. 314 * 315 * @param startLineNo the starting line number 316 * @param startColNo the starting column number 317 * @param endLineNo the ending line number 318 * @param endColNo the ending column number 319 * @return true if the positions intersects with a block comment. 320 */ 321 private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo, 322 int endLineNo, int endColNo) { 323 // Check C comments (all comments should be checked) 324 final Collection<List<TextBlock>> values = clangComments.values(); 325 return values.stream() 326 .flatMap(List::stream) 327 .anyMatch(comment -> comment.intersects(startLineNo, startColNo, endLineNo, endColNo)); 328 } 329 330 /** 331 * Checks if the specified position intersects with a single-line comment. 332 * 333 * @param startLineNo the starting line number 334 * @param startColNo the starting column number 335 * @param endLineNo the ending line number 336 * @param endColNo the ending column number 337 * @return true if the positions intersects with a single-line comment. 338 */ 339 private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo, 340 int endLineNo, int endColNo) { 341 boolean hasIntersection = false; 342 // Check CPP comments (line searching is possible) 343 for (int lineNumber = startLineNo; lineNumber <= endLineNo; 344 lineNumber++) { 345 final TextBlock comment = cppComments.get(lineNumber); 346 if (comment != null && comment.intersects(startLineNo, startColNo, 347 endLineNo, endColNo)) { 348 hasIntersection = true; 349 break; 350 } 351 } 352 return hasIntersection; 353 } 354 355 /** 356 * Returns a map of all the single-line comments. The key is a line number, 357 * the value is the comment {@link TextBlock} at the line. 358 * 359 * @return the Map of comments 360 */ 361 public Map<Integer, TextBlock> getSingleLineComments() { 362 return Collections.unmodifiableMap(cppComments); 363 } 364 365 /** 366 * Returns a map of all block comments. The key is the line number, the 367 * value is a {@link List} of block comment {@link TextBlock}s that start at that line. 368 * 369 * @return the map of comments 370 */ 371 public Map<Integer, List<TextBlock>> getBlockComments() { 372 return Collections.unmodifiableMap(clangComments); 373 } 374 375 /** 376 * Checks if the current file is a package-info.java file. 377 * 378 * @return true if the package file. 379 * @deprecated use {@link CheckUtil#isPackageInfo(String)} for the same functionality, 380 * or use {@link AbstractCheck#getFilePath()} to process your own standards. 381 */ 382 @Deprecated(since = "10.2") 383 public boolean inPackageInfo() { 384 return "package-info.java".equals(text.getFile().getName()); 385 } 386 387}