001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.io.File;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.regex.Pattern;
030
031import com.google.common.collect.ImmutableMap;
032import com.puppycrawl.tools.checkstyle.grammars.CommentListener;
033
034/**
035 * Represents the contents of a file.
036 *
037 * @author Oliver Burn
038 */
039public final class FileContents implements CommentListener {
040    /**
041     * The pattern to match a single line comment containing only the comment
042     * itself -- no code.
043     */
044    private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
045    /** Compiled regexp to match a single-line comment line. */
046    private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
047            .compile(MATCH_SINGLELINE_COMMENT_PAT);
048
049    /** The file name. */
050    private final String fileName;
051
052    /** The text. */
053    private final FileText text;
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 filename name of the file
072     * @param lines the contents of the file
073     * @deprecated Use {@link #FileContents(FileText)} instead
074     *     in order to preserve the original line breaks where possible.
075     */
076    @Deprecated
077    public FileContents(String filename, String... lines) {
078        fileName = filename;
079        text = FileText.fromLines(new File(filename), Arrays.asList(lines));
080    }
081
082    /**
083     * Creates a new {@code FileContents} instance.
084     *
085     * @param text the contents of the file
086     */
087    public FileContents(FileText text) {
088        fileName = text.getFile().toString();
089        this.text = new FileText(text);
090    }
091
092    @Override
093    public void reportSingleLineComment(String type, int startLineNo,
094            int startColNo) {
095        reportSingleLineComment(startLineNo, startColNo);
096    }
097
098    /**
099     * Report the location of a single line comment.
100     * @param startLineNo the starting line number
101     * @param startColNo the starting column number
102     **/
103    public void reportSingleLineComment(int startLineNo, int startColNo) {
104        final String line = line(startLineNo - 1);
105        final String[] txt = {line.substring(startColNo)};
106        final Comment comment = new Comment(txt, startColNo, startLineNo,
107                line.length() - 1);
108        cppComments.put(startLineNo, comment);
109    }
110
111    @Override
112    public void reportBlockComment(String type, int startLineNo,
113            int startColNo, int endLineNo, int endColNo) {
114        reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
115    }
116
117    /**
118     * Report the location of a block comment.
119     * @param startLineNo the starting line number
120     * @param startColNo the starting column number
121     * @param endLineNo the ending line number
122     * @param endColNo the ending column number
123     **/
124    public void reportBlockComment(int startLineNo, int startColNo,
125            int endLineNo, int endColNo) {
126        final String[] cComment = extractBlockComment(startLineNo, startColNo,
127                endLineNo, endColNo);
128        final Comment comment = new Comment(cComment, startColNo, endLineNo,
129                endColNo);
130
131        // save the comment
132        if (clangComments.containsKey(startLineNo)) {
133            final List<TextBlock> entries = clangComments.get(startLineNo);
134            entries.add(comment);
135        }
136        else {
137            final List<TextBlock> entries = new ArrayList<>();
138            entries.add(comment);
139            clangComments.put(startLineNo, entries);
140        }
141
142        // Remember if possible Javadoc comment
143        final String firstLine = line(startLineNo - 1);
144        if (firstLine.contains("/**") && !firstLine.contains("/**/")) {
145            javadocComments.put(endLineNo - 1, comment);
146        }
147    }
148
149    /**
150     * Report the location of a C++ style comment.
151     * @param startLineNo the starting line number
152     * @param startColNo the starting column number
153     * @deprecated Use {@link #reportSingleLineComment(int, int)} instead.
154     **/
155    @Deprecated
156    public void reportCppComment(int startLineNo, int startColNo) {
157        reportSingleLineComment(startLineNo, startColNo);
158    }
159
160    /**
161     * Returns a map of all the C++ style comments. The key is a line number,
162     * the value is the comment {@link TextBlock} at the line.
163     * @return the Map of comments
164     * @deprecated Use {@link #getSingleLineComments()} instead.
165     */
166    @Deprecated
167    public ImmutableMap<Integer, TextBlock> getCppComments() {
168        return getSingleLineComments();
169    }
170
171    /**
172     * Returns a map of all the single line comments. The key is a line number,
173     * the value is the comment {@link TextBlock} at the line.
174     * @return the Map of comments
175     */
176    public ImmutableMap<Integer, TextBlock> getSingleLineComments() {
177        return ImmutableMap.copyOf(cppComments);
178    }
179
180    /**
181     * Report the location of a C-style comment.
182     * @param startLineNo the starting line number
183     * @param startColNo the starting column number
184     * @param endLineNo the ending line number
185     * @param endColNo the ending column number
186     * @deprecated Use {@link #reportBlockComment(int, int, int, int)} instead.
187     **/
188    // -@cs[AbbreviationAsWordInName] Can't change yet since class is API.
189    @Deprecated
190    public void reportCComment(int startLineNo, int startColNo,
191            int endLineNo, int endColNo) {
192        reportBlockComment(startLineNo, startColNo, endLineNo, endColNo);
193    }
194
195    /**
196     * Returns a map of all C style comments. The key is the line number, the
197     * value is a {@link List} of C style comment {@link TextBlock}s
198     * that start at that line.
199     * @return the map of comments
200     * @deprecated Use {@link #getBlockComments()} instead.
201     */
202    // -@cs[AbbreviationAsWordInName] Can't change yet since class is API.
203    @Deprecated
204    public ImmutableMap<Integer, List<TextBlock>> getCComments() {
205        return getBlockComments();
206    }
207
208    /**
209     * Returns a map of all block comments. The key is the line number, the
210     * value is a {@link List} of block comment {@link TextBlock}s
211     * that start at that line.
212     * @return the map of comments
213     */
214    public ImmutableMap<Integer, List<TextBlock>> getBlockComments() {
215        return ImmutableMap.copyOf(clangComments);
216    }
217
218    /**
219     * Returns the specified block comment as a String array.
220     * @param startLineNo the starting line number
221     * @param startColNo the starting column number
222     * @param endLineNo the ending line number
223     * @param endColNo the ending column number
224     * @return block comment as an array
225     **/
226    private String[] extractBlockComment(int startLineNo, int startColNo,
227            int endLineNo, int endColNo) {
228        final String[] returnValue;
229        if (startLineNo == endLineNo) {
230            returnValue = new String[1];
231            returnValue[0] = line(startLineNo - 1).substring(startColNo,
232                    endColNo + 1);
233        }
234        else {
235            returnValue = new String[endLineNo - startLineNo + 1];
236            returnValue[0] = line(startLineNo - 1).substring(startColNo);
237            for (int i = startLineNo; i < endLineNo; i++) {
238                returnValue[i - startLineNo + 1] = line(i);
239            }
240            returnValue[returnValue.length - 1] = line(endLineNo - 1).substring(0,
241                    endColNo + 1);
242        }
243        return returnValue;
244    }
245
246    /**
247     * Returns the Javadoc comment before the specified line.
248     * A return value of {@code null} means there is no such comment.
249     * @param lineNoBefore the line number to check before
250     * @return the Javadoc comment, or {@code null} if none
251     **/
252    public TextBlock getJavadocBefore(int lineNoBefore) {
253        // Lines start at 1 to the callers perspective, so need to take off 2
254        int lineNo = lineNoBefore - 2;
255
256        // skip blank lines
257        while (lineNo > 0 && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
258            lineNo--;
259        }
260
261        return javadocComments.get(lineNo);
262    }
263
264    /**
265     * Get a single line.
266     * For internal use only, as getText().get(lineNo) is just as
267     * suitable for external use and avoids method duplication.
268     * @param lineNo the number of the line to get
269     * @return the corresponding line, without terminator
270     * @throws IndexOutOfBoundsException if lineNo is invalid
271     */
272    private String line(int lineNo) {
273        return text.get(lineNo);
274    }
275
276    /**
277     * Get the full text of the file.
278     * @return an object containing the full text of the file
279     */
280    public FileText getText() {
281        return new FileText(text);
282    }
283
284    /**
285     * Gets the lines in the file.
286     * @return the lines in the file
287     */
288    public String[] getLines() {
289        return text.toLinesArray();
290    }
291
292    /**
293     * Get the line from text of the file.
294     * @param index index of the line
295     * @return line from text of the file
296     */
297    public String getLine(int index) {
298        return text.get(index);
299    }
300
301    /**
302     * Gets the name of the file.
303     * @return the name of the file
304     */
305    public String getFileName() {
306        return fileName;
307    }
308
309    /**
310     * Getter.
311     * @return the name of the file
312     * @deprecated use {@link #getFileName} instead
313     */
314    @Deprecated
315    public String getFilename() {
316        return fileName;
317    }
318
319    /**
320     * Checks if the specified line is blank.
321     * @param lineNo the line number to check
322     * @return if the specified line consists only of tabs and spaces.
323     **/
324    public boolean lineIsBlank(int lineNo) {
325        // possible improvement: avoid garbage creation in trim()
326        return line(lineNo).trim().isEmpty();
327    }
328
329    /**
330     * Checks if the specified line is a single-line comment without code.
331     * @param lineNo  the line number to check
332     * @return if the specified line consists of only a single line comment
333     *         without code.
334     **/
335    public boolean lineIsComment(int lineNo) {
336        return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
337    }
338
339    /**
340     * Checks if the specified position intersects with a comment.
341     * @param startLineNo the starting line number
342     * @param startColNo the starting column number
343     * @param endLineNo the ending line number
344     * @param endColNo the ending column number
345     * @return true if the positions intersects with a comment.
346     **/
347    public boolean hasIntersectionWithComment(int startLineNo,
348            int startColNo, int endLineNo, int endColNo) {
349        return hasIntersectionWithBlockComment(startLineNo, startColNo, endLineNo, endColNo)
350                || hasIntersectionWithSingleLineComment(startLineNo, startColNo, endLineNo,
351                        endColNo);
352    }
353
354    /**
355     * Checks if the current file is a package-info.java file.
356     * @return true if the package file.
357     */
358    public boolean inPackageInfo() {
359        return fileName.endsWith("package-info.java");
360    }
361
362    /**
363     * Checks if the specified position intersects with a block comment.
364     * @param startLineNo the starting line number
365     * @param startColNo the starting column number
366     * @param endLineNo the ending line number
367     * @param endColNo the ending column number
368     * @return true if the positions intersects with a block comment.
369     */
370    private boolean hasIntersectionWithBlockComment(int startLineNo, int startColNo,
371            int endLineNo, int endColNo) {
372        boolean hasIntersection = false;
373        // Check C comments (all comments should be checked)
374        final Collection<List<TextBlock>> values = clangComments.values();
375        for (final List<TextBlock> row : values) {
376            for (final TextBlock comment : row) {
377                if (comment.intersects(startLineNo, startColNo, endLineNo, endColNo)) {
378                    hasIntersection = true;
379                    break;
380                }
381            }
382            if (hasIntersection) {
383                break;
384            }
385        }
386        return hasIntersection;
387    }
388
389    /**
390     * Checks if the specified position intersects with a single line comment.
391     * @param startLineNo the starting line number
392     * @param startColNo the starting column number
393     * @param endLineNo the ending line number
394     * @param endColNo the ending column number
395     * @return true if the positions intersects with a single line comment.
396     */
397    private boolean hasIntersectionWithSingleLineComment(int startLineNo, int startColNo,
398            int endLineNo, int endColNo) {
399        boolean hasIntersection = false;
400        // Check CPP comments (line searching is possible)
401        for (int lineNumber = startLineNo; lineNumber <= endLineNo;
402             lineNumber++) {
403            final TextBlock comment = cppComments.get(lineNumber);
404            if (comment != null && comment.intersects(startLineNo, startColNo,
405                    endLineNo, endColNo)) {
406                hasIntersection = true;
407                break;
408            }
409        }
410        return hasIntersection;
411    }
412}