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 mFilename;
052
053    /** the text */
054    private final FileText mText;
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> mJavadocComments = Maps.newHashMap();
060    /** map of the C++ comments indexed on the first line of the comment. */
061    private final Map<Integer, TextBlock> mCPlusPlusComments =
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>> mCComments = Maps.newHashMap();
069
070    /**
071     * Creates a new <code>FileContents</code> instance.
072     *
073     * @param aFilename name of the file
074     * @param aLines 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 aFilename, String[] aLines)
079    {
080        mFilename = aFilename;
081        mText = FileText.fromLines(new File(aFilename), Arrays.asList(aLines));
082    }
083
084    /**
085     * Creates a new <code>FileContents</code> instance.
086     *
087     * @param aText the contents of the file
088     */
089    public FileContents(FileText aText)
090    {
091        mFilename = aText.getFile().toString();
092        mText = aText;
093    }
094
095    /** {@inheritDoc} */
096    public void reportSingleLineComment(String aType, int aStartLineNo,
097            int aStartColNo)
098    {
099        reportCppComment(aStartLineNo, aStartColNo);
100    }
101
102    /** {@inheritDoc} */
103    public void reportBlockComment(String aType, int aStartLineNo,
104            int aStartColNo, int aEndLineNo, int aEndColNo)
105    {
106        reportCComment(aStartLineNo, aStartColNo, aEndLineNo, aEndColNo);
107    }
108
109    /**
110     * Report the location of a C++ style comment.
111     * @param aStartLineNo the starting line number
112     * @param aStartColNo the starting column number
113     **/
114    public void reportCppComment(int aStartLineNo, int aStartColNo)
115    {
116        final String line = line(aStartLineNo - 1);
117        final String[] txt = new String[] {line.substring(aStartColNo)};
118        final Comment comment = new Comment(txt, aStartColNo, aStartLineNo,
119                line.length() - 1);
120        mCPlusPlusComments.put(aStartLineNo, comment);
121    }
122
123    /**
124     * Returns a map of all the C++ style comments. The key is a line number,
125     * the value is the comment {@link TextBlock} at the line.
126     * @return the Map of comments
127     */
128    public ImmutableMap<Integer, TextBlock> getCppComments()
129    {
130        return ImmutableMap.copyOf(mCPlusPlusComments);
131    }
132
133    /**
134     * Report the location of a C-style comment.
135     * @param aStartLineNo the starting line number
136     * @param aStartColNo the starting column number
137     * @param aEndLineNo the ending line number
138     * @param aEndColNo the ending column number
139     **/
140    public void reportCComment(int aStartLineNo, int aStartColNo,
141            int aEndLineNo, int aEndColNo)
142    {
143        final String[] cc = extractCComment(aStartLineNo, aStartColNo,
144                aEndLineNo, aEndColNo);
145        final Comment comment = new Comment(cc, aStartColNo, aEndLineNo,
146                aEndColNo);
147
148        // save the comment
149        if (mCComments.containsKey(aStartLineNo)) {
150            final List<TextBlock> entries = mCComments.get(aStartLineNo);
151            entries.add(comment);
152        }
153        else {
154            final List<TextBlock> entries = Lists.newArrayList();
155            entries.add(comment);
156            mCComments.put(aStartLineNo, entries);
157        }
158
159        // Remember if possible Javadoc comment
160        if (line(aStartLineNo - 1).indexOf("/**", aStartColNo) != -1) {
161            mJavadocComments.put(aEndLineNo - 1, comment);
162        }
163    }
164
165    /**
166     * Returns a map of all C style comments. The key is the line number, the
167     * value is a {@link List} of C style comment {@link TextBlock}s
168     * that start at that line.
169     * @return the map of comments
170     */
171    public ImmutableMap<Integer, List<TextBlock>> getCComments()
172    {
173        return ImmutableMap.copyOf(mCComments);
174    }
175
176    /**
177     * Returns the specified C comment as a String array.
178     * @return C comment as a array
179     * @param aStartLineNo the starting line number
180     * @param aStartColNo the starting column number
181     * @param aEndLineNo the ending line number
182     * @param aEndColNo the ending column number
183     **/
184    private String[] extractCComment(int aStartLineNo, int aStartColNo,
185            int aEndLineNo, int aEndColNo)
186    {
187        String[] retVal;
188        if (aStartLineNo == aEndLineNo) {
189            retVal = new String[1];
190            retVal[0] = line(aStartLineNo - 1).substring(aStartColNo,
191                    aEndColNo + 1);
192        }
193        else {
194            retVal = new String[aEndLineNo - aStartLineNo + 1];
195            retVal[0] = line(aStartLineNo - 1).substring(aStartColNo);
196            for (int i = aStartLineNo; i < aEndLineNo; i++) {
197                retVal[i - aStartLineNo + 1] = line(i);
198            }
199            retVal[retVal.length - 1] = line(aEndLineNo - 1).substring(0,
200                    aEndColNo + 1);
201        }
202        return retVal;
203    }
204
205    /**
206     * Returns the Javadoc comment before the specified line.
207     * A return value of <code>null</code> means there is no such comment.
208     * @return the Javadoc comment, or <code>null</code> if none
209     * @param aLineNo the line number to check before
210     **/
211    public TextBlock getJavadocBefore(int aLineNo)
212    {
213        // Lines start at 1 to the callers perspective, so need to take off 2
214        int lineNo = aLineNo - 2;
215
216        // skip blank lines
217        while ((lineNo > 0) && (lineIsBlank(lineNo) || lineIsComment(lineNo))) {
218            lineNo--;
219        }
220
221        return mJavadocComments.get(lineNo);
222    }
223
224    /**
225     * Get a single line.
226     * For internal use only, as getText().get(lineNo) is just as
227     * suitable for external use and avoids method duplication.
228     * @param aLineNo the number of the line to get
229     * @return the corresponding line, without terminator
230     * @throws IndexOutOfBoundsException if lineNo is invalid
231     */
232    private String line(int aLineNo)
233    {
234        return mText.get(aLineNo);
235    }
236
237    /**
238     * Get the full text of the file.
239     * @return an object containing the full text of the file
240     */
241    public FileText getText()
242    {
243        return mText;
244    }
245
246    /** @return the lines in the file */
247    public String[] getLines()
248    {
249        return mText.toLinesArray();
250    }
251
252    /** @return the name of the file */
253    public String getFilename()
254    {
255        return mFilename;
256    }
257
258    /**
259     * Checks if the specified line is blank.
260     * @param aLineNo the line number to check
261     * @return if the specified line consists only of tabs and spaces.
262     **/
263    public boolean lineIsBlank(int aLineNo)
264    {
265        // possible improvement: avoid garbage creation in trim()
266        return "".equals(line(aLineNo).trim());
267    }
268
269    /**
270     * Checks if the specified line is a single-line comment without code.
271     * @param aLineNo  the line number to check
272     * @return if the specified line consists of only a single line comment
273     *         without code.
274     **/
275    public boolean lineIsComment(int aLineNo)
276    {
277        return MATCH_SINGLELINE_COMMENT.matcher(line(aLineNo)).matches();
278    }
279
280    /**
281     * Checks if the specified position intersects with a comment.
282     * @param aStartLineNo the starting line number
283     * @param aStartColNo the starting column number
284     * @param aEndLineNo the ending line number
285     * @param aEndColNo the ending column number
286     * @return true if the positions intersects with a comment.
287     **/
288    public boolean hasIntersectionWithComment(int aStartLineNo,
289            int aStartColNo, int aEndLineNo, int aEndColNo)
290    {
291        // Check C comments (all comments should be checked)
292        final Collection<List<TextBlock>> values = mCComments.values();
293        for (final List<TextBlock> row : values) {
294            for (final TextBlock comment : row) {
295                if (comment.intersects(aStartLineNo, aStartColNo, aEndLineNo,
296                        aEndColNo))
297                {
298                    return true;
299                }
300            }
301        }
302
303        // Check CPP comments (line searching is possible)
304        for (int lineNumber = aStartLineNo; lineNumber <= aEndLineNo;
305             lineNumber++)
306        {
307            final TextBlock comment = mCPlusPlusComments.get(lineNumber);
308            if ((comment != null)
309                    && comment.intersects(aStartLineNo, aStartColNo,
310                            aEndLineNo, aEndColNo))
311            {
312                return true;
313            }
314        }
315        return false;
316    }
317
318    /**
319     * Checks if the current file is a package-info.java file.
320     * @return true if the package file.
321     */
322    public boolean inPackageInfo()
323    {
324        return this.getFilename().endsWith("package-info.java");
325    }
326}