1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.api;
21  
22  import java.io.File;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collection;
26  import java.util.Collections;
27  import java.util.HashMap;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.regex.Pattern;
31  
32  import com.puppycrawl.tools.checkstyle.grammar.CommentListener;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
34  
35  /**
36   * Represents the contents of a file.
37   *
38   */
39  public final class FileContents implements CommentListener {
40  
41      /**
42       * The pattern to match a single line comment containing only the comment
43       * itself -- no code.
44       */
45      private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
46      /** Compiled regexp to match a single-line comment line. */
47      private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
48              .compile(MATCH_SINGLELINE_COMMENT_PAT);
49  
50      /** The file name. */
51      private final String fileName;
52  
53      /** The text. */
54      private final FileText text;
55  
56      /** Map of the Javadoc comments indexed on the last line of the comment.
57       * The hack is it assumes that there is only one Javadoc comment per line.
58       */
59      private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
60      /** Map of the C++ comments indexed on the first line of the comment. */
61      private final Map<Integer, TextBlock> cppComments = new HashMap<>();
62  
63      /**
64       * Map of the C comments indexed on the first line of the comment to a list
65       * of comments on that line.
66       */
67      private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
68  
69      /**
70       * Creates a new {@code FileContents} instance.
71       *
72       * @param filename name of the file
73       * @param lines the contents of the file
74       * @deprecated Use {@link #FileContents(FileText)} instead
75       *     in order to preserve the original line breaks where possible.
76       */
77      @Deprecated
78      public FileContents(String filename, String... lines) {
79          fileName = filename;
80          text = new FileText(new File(filename), Arrays.asList(lines));
81      }
82  
83      /**
84       * Creates a new {@code FileContents} instance.
85       *
86       * @param text the contents of the file
87       */
88      public FileContents(FileText text) {
89          fileName = text.getFile().toString();
90          this.text = new FileText(text);
91      }
92  
93      @Override
94      public void reportSingleLineComment(String type, int startLineNo,
95              int startColNo) {
96          reportSingleLineComment(startLineNo, startColNo);
97      }
98  
99      /**
100      * Report the location of a single line comment.
101      * @param startLineNo the starting line number
102      * @param startColNo the starting column number
103      **/
104     public void reportSingleLineComment(int startLineNo, int startColNo) {
105         final String line = line(startLineNo - 1);
106         final String[] txt = {line.substring(startColNo)};
107         final Comment comment = new Comment(txt, startColNo, startLineNo,
108                 line.length() - 1);
109         cppComments.put(startLineNo, comment);
110     }
111 
112     @Override
113     public void reportBlockComment(String type, int startLineNo,
114             int startColNo, int endLineNo, int endColNo) {
115         reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
116     }
117 
118     /**
119      * Report the location of a block comment.
120      * @param startLineNo the starting line number
121      * @param startColNo the starting column number
122      * @param endLineNo the ending line number
123      * @param endColNo the ending column number
124      **/
125     private void reportBlockComment(int startLineNo, int startColNo,
126             int endLineNo, int endColNo) {
127         final String[] cComment = extractBlockComment(startLineNo, startColNo,
128                 endLineNo, endColNo);
129         final Comment comment = new Comment(cComment, startColNo, endLineNo,
130                 endColNo);
131 
132         // save the comment
133         if (clangComments.containsKey(startLineNo)) {
134             final List<TextBlock> entries = clangComments.get(startLineNo);
135             entries.add(comment);
136         }
137         else {
138             final List<TextBlock> entries = new ArrayList<>();
139             entries.add(comment);
140             clangComments.put(startLineNo, entries);
141         }
142 
143         // Remember if possible Javadoc comment
144         final String firstLine = line(startLineNo - 1);
145         if (firstLine.contains("/**") && !firstLine.contains("/**/")) {
146             javadocComments.put(endLineNo - 1, comment);
147         }
148     }
149 
150     /**
151      * Report the location of a C++ style comment.
152      * @param startLineNo the starting line number
153      * @param startColNo the starting column number
154      * @deprecated Use {@link #reportSingleLineComment(int, int)} instead.
155      **/
156     @Deprecated
157     public void reportCppComment(int startLineNo, int startColNo) {
158         reportSingleLineComment(startLineNo, startColNo);
159     }
160 
161     /**
162      * Returns a map of all the C++ style comments. The key is a line number,
163      * the value is the comment {@link TextBlock} at the line.
164      * @return the Map of comments
165      * @deprecated Use {@link #getSingleLineComments()} instead.
166      */
167     @Deprecated
168     public Map<Integer, TextBlock> getCppComments() {
169         return getSingleLineComments();
170     }
171 
172     /**
173      * Returns a map of all the single line comments. The key is a line number,
174      * the value is the comment {@link TextBlock} at the line.
175      * @return the Map of comments
176      */
177     public Map<Integer, TextBlock> getSingleLineComments() {
178         return Collections.unmodifiableMap(cppComments);
179     }
180 
181     /**
182      * Report the location of a C-style comment.
183      * @param startLineNo the starting line number
184      * @param startColNo the starting column number
185      * @param endLineNo the ending line number
186      * @param endColNo the ending column number
187      * @deprecated Use {@link #reportBlockComment(int, int, int, int)} instead.
188      **/
189     // -@cs[AbbreviationAsWordInName] Can't change yet since class is API.
190     @Deprecated
191     public void reportCComment(int startLineNo, int startColNo,
192             int endLineNo, int endColNo) {
193         reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
194     }
195 
196     /**
197      * Returns a map of all C style comments. The key is the line number, the
198      * value is a {@link List} of C style comment {@link TextBlock}s
199      * that start at that line.
200      * @return the map of comments
201      * @deprecated Use {@link #getBlockComments()} instead.
202      */
203     // -@cs[AbbreviationAsWordInName] Can't change yet since class is API.
204     @Deprecated
205     public Map<Integer, List<TextBlock>> getCComments() {
206         return getBlockComments();
207     }
208 
209     /**
210      * Returns a map of all block comments. The key is the line number, the
211      * value is a {@link List} of block comment {@link TextBlock}s
212      * that start at that line.
213      * @return the map of comments
214      */
215     public Map<Integer, List<TextBlock>> getBlockComments() {
216         return Collections.unmodifiableMap(clangComments);
217     }
218 
219     /**
220      * Returns the specified block comment as a String array.
221      * @param startLineNo the starting line number
222      * @param startColNo the starting column number
223      * @param endLineNo the ending line number
224      * @param endColNo the ending column number
225      * @return block comment as an array
226      **/
227     private String[] extractBlockComment(int startLineNo, int startColNo,
228             int endLineNo, int endColNo) {
229         final String[] returnValue;
230         if (startLineNo == endLineNo) {
231             returnValue = new String[1];
232             returnValue[0] = line(startLineNo - 1).substring(startColNo,
233                     endColNo + 1);
234         }
235         else {
236             returnValue = new String[endLineNo - startLineNo + 1];
237             returnValue[0] = line(startLineNo - 1).substring(startColNo);
238             for (int i = startLineNo; i < endLineNo; i++) {
239                 returnValue[i - startLineNo + 1] = line(i);
240             }
241             returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0,
242                     endColNo + 1);
243         }
244         return returnValue;
245     }
246 
247     /**
248      * Returns the Javadoc comment before the specified line.
249      * A return value of {@code null} means there is no such comment.
250      * @param lineNoBefore the line number to check before
251      * @return the Javadoc comment, or {@code null} if none
252      **/
253     public TextBlock getJavadocBefore(int lineNoBefore) {
254         // Lines start at 1 to the callers perspective, so need to take off 2
255         int lineNo = lineNoBefore - 2;
256 
257         // skip blank lines
258         while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
259             lineNo--;
260         }
261 
262         return javadocComments.get(lineNo);
263     }
264 
265     /**
266      * Get a single line.
267      * For internal use only, as getText().get(lineNo) is just as
268      * suitable for external use and avoids method duplication.
269      * @param lineNo the number of the line to get
270      * @return the corresponding line, without terminator
271      * @throws IndexOutOfBoundsException if lineNo is invalid
272      */
273     private String line(int lineNo) {
274         return text.get(lineNo);
275     }
276 
277     /**
278      * Get the full text of the file.
279      * @return an object containing the full text of the file
280      */
281     public FileText getText() {
282         return new FileText(text);
283     }
284 
285     /**
286      * Gets the lines in the file.
287      * @return the lines in the file
288      */
289     public String[] getLines() {
290         return text.toLinesArray();
291     }
292 
293     /**
294      * Get the line from text of the file.
295      * @param index index of the line
296      * @return line from text of the file
297      */
298     public String getLine(int index) {
299         return text.get(index);
300     }
301 
302     /**
303      * Gets the name of the file.
304      * @return the name of the file
305      */
306     public String getFileName() {
307         return fileName;
308     }
309 
310     /**
311      * Checks if the specified line is blank.
312      * @param lineNo the line number to check
313      * @return if the specified line consists only of tabs and spaces.
314      **/
315     public boolean lineIsBlank(int lineNo) {
316         return CommonUtil.isBlank(line(lineNo));
317     }
318 
319     /**
320      * Checks if the specified line is a single-line comment without code.
321      * @param lineNo  the line number to check
322      * @return if the specified line consists of only a single line comment
323      *         without code.
324      **/
325     public boolean lineIsComment(int lineNo) {
326         return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
327     }
328 
329     /**
330      * Checks if the specified position intersects with a comment.
331      * @param startLineNo the starting line number
332      * @param startColNo the starting column number
333      * @param endLineNo the ending line number
334      * @param endColNo the ending column number
335      * @return true if the positions intersects with a comment.
336      **/
337     public boolean hasIntersectionWithComment(int startLineNo,
338             int startColNo, int endLineNo, int endColNo) {
339         return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
340                 || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
341                         endColNo);
342     }
343 
344     /**
345      * Checks if the current file is a package-info.java file.
346      * @return true if the package file.
347      */
348     public boolean inPackageInfo() {
349         return fileName.endsWith("package-info.java");
350     }
351 
352     /**
353      * Checks if the specified position intersects with a block comment.
354      * @param startLineNo the starting line number
355      * @param startColNo the starting column number
356      * @param endLineNo the ending line number
357      * @param endColNo the ending column number
358      * @return true if the positions intersects with a block comment.
359      */
360     private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
361             int endLineNo, int endColNo) {
362         boolean hasIntersection = false;
363         // Check C comments (all comments should be checked)
364         final Collection<List<TextBlock>> values = clangComments.values();
365         for (final List<TextBlock> row : values) {
366             for (final TextBlock comment : row) {
367                 if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) {
368                     hasIntersection = true;
369                     break;
370                 }
371             }
372             if (hasIntersection) {
373                 break;
374             }
375         }
376         return hasIntersection;
377     }
378 
379     /**
380      * Checks if the specified position intersects with a single line comment.
381      * @param startLineNo the starting line number
382      * @param startColNo the starting column number
383      * @param endLineNo the ending line number
384      * @param endColNo the ending column number
385      * @return true if the positions intersects with a single line comment.
386      */
387     private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
388             int endLineNo, int endColNo) {
389         boolean hasIntersection = false;
390         // Check CPP comments (line searching is possible)
391         for (int lineNumber = startLineNo; lineNumber <= endLineNo;
392              lineNumber++) {
393             final TextBlock comment = cppComments.get(lineNumber);
394             if (comment != null && comment.intersects(startLineNo, startColNo,
395                     endLineNo, endColNo)) {
396                 hasIntersection = true;
397                 break;
398             }
399         }
400         return hasIntersection;
401     }
402 
403 }