001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2020 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 file name. */ 049 private final String fileName; 050 051 /** The text. */ 052 private final FileText text; 053 054 /** 055 * Map of the Javadoc comments indexed on the last line of the comment. 056 * The hack is it assumes that there is only one Javadoc comment per line. 057 */ 058 private final Map<Integer, TextBlock> javadocComments = new HashMap<>(); 059 /** Map of the C++ comments indexed on the first line of the comment. */ 060 private final Map<Integer, TextBlock> cppComments = new HashMap<>(); 061 062 /** 063 * Map of the C comments indexed on the first line of the comment to a list 064 * of comments on that line. 065 */ 066 private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>(); 067 068 /** 069 * Creates a new {@code FileContents} instance. 070 * 071 * @param text the contents of the file 072 */ 073 public FileContents(FileText text) { 074 fileName = text.getFile().toString(); 075 this.text = new FileText(text); 076 } 077 078 /** 079 * Get the full text of the file. 080 * 081 * @return an object containing the full text of the file 082 */ 083 public FileText getText() { 084 return new FileText(text); 085 } 086 087 /** 088 * Gets the lines in the file. 089 * 090 * @return the lines in the file 091 */ 092 public String[] getLines() { 093 return text.toLinesArray(); 094 } 095 096 /** 097 * Get the line from text of the file. 098 * 099 * @param index index of the line 100 * @return line from text of the file 101 */ 102 public String getLine(int index) { 103 return text.get(index); 104 } 105 106 /** 107 * Gets the name of the file. 108 * 109 * @return the name of the file 110 */ 111 public String getFileName() { 112 return fileName; 113 } 114 115 @Override 116 public void reportSingleLineComment(String type, int startLineNo, 117 int startColNo) { 118 reportSingleLineComment(startLineNo, startColNo); 119 } 120 121 /** 122 * Report the location of a single line comment. 123 * 124 * @param startLineNo the starting line number 125 * @param startColNo the starting column number 126 **/ 127 public void reportSingleLineComment(int startLineNo, int startColNo) { 128 final String line = line(startLineNo - 1); 129 final String[] txt = {line.substring(startColNo)}; 130 final Comment comment = new Comment(txt, startColNo, startLineNo, 131 line.length() - 1); 132 cppComments.put(startLineNo, comment); 133 } 134 135 @Override 136 public void reportBlockComment(String type, int startLineNo, 137 int startColNo, int endLineNo, int endColNo) { 138 reportBlockComment(startLineNo, startColNo, endLineNo, endColNo); 139 } 140 141 /** 142 * Report the location of a block comment. 143 * 144 * @param startLineNo the starting line number 145 * @param startColNo the starting column number 146 * @param endLineNo the ending line number 147 * @param endColNo the ending column number 148 **/ 149 public void reportBlockComment(int startLineNo, int startColNo, 150 int endLineNo, int endColNo) { 151 final String[] cComment = extractBlockComment(startLineNo, startColNo, 152 endLineNo, endColNo); 153 final Comment comment = new Comment(cComment, startColNo, endLineNo, 154 endColNo); 155 156 // save the comment 157 if (clangComments.containsKey(startLineNo)) { 158 final List<TextBlock> entries = clangComments.get(startLineNo); 159 entries.add(comment); 160 } 161 else { 162 final List<TextBlock> entries = new ArrayList<>(); 163 entries.add(comment); 164 clangComments.put(startLineNo, entries); 165 } 166 167 // Remember if possible Javadoc comment 168 final String firstLine = line(startLineNo - 1); 169 if (firstLine.contains("/**") && !firstLine.contains("/**/")) { 170 javadocComments.put(endLineNo - 1, comment); 171 } 172 } 173 174 /** 175 * Returns the specified block comment as a String array. 176 * 177 * @param startLineNo the starting line number 178 * @param startColNo the starting column number 179 * @param endLineNo the ending line number 180 * @param endColNo the ending column number 181 * @return block comment as an array 182 **/ 183 private String[] extractBlockComment(int startLineNo, int startColNo, 184 int endLineNo, int endColNo) { 185 final String[] returnValue; 186 if (startLineNo == endLineNo) { 187 returnValue = new String[1]; 188 returnValue[0] = line(startLineNo - 1).substring(startColNo, 189 endColNo + 1); 190 } 191 else { 192 returnValue = new String[endLineNo - startLineNo + 1]; 193 returnValue[0] = line(startLineNo - 1).substring(startColNo); 194 for (int i = startLineNo; i < endLineNo; i++) { 195 returnValue[i - startLineNo + 1] = line(i); 196 } 197 returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0, 198 endColNo + 1); 199 } 200 return returnValue; 201 } 202 203 /** 204 * Get a single line. 205 * For internal use only, as getText().get(lineNo) is just as 206 * suitable for external use and avoids method duplication. 207 * 208 * @param lineNo the number of the line to get 209 * @return the corresponding line, without terminator 210 * @throws IndexOutOfBoundsException if lineNo is invalid 211 */ 212 private String line(int lineNo) { 213 return text.get(lineNo); 214 } 215 216 /** 217 * Returns the Javadoc comment before the specified line. 218 * A return value of {@code null} means there is no such comment. 219 * 220 * @param lineNoBefore the line number to check before 221 * @return the Javadoc comment, or {@code null} if none 222 **/ 223 public TextBlock getJavadocBefore(int lineNoBefore) { 224 // Lines start at 1 to the callers perspective, so need to take off 2 225 int lineNo = lineNoBefore - 2; 226 227 // skip blank lines 228 while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) { 229 lineNo--; 230 } 231 232 return javadocComments.get(lineNo); 233 } 234 235 /** 236 * Checks if the specified line is blank. 237 * 238 * @param lineNo the line number to check 239 * @return if the specified line consists only of tabs and spaces. 240 **/ 241 public boolean lineIsBlank(int lineNo) { 242 return CommonUtil.isBlank(line(lineNo)); 243 } 244 245 /** 246 * Checks if the specified line is a single-line comment without code. 247 * 248 * @param lineNo the line number to check 249 * @return if the specified line consists of only a single line comment 250 * without code. 251 **/ 252 public boolean lineIsComment(int lineNo) { 253 return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches(); 254 } 255 256 /** 257 * Checks if the specified position intersects with a comment. 258 * 259 * @param startLineNo the starting line number 260 * @param startColNo the starting column number 261 * @param endLineNo the ending line number 262 * @param endColNo the ending column number 263 * @return true if the positions intersects with a comment. 264 **/ 265 public boolean hasIntersectionWithComment(int startLineNo, 266 int startColNo, int endLineNo, int endColNo) { 267 return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo) 268 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo, 269 endColNo); 270 } 271 272 /** 273 * Checks if the specified position intersects with a block comment. 274 * 275 * @param startLineNo the starting line number 276 * @param startColNo the starting column number 277 * @param endLineNo the ending line number 278 * @param endColNo the ending column number 279 * @return true if the positions intersects with a block comment. 280 */ 281 private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo, 282 int endLineNo, int endColNo) { 283 boolean hasIntersection = false; 284 // Check C comments (all comments should be checked) 285 final Collection<List<TextBlock>> values = clangComments.values(); 286 for (final List<TextBlock> row : values) { 287 for (final TextBlock comment : row) { 288 if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) { 289 hasIntersection = true; 290 break; 291 } 292 } 293 if (hasIntersection) { 294 break; 295 } 296 } 297 return hasIntersection; 298 } 299 300 /** 301 * Checks if the specified position intersects with a single line comment. 302 * 303 * @param startLineNo the starting line number 304 * @param startColNo the starting column number 305 * @param endLineNo the ending line number 306 * @param endColNo the ending column number 307 * @return true if the positions intersects with a single line comment. 308 */ 309 private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo, 310 int endLineNo, int endColNo) { 311 boolean hasIntersection = false; 312 // Check CPP comments (line searching is possible) 313 for (int lineNumber = startLineNo; lineNumber <= endLineNo; 314 lineNumber++) { 315 final TextBlock comment = cppComments.get(lineNumber); 316 if (comment != null && comment.intersects(startLineNo, startColNo, 317 endLineNo, endColNo)) { 318 hasIntersection = true; 319 break; 320 } 321 } 322 return hasIntersection; 323 } 324 325 /** 326 * Returns a map of all the single line comments. The key is a line number, 327 * the value is the comment {@link TextBlock} at the line. 328 * 329 * @return the Map of comments 330 */ 331 public Map<Integer, TextBlock> getSingleLineComments() { 332 return Collections.unmodifiableMap(cppComments); 333 } 334 335 /** 336 * Returns a map of all block comments. The key is the line number, the 337 * value is a {@link List} of block comment {@link TextBlock}s 338 * that start at that line. 339 * 340 * @return the map of comments 341 */ 342 public Map<Integer, List<TextBlock>> getBlockComments() { 343 return Collections.unmodifiableMap(clangComments); 344 } 345 346 /** 347 * Checks if the current file is a package-info.java file. 348 * 349 * @return true if the package file. 350 */ 351 public boolean inPackageInfo() { 352 return fileName.endsWith("package-info.java"); 353 } 354 355}