001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2021 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 // Check C comments (all comments should be checked) 284 final Collection<List<TextBlock>> values = clangComments.values(); 285 return values.stream() 286 .flatMap(List::stream) 287 .anyMatch(comment -> comment.intersects(startLineNo, startColNo, endLineNo, endColNo)); 288 } 289 290 /** 291 * Checks if the specified position intersects with a single line comment. 292 * 293 * @param startLineNo the starting line number 294 * @param startColNo the starting column number 295 * @param endLineNo the ending line number 296 * @param endColNo the ending column number 297 * @return true if the positions intersects with a single line comment. 298 */ 299 private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo, 300 int endLineNo, int endColNo) { 301 boolean hasIntersection = false; 302 // Check CPP comments (line searching is possible) 303 for (int lineNumber = startLineNo; lineNumber <= endLineNo; 304 lineNumber++) { 305 final TextBlock comment = cppComments.get(lineNumber); 306 if (comment != null && comment.intersects(startLineNo, startColNo, 307 endLineNo, endColNo)) { 308 hasIntersection = true; 309 break; 310 } 311 } 312 return hasIntersection; 313 } 314 315 /** 316 * Returns a map of all the single line comments. The key is a line number, 317 * the value is the comment {@link TextBlock} at the line. 318 * 319 * @return the Map of comments 320 */ 321 public Map<Integer, TextBlock> getSingleLineComments() { 322 return Collections.unmodifiableMap(cppComments); 323 } 324 325 /** 326 * Returns a map of all block comments. The key is the line number, the 327 * value is a {@link List} of block comment {@link TextBlock}s 328 * that start at that line. 329 * 330 * @return the map of comments 331 */ 332 public Map<Integer, List<TextBlock>> getBlockComments() { 333 return Collections.unmodifiableMap(clangComments); 334 } 335 336 /** 337 * Checks if the current file is a package-info.java file. 338 * 339 * @return true if the package file. 340 */ 341 public boolean inPackageInfo() { 342 return fileName.endsWith("package-info.java"); 343 } 344 345}