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