001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2022 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.CommonUtil; 032 033/** 034 * Represents the contents of a file. 035 * 036 */ 037public final class FileContents implements CommentListener { 038 039 /** 040 * The pattern to match a single line comment containing only the comment 041 * 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 @Override 112 public void reportSingleLineComment(String type, int startLineNo, 113 int startColNo) { 114 reportSingleLineComment(startLineNo, startColNo); 115 } 116 117 /** 118 * Report the location of a single line comment. 119 * 120 * @param startLineNo the starting line number 121 * @param startColNo the starting column number 122 **/ 123 public void reportSingleLineComment(int startLineNo, int startColNo) { 124 final String line = line(startLineNo - 1); 125 final String[] txt = {line.substring(startColNo)}; 126 final Comment comment = new Comment(txt, startColNo, startLineNo, 127 line.length() - 1); 128 cppComments.put(startLineNo, comment); 129 } 130 131 @Override 132 public void reportBlockComment(String type, int startLineNo, 133 int startColNo, int endLineNo, int endColNo) { 134 reportBlockComment(startLineNo, startColNo, endLineNo, endColNo); 135 } 136 137 /** 138 * Report the location of a block comment. 139 * 140 * @param startLineNo the starting line number 141 * @param startColNo the starting column number 142 * @param endLineNo the ending line number 143 * @param endColNo the ending column number 144 **/ 145 public void reportBlockComment(int startLineNo, int startColNo, 146 int endLineNo, int endColNo) { 147 final String[] cComment = extractBlockComment(startLineNo, startColNo, 148 endLineNo, endColNo); 149 final Comment comment = new Comment(cComment, startColNo, endLineNo, 150 endColNo); 151 152 // save the comment 153 final List<TextBlock> entries = clangComments.computeIfAbsent(startLineNo, 154 empty -> new ArrayList<>()); 155 156 entries.add(comment); 157 158 // Remember if possible Javadoc comment 159 final String firstLine = line(startLineNo - 1); 160 if (firstLine.contains("/**") && !firstLine.contains("/**/")) { 161 javadocComments.put(endLineNo - 1, comment); 162 } 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 i = startLineNo; i < endLineNo; i++) { 186 returnValue[i - startLineNo + 1] = line(i); 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 **/ 214 public TextBlock getJavadocBefore(int lineNoBefore) { 215 // Lines start at 1 to the callers perspective, so need to take off 2 216 int lineNo = lineNoBefore - 2; 217 218 // skip blank lines 219 while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) { 220 lineNo--; 221 } 222 223 return javadocComments.get(lineNo); 224 } 225 226 /** 227 * Checks if the specified line is blank. 228 * 229 * @param lineNo the line number to check 230 * @return if the specified line consists only of tabs and spaces. 231 **/ 232 public boolean lineIsBlank(int lineNo) { 233 return CommonUtil.isBlank(line(lineNo)); 234 } 235 236 /** 237 * Checks if the specified line is a single-line comment without code. 238 * 239 * @param lineNo the line number to check 240 * @return if the specified line consists of only a single line comment 241 * without code. 242 **/ 243 public boolean lineIsComment(int lineNo) { 244 return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches(); 245 } 246 247 /** 248 * Checks if the specified position intersects with a comment. 249 * 250 * @param startLineNo the starting line number 251 * @param startColNo the starting column number 252 * @param endLineNo the ending line number 253 * @param endColNo the ending column number 254 * @return true if the positions intersects with a comment. 255 **/ 256 public boolean hasIntersectionWithComment(int startLineNo, 257 int startColNo, int endLineNo, int endColNo) { 258 return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo) 259 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo, 260 endColNo); 261 } 262 263 /** 264 * Checks if the specified position intersects with a block comment. 265 * 266 * @param startLineNo the starting line number 267 * @param startColNo the starting column number 268 * @param endLineNo the ending line number 269 * @param endColNo the ending column number 270 * @return true if the positions intersects with a block comment. 271 */ 272 private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo, 273 int endLineNo, int endColNo) { 274 // Check C comments (all comments should be checked) 275 final Collection<List<TextBlock>> values = clangComments.values(); 276 return values.stream() 277 .flatMap(List::stream) 278 .anyMatch(comment -> comment.intersects(startLineNo, startColNo, endLineNo, endColNo)); 279 } 280 281 /** 282 * Checks if the specified position intersects with a single line comment. 283 * 284 * @param startLineNo the starting line number 285 * @param startColNo the starting column number 286 * @param endLineNo the ending line number 287 * @param endColNo the ending column number 288 * @return true if the positions intersects with a single line comment. 289 */ 290 private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo, 291 int endLineNo, int endColNo) { 292 boolean hasIntersection = false; 293 // Check CPP comments (line searching is possible) 294 for (int lineNumber = startLineNo; lineNumber <= endLineNo; 295 lineNumber++) { 296 final TextBlock comment = cppComments.get(lineNumber); 297 if (comment != null && comment.intersects(startLineNo, startColNo, 298 endLineNo, endColNo)) { 299 hasIntersection = true; 300 break; 301 } 302 } 303 return hasIntersection; 304 } 305 306 /** 307 * Returns a map of all the single line comments. The key is a line number, 308 * the value is the comment {@link TextBlock} at the line. 309 * 310 * @return the Map of comments 311 */ 312 public Map<Integer, TextBlock> getSingleLineComments() { 313 return Collections.unmodifiableMap(cppComments); 314 } 315 316 /** 317 * Returns a map of all block comments. The key is the line number, the 318 * value is a {@link List} of block comment {@link TextBlock}s 319 * that start at that line. 320 * 321 * @return the map of comments 322 */ 323 public Map<Integer, List<TextBlock>> getBlockComments() { 324 return Collections.unmodifiableMap(clangComments); 325 } 326 327 /** 328 * Checks if the current file is a package-info.java file. 329 * 330 * @return true if the package file. 331 */ 332 public boolean inPackageInfo() { 333 return getFileName().endsWith("package-info.java"); 334 } 335 336}