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