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