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;
033import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
034
035/**
036 * Represents the contents of a file.
037 *
038 * @author Oliver Burn
039 */
040public final class FileContents implements CommentListener {
041    /**
042     * The pattern to match a single line comment containing only the comment
043     * itself -- no code.
044     */
045    private static final String MATCH_SINGLELINE_COMMENT_PAT = "^\\s*//.*$";
046    /** Compiled regexp to match a single-line comment line. */
047    private static final Pattern MATCH_SINGLELINE_COMMENT = Pattern
048            .compile(MATCH_SINGLELINE_COMMENT_PAT);
049
050    /** The file name. */
051    private final String fileName;
052
053    /** The text. */
054    private final FileText text;
055
056    /** Map of the Javadoc comments indexed on the last line of the comment.
057     * The hack is it assumes that there is only one Javadoc comment per line.
058     */
059    private final Map<Integer, TextBlock> javadocComments = new HashMap<>();
060    /** Map of the C++ comments indexed on the first line of the comment. */
061    private final Map<Integer, TextBlock> cppComments = new HashMap<>();
062
063    /**
064     * Map of the C comments indexed on the first line of the comment to a list
065     * of comments on that line.
066     */
067    private final Map<Integer, List<TextBlock>> clangComments = new HashMap<>();
068
069    /**
070     * Creates a new {@code FileContents} instance.
071     *
072     * @param filename name of the file
073     * @param lines the contents of the file
074     * @deprecated Use {@link #FileContents(FileText)} instead
075     *     in order to preserve the original line breaks where possible.
076     */
077    @Deprecated
078    public FileContents(String filename, String... lines) {
079        fileName = filename;
080        text = new FileText(new File(filename), Arrays.asList(lines));
081    }
082
083    /**
084     * Creates a new {@code FileContents} instance.
085     *
086     * @param text the contents of the file
087     */
088    public FileContents(FileText text) {
089        fileName = text.getFile().toString();
090        this.text = new FileText(text);
091    }
092
093    @Override
094    public void reportSingleLineComment(String type, int startLineNo,
095            int startColNo) {
096        reportSingleLineComment(startLineNo, startColNo);
097    }
098
099    /**
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 ImmutableMap<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 ImmutableMap<Integer, TextBlock> getSingleLineComments() {
178        return ImmutableMap.copyOf(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 ImmutableMap<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 ImmutableMap<Integer, List<TextBlock>> getBlockComments() {
216        return ImmutableMap.copyOf(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 CommonUtils.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}