001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle.api;
020
021import com.google.common.collect.ImmutableMap;
022
023import com.google.common.collect.Lists;
024import com.google.common.collect.Maps;
025import com.puppycrawl.tools.checkstyle.grammars.CommentListener;
026import java.io.File;
027import java.util.Arrays;
028import java.util.Collection;
029import java.util.List;
030import java.util.Map;
031import java.util.regex.Pattern;
032
033/**
034 * Represents the contents of a file.
035 *
036 * @author Oliver Burn
037 * @version 1.0
038 */
039public final class FileContents implements CommentListener
040{
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 = Maps.newHashMap();
060    /** map of the C++ comments indexed on the first line of the comment. */
061    private final Map<Integer, TextBlock> cppComments =
062        Maps.newHashMap();
063
064    /**
065     * map of the C comments indexed on the first line of the comment to a list
066     * of comments on that line
067     */
068    private final Map<Integer, List<TextBlock>> clangComments = Maps.newHashMap();
069
070    /**
071     * Creates a new <code>FileContents</code> instance.
072     *
073     * @param filename name of the file
074     * @param lines the contents of the file
075     * @deprecated Use {@link #FileContents(FileText)} instead
076     *   in order to preserve the original line breaks where possible.
077     */
078    @Deprecated public FileContents(String filename, String[] lines)
079    {
080        this.filename = filename;
081        text = FileText.fromLines(new File(filename), Arrays.asList(lines));
082    }
083
084    /**
085     * Creates a new <code>FileContents</code> instance.
086     *
087     * @param text the contents of the file
088     */
089    public FileContents(FileText text)
090    {
091        filename = text.getFile().toString();
092        this.text = text;
093    }
094
095    /** {@inheritDoc} */
096    @Override
097    public void reportSingleLineComment(String type, int startLineNo,
098            int startColNo)
099    {
100        reportCppComment(startLineNo, startColNo);
101    }
102
103    /** {@inheritDoc} */
104    @Override
105    public void reportBlockComment(String type, int startLineNo,
106            int startColNo, int endLineNo, int endColNo)
107    {
108        reportCComment(startLineNo, startColNo, endLineNo, endColNo);
109    }
110
111    /**
112     * Report the location of a C++ style comment.
113     * @param startLineNo the starting line number
114     * @param startColNo the starting column number
115     **/
116    public void reportCppComment(int startLineNo, int startColNo)
117    {
118        final String line = line(startLineNo - 1);
119        final String[] txt = new String[] {line.substring(startColNo)};
120        final Comment comment = new Comment(txt, startColNo, startLineNo,
121                line.length() - 1);
122        cppComments.put(startLineNo, comment);
123    }
124
125    /**
126     * Returns a map of all the C++ style comments. The key is a line number,
127     * the value is the comment {@link TextBlock} at the line.
128     * @return the Map of comments
129     */
130    public ImmutableMap<Integer, TextBlock> getCppComments()
131    {
132        return ImmutableMap.copyOf(cppComments);
133    }
134
135    /**
136     * Report the location of a C-style comment.
137     * @param startLineNo the starting line number
138     * @param startColNo the starting column number
139     * @param endLineNo the ending line number
140     * @param endColNo the ending column number
141     **/
142    public void reportCComment(int startLineNo, int startColNo,
143            int endLineNo, int endColNo)
144    {
145        final String[] cc = extractCComment(startLineNo, startColNo,
146                endLineNo, endColNo);
147        final Comment comment = new Comment(cc, startColNo, endLineNo,
148                endColNo);
149
150        // save the comment
151        if (clangComments.containsKey(startLineNo)) {
152            final List<TextBlock> entries = clangComments.get(startLineNo);
153            entries.add(comment);
154        }
155        else {
156            final List<TextBlock> entries = Lists.newArrayList();
157            entries.add(comment);
158            clangComments.put(startLineNo, entries);
159        }
160
161        // Remember if possible Javadoc comment
162        if (line(startLineNo - 1).indexOf("/**", startColNo) != -1) {
163            javadocComments.put(endLineNo - 1, comment);
164        }
165    }
166
167    /**
168     * Returns a map of all C style comments. The key is the line number, the
169     * value is a {@link List} of C style comment {@link TextBlock}s
170     * that start at that line.
171     * @return the map of comments
172     */
173    public ImmutableMap<Integer, List<TextBlock>> getCComments()
174    {
175        return ImmutableMap.copyOf(clangComments);
176    }
177
178    /**
179     * Returns the specified C comment as a String array.
180     * @return C comment as a array
181     * @param startLineNo the starting line number
182     * @param startColNo the starting column number
183     * @param endLineNo the ending line number
184     * @param endColNo the ending column number
185     **/
186    private String[] extractCComment(int startLineNo, int startColNo,
187            int endLineNo, int endColNo)
188    {
189        String[] retVal;
190        if (startLineNo == endLineNo) {
191            retVal = new String[1];
192            retVal[0] = line(startLineNo - 1).substring(startColNo,
193                    endColNo + 1);
194        }
195        else {
196            retVal = new String[endLineNo - startLineNo + 1];
197            retVal[0] = line(startLineNo - 1).substring(startColNo);
198            for (int i = startLineNo; i < endLineNo; i++) {
199                retVal[i - startLineNo + 1] = line(i);
200            }
201            retVal[retVal.length - 1] = line(endLineNo - 1).substring(0,
202                    endColNo + 1);
203        }
204        return retVal;
205    }
206
207    /**
208     * Returns the Javadoc comment before the specified line.
209     * A return value of <code>null</code> means there is no such comment.
210     * @return the Javadoc comment, or <code>null</code> if none
211     * @param lineNoBefore the line number to check before
212     **/
213    public TextBlock getJavadocBefore(int lineNoBefore)
214    {
215        // Lines start at 1 to the callers perspective, so need to take off 2
216        int lineNo = lineNoBefore - 2;
217
218        // skip blank lines
219        while ((lineNo > 0) && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
220            lineNo--;
221        }
222
223        return javadocComments.get(lineNo);
224    }
225
226    /**
227     * Get a single line.
228     * For internal use only, as getText().get(lineNo) is just as
229     * suitable for external use and avoids method duplication.
230     * @param lineNo the number of the line to get
231     * @return the corresponding line, without terminator
232     * @throws IndexOutOfBoundsException if lineNo is invalid
233     */
234    private String line(int lineNo)
235    {
236        return text.get(lineNo);
237    }
238
239    /**
240     * Get the full text of the file.
241     * @return an object containing the full text of the file
242     */
243    public FileText getText()
244    {
245        return text;
246    }
247
248    /** @return the lines in the file */
249    public String[] getLines()
250    {
251        return text.toLinesArray();
252    }
253
254    /**
255     * Get the line from text of the file.
256     * @param index index of the line
257     * @return line from text of the file
258     */
259    public String getLine(int index)
260    {
261        return text.get(index);
262    }
263
264    /** @return the name of the file */
265    public String getFilename()
266    {
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    {
277        // possible improvement: avoid garbage creation in trim()
278        return "".equals(line(lineNo).trim());
279    }
280
281    /**
282     * Checks if the specified line is a single-line comment without code.
283     * @param lineNo  the line number to check
284     * @return if the specified line consists of only a single line comment
285     *         without code.
286     **/
287    public boolean lineIsComment(int lineNo)
288    {
289        return MATCH_SINGLELINE_COMMENT.matcher(line(lineNo)).matches();
290    }
291
292    /**
293     * Checks if the specified position intersects with a comment.
294     * @param startLineNo the starting line number
295     * @param startColNo the starting column number
296     * @param endLineNo the ending line number
297     * @param endColNo the ending column number
298     * @return true if the positions intersects with a comment.
299     **/
300    public boolean hasIntersectionWithComment(int startLineNo,
301            int startColNo, int endLineNo, int endColNo)
302    {
303        // Check C comments (all comments should be checked)
304        final Collection<List<TextBlock>> values = clangComments.values();
305        for (final List<TextBlock> row : values) {
306            for (final TextBlock comment : row) {
307                if (comment.intersects(startLineNo, startColNo, endLineNo,
308                        endColNo))
309                {
310                    return true;
311                }
312            }
313        }
314
315        // Check CPP comments (line searching is possible)
316        for (int lineNumber = startLineNo; lineNumber <= endLineNo;
317             lineNumber++)
318        {
319            final TextBlock comment = cppComments.get(lineNumber);
320            if ((comment != null)
321                    && comment.intersects(startLineNo, startColNo,
322                            endLineNo, endColNo))
323            {
324                return true;
325            }
326        }
327        return false;
328    }
329
330    /**
331     * Checks if the current file is a package-info.java file.
332     * @return true if the package file.
333     */
334    public boolean inPackageInfo()
335    {
336        return this.getFilename().endsWith("package-info.java");
337    }
338}