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 * @return an object containing the full text of the file 081 */ 082 public FileText getText() { 083 return new FileText(text); 084 } 085 086 /** 087 * Gets the lines in the file. 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 * @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 * @return the name of the file 106 */ 107 public String getFileName() { 108 return fileName; 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 if (clangComments.containsKey(startLineNo)) { 154 final List<TextBlock> entries = clangComments.get(startLineNo); 155 entries.add(comment); 156 } 157 else { 158 final List<TextBlock> entries = new ArrayList<>(); 159 entries.add(comment); 160 clangComments.put(startLineNo, entries); 161 } 162 163 // Remember if possible Javadoc comment 164 final String firstLine = line(startLineNo - 1); 165 if (firstLine.contains("/**") && !firstLine.contains("/**/")) { 166 javadocComments.put(endLineNo - 1, comment); 167 } 168 } 169 170 /** 171 * Returns the specified block comment as a String array. 172 * 173 * @param startLineNo the starting line number 174 * @param startColNo the starting column number 175 * @param endLineNo the ending line number 176 * @param endColNo the ending column number 177 * @return block comment as an array 178 **/ 179 private String[] extractBlockComment(int startLineNo, int startColNo, 180 int endLineNo, int endColNo) { 181 final String[] returnValue; 182 if (startLineNo == endLineNo) { 183 returnValue = new String[1]; 184 returnValue[0] = line(startLineNo - 1).substring(startColNo, 185 endColNo + 1); 186 } 187 else { 188 returnValue = new String[endLineNo - startLineNo + 1]; 189 returnValue[0] = line(startLineNo - 1).substring(startColNo); 190 for (int i = startLineNo; i < endLineNo; i++) { 191 returnValue[i - startLineNo + 1] = line(i); 192 } 193 returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0, 194 endColNo + 1); 195 } 196 return returnValue; 197 } 198 199 /** 200 * Get a single line. 201 * For internal use only, as getText().get(lineNo) is just as 202 * suitable for external use and avoids method duplication. 203 * @param lineNo the number of the line to get 204 * @return the corresponding line, without terminator 205 * @throws IndexOutOfBoundsException if lineNo is invalid 206 */ 207 private String line(int lineNo) { 208 return text.get(lineNo); 209 } 210 211 /** 212 * Returns the Javadoc comment before the specified line. 213 * A return value of {@code null} means there is no such comment. 214 * 215 * @param lineNoBefore the line number to check before 216 * @return the Javadoc comment, or {@code null} if none 217 **/ 218 public TextBlock getJavadocBefore(int lineNoBefore) { 219 // Lines start at 1 to the callers perspective, so need to take off 2 220 int lineNo = lineNoBefore - 2; 221 222 // skip blank lines 223 while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) { 224 lineNo--; 225 } 226 227 return javadocComments.get(lineNo); 228 } 229 230 /** 231 * Checks if the specified line is blank. 232 * 233 * @param lineNo the line number to check 234 * @return if the specified line consists only of tabs and spaces. 235 **/ 236 public boolean lineIsBlank(int lineNo) { 237 return CommonUtil.isBlank(line(lineNo)); 238 } 239 240 /** 241 * Checks if the specified line is a single-line comment without code. 242 * 243 * @param lineNo the line number to check 244 * @return if the specified line consists of only a single line comment 245 * without code. 246 **/ 247 public boolean lineIsComment(int lineNo) { 248 return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches(); 249 } 250 251 /** 252 * Checks if the specified position intersects with a comment. 253 * 254 * @param startLineNo the starting line number 255 * @param startColNo the starting column number 256 * @param endLineNo the ending line number 257 * @param endColNo the ending column number 258 * @return true if the positions intersects with a comment. 259 **/ 260 public boolean hasIntersectionWithComment(int startLineNo, 261 int startColNo, int endLineNo, int endColNo) { 262 return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo) 263 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo, 264 endColNo); 265 } 266 267 /** 268 * Checks if the specified position intersects with a block comment. 269 * 270 * @param startLineNo the starting line number 271 * @param startColNo the starting column number 272 * @param endLineNo the ending line number 273 * @param endColNo the ending column number 274 * @return true if the positions intersects with a block comment. 275 */ 276 private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo, 277 int endLineNo, int endColNo) { 278 boolean hasIntersection = false; 279 // Check C comments (all comments should be checked) 280 final Collection<List<TextBlock>> values = clangComments.values(); 281 for (final List<TextBlock> row : values) { 282 for (final TextBlock comment : row) { 283 if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) { 284 hasIntersection = true; 285 break; 286 } 287 } 288 if (hasIntersection) { 289 break; 290 } 291 } 292 return hasIntersection; 293 } 294 295 /** 296 * Checks if the specified position intersects with a single line comment. 297 * 298 * @param startLineNo the starting line number 299 * @param startColNo the starting column number 300 * @param endLineNo the ending line number 301 * @param endColNo the ending column number 302 * @return true if the positions intersects with a single line comment. 303 */ 304 private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo, 305 int endLineNo, int endColNo) { 306 boolean hasIntersection = false; 307 // Check CPP comments (line searching is possible) 308 for (int lineNumber = startLineNo; lineNumber <= endLineNo; 309 lineNumber++) { 310 final TextBlock comment = cppComments.get(lineNumber); 311 if (comment != null && comment.intersects(startLineNo, startColNo, 312 endLineNo, endColNo)) { 313 hasIntersection = true; 314 break; 315 } 316 } 317 return hasIntersection; 318 } 319 320 /** 321 * Returns a map of all the single line comments. The key is a line number, 322 * the value is the comment {@link TextBlock} at the line. 323 * @return the Map of comments 324 */ 325 public Map<Integer, TextBlock> getSingleLineComments() { 326 return Collections.unmodifiableMap(cppComments); 327 } 328 329 /** 330 * Returns a map of all block comments. The key is the line number, the 331 * value is a {@link List} of block comment {@link TextBlock}s 332 * that start at that line. 333 * @return the map of comments 334 */ 335 public Map<Integer, List<TextBlock>> getBlockComments() { 336 return Collections.unmodifiableMap(clangComments); 337 } 338 339 /** 340 * Checks if the current file is a package-info.java file. 341 * @return true if the package file. 342 */ 343 public boolean inPackageInfo() { 344 return fileName.endsWith("package-info.java"); 345 } 346 347}